@jqhtml/parser 2.2.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/bin/jqhtml-compile +218 -0
- package/dist/ast.d.ts +102 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +30 -0
- package/dist/ast.js.map +1 -0
- package/dist/codegen.d.ts +108 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +1377 -0
- package/dist/codegen.js.map +1 -0
- package/dist/compiler.d.ts +25 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +273 -0
- package/dist/compiler.js.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +155 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +3 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +47 -0
- package/dist/integration.js.map +1 -0
- package/dist/lexer.d.ts +123 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +1446 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser.d.ts +56 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +845 -0
- package/dist/parser.js.map +1 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +106 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +60 -0
package/dist/codegen.js
ADDED
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
// JQHTML Code Generator - Converts AST to JavaScript functions
|
|
2
|
+
// Generates v1-compatible instruction arrays for efficient DOM construction
|
|
3
|
+
import { NodeType } from './ast.js';
|
|
4
|
+
import { SourceMapGenerator } from 'source-map';
|
|
5
|
+
import { JQHTMLParseError } from './errors.js';
|
|
6
|
+
import * as vm from 'vm';
|
|
7
|
+
export class CodeGenerator {
|
|
8
|
+
indent_level = 0;
|
|
9
|
+
components = new Map();
|
|
10
|
+
current_component = null;
|
|
11
|
+
in_slot = false;
|
|
12
|
+
tag_depth = 0;
|
|
13
|
+
lastOutput = ''; // Track last generated output for deduplication
|
|
14
|
+
// Position tracking for source maps
|
|
15
|
+
outputLine = 1;
|
|
16
|
+
outputColumn = 0;
|
|
17
|
+
sourceMapGenerator;
|
|
18
|
+
sourceContent;
|
|
19
|
+
sourceFile;
|
|
20
|
+
outputBuffer = [];
|
|
21
|
+
// Enable position tracking for debugging
|
|
22
|
+
enablePositionTracking = false;
|
|
23
|
+
positionLog = [];
|
|
24
|
+
// Line preservation for 1:1 source mapping
|
|
25
|
+
currentOutputLine = 1;
|
|
26
|
+
preserveLines = false;
|
|
27
|
+
// For true 1:1 line mapping
|
|
28
|
+
outputLines = [];
|
|
29
|
+
sourceLines = [];
|
|
30
|
+
generate(ast, sourceFile, sourceContent) {
|
|
31
|
+
// Reset state
|
|
32
|
+
this.components.clear();
|
|
33
|
+
this.current_component = null;
|
|
34
|
+
this.tag_depth = 0;
|
|
35
|
+
this.resetPositionTracking();
|
|
36
|
+
// Store source info for error reporting
|
|
37
|
+
this.sourceFile = sourceFile;
|
|
38
|
+
this.sourceContent = sourceContent;
|
|
39
|
+
// Process all top-level nodes
|
|
40
|
+
for (const node of ast.body) {
|
|
41
|
+
if (node.type === NodeType.COMPONENT_DEFINITION) {
|
|
42
|
+
this.generate_component(node);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Build the final code
|
|
46
|
+
const code = this.build_module_code();
|
|
47
|
+
return {
|
|
48
|
+
code,
|
|
49
|
+
components: this.components
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Generate code with source maps using 1:1 line mapping + SourceMapGenerator
|
|
54
|
+
*/
|
|
55
|
+
generateWithSourceMap(ast, sourceFile, sourceContent) {
|
|
56
|
+
// Use the regular generate method which creates 1:1 line mapping
|
|
57
|
+
const result = this.generate(ast, sourceFile, sourceContent);
|
|
58
|
+
// Now create a proper sourcemap for each component using SourceMapGenerator
|
|
59
|
+
const componentsWithSourcemaps = new Map();
|
|
60
|
+
for (const [name, component] of result.components) {
|
|
61
|
+
// Create a SourceMapGenerator for this component
|
|
62
|
+
const generator = new SourceMapGenerator({
|
|
63
|
+
file: sourceFile.replace(/\.jqhtml$/, '.js')
|
|
64
|
+
});
|
|
65
|
+
generator.setSourceContent(sourceFile, sourceContent);
|
|
66
|
+
// Parse the render function to find line count
|
|
67
|
+
const renderLines = component.render_function.split('\n');
|
|
68
|
+
// The first line is the function declaration - maps to the Define line (line 1)
|
|
69
|
+
generator.addMapping({
|
|
70
|
+
generated: { line: 1, column: 0 },
|
|
71
|
+
source: sourceFile,
|
|
72
|
+
original: { line: 1, column: 0 }
|
|
73
|
+
});
|
|
74
|
+
// Map ALL lines in the generated output
|
|
75
|
+
// This ensures that even lines beyond the source file length are mapped
|
|
76
|
+
const sourceLines = sourceContent.split('\n');
|
|
77
|
+
const numSourceLines = sourceLines.length;
|
|
78
|
+
// CRITICAL FIX: The render function has a consistent offset from source
|
|
79
|
+
// We need to map with the proper offset, not naive 1:1
|
|
80
|
+
// The parser generates code with a fixed offset (typically around 10-13 lines)
|
|
81
|
+
// Calculate the actual offset by looking at the generated code
|
|
82
|
+
// The render function's first real content line starts around line 2
|
|
83
|
+
// but it maps to source line 1 (the Define line)
|
|
84
|
+
// After that, we need to maintain proper offset mapping
|
|
85
|
+
// For now, use the simple 1:1 mapping with clamping
|
|
86
|
+
// The CLI's adjustSourcemapForCLI will handle the offset adjustment
|
|
87
|
+
for (let lineNum = 2; lineNum <= renderLines.length; lineNum++) {
|
|
88
|
+
// Map each line to its corresponding source line
|
|
89
|
+
// When we go beyond source length, clamp to last source line
|
|
90
|
+
const sourceLine = Math.min(lineNum, numSourceLines);
|
|
91
|
+
generator.addMapping({
|
|
92
|
+
generated: { line: lineNum, column: 0 },
|
|
93
|
+
source: sourceFile,
|
|
94
|
+
original: { line: sourceLine, column: 0 }
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Generate the sourcemap
|
|
98
|
+
const sourcemap = generator.toJSON();
|
|
99
|
+
const sourcemapJson = JSON.stringify(sourcemap);
|
|
100
|
+
const sourcemapBase64 = Buffer.from(sourcemapJson).toString('base64');
|
|
101
|
+
const sourceMapDataUri = `data:application/json;charset=utf-8;base64,${sourcemapBase64}`;
|
|
102
|
+
// Add inline sourcemap to render function
|
|
103
|
+
const renderWithSourcemap = component.render_function +
|
|
104
|
+
'\n//# sourceMappingURL=' + sourceMapDataUri;
|
|
105
|
+
componentsWithSourcemaps.set(name, {
|
|
106
|
+
...component,
|
|
107
|
+
render_function: renderWithSourcemap
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
code: result.code,
|
|
112
|
+
components: componentsWithSourcemaps,
|
|
113
|
+
source_map: JSON.stringify({}), // Not used for inline sourcemaps
|
|
114
|
+
source_map_data_uri: '' // Not used for inline sourcemaps
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
generate_component_with_mappings_TESTING(node) {
|
|
118
|
+
// Extract metadata
|
|
119
|
+
let tagName = 'div';
|
|
120
|
+
const defaultAttributes = {};
|
|
121
|
+
const dependencies = new Set();
|
|
122
|
+
// Extract new fields from Define tag
|
|
123
|
+
const defineArgs = node.defineArgs;
|
|
124
|
+
const extendsValue = node.extends;
|
|
125
|
+
for (const [key, value] of Object.entries(node.attributes)) {
|
|
126
|
+
if (key === 'tag') {
|
|
127
|
+
if (typeof value === 'object' && value.quoted) {
|
|
128
|
+
tagName = value.value;
|
|
129
|
+
}
|
|
130
|
+
else if (typeof value === 'string') {
|
|
131
|
+
tagName = value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
if (typeof value === 'object' && value.quoted) {
|
|
136
|
+
defaultAttributes[key] = value.value;
|
|
137
|
+
}
|
|
138
|
+
else if (typeof value === 'object' && value.expression) {
|
|
139
|
+
defaultAttributes[key] = value.value;
|
|
140
|
+
}
|
|
141
|
+
else if (typeof value === 'object' && value.identifier) {
|
|
142
|
+
defaultAttributes[key] = value.value;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
defaultAttributes[key] = value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Reset output tracking for this component
|
|
150
|
+
this.outputLine = 1;
|
|
151
|
+
this.outputColumn = 0;
|
|
152
|
+
this.outputBuffer = [];
|
|
153
|
+
// Generate function header
|
|
154
|
+
const header = 'function render(data, args, content, jqhtml) { let _output = []; const _cid = this._cid; const that = this;\n';
|
|
155
|
+
this.outputBuffer.push(header);
|
|
156
|
+
this.outputLine = 2; // We're now on line 2 after the header
|
|
157
|
+
// Generate function body - each node's emit() will track its position
|
|
158
|
+
for (const childNode of node.body) {
|
|
159
|
+
const childCode = this.generate_node(childNode);
|
|
160
|
+
if (childCode) {
|
|
161
|
+
// Don't add extra space - let the code maintain its position
|
|
162
|
+
this.outputBuffer.push(childCode);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Add the return statement
|
|
166
|
+
this.outputBuffer.push('return [_output, this]; }');
|
|
167
|
+
// Combine all the output
|
|
168
|
+
const renderFunction = this.outputBuffer.join('');
|
|
169
|
+
// Store the component
|
|
170
|
+
this.components.set(node.name, {
|
|
171
|
+
name: node.name,
|
|
172
|
+
render_function: renderFunction,
|
|
173
|
+
dependencies: Array.from(dependencies),
|
|
174
|
+
tagName,
|
|
175
|
+
defaultAttributes,
|
|
176
|
+
defineArgs,
|
|
177
|
+
extends: extendsValue
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
generate_component(node) {
|
|
181
|
+
this.current_component = node.name;
|
|
182
|
+
this.lastOutput = ''; // Reset output tracking for each component
|
|
183
|
+
const dependencies = new Set();
|
|
184
|
+
// Always use 1:1 line mapping for proper sourcemaps
|
|
185
|
+
// Even when using SourceMapGenerator, we need the line structure
|
|
186
|
+
// Enable line preservation for 1:1 source maps
|
|
187
|
+
this.preserveLines = true;
|
|
188
|
+
this.currentOutputLine = 1;
|
|
189
|
+
// Initialize output lines array for 1:1 mapping
|
|
190
|
+
// We'll build the function line by line to match source structure
|
|
191
|
+
this.outputLines = [];
|
|
192
|
+
// Extract metadata and default attributes
|
|
193
|
+
// Handle the 'tag' attribute which might be a quoted object from the parser
|
|
194
|
+
let tagName = 'div';
|
|
195
|
+
const defaultAttributes = {};
|
|
196
|
+
// Extract new fields from Define tag (extends and defineArgs)
|
|
197
|
+
const defineArgs = node.defineArgs;
|
|
198
|
+
const extendsValue = node.extends;
|
|
199
|
+
// Process all attributes from the Define tag
|
|
200
|
+
for (const [key, value] of Object.entries(node.attributes)) {
|
|
201
|
+
if (key === 'tag') {
|
|
202
|
+
// Special handling for 'tag' attribute
|
|
203
|
+
if (typeof value === 'object' && value.quoted) {
|
|
204
|
+
tagName = value.value;
|
|
205
|
+
}
|
|
206
|
+
else if (typeof value === 'string') {
|
|
207
|
+
tagName = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
// Store as default attribute - extract actual value from quoted objects
|
|
212
|
+
if (typeof value === 'object' && value.quoted) {
|
|
213
|
+
defaultAttributes[key] = value.value;
|
|
214
|
+
}
|
|
215
|
+
else if (typeof value === 'object' && value.expression) {
|
|
216
|
+
// For expressions, we need to store them as is to be evaluated at runtime
|
|
217
|
+
defaultAttributes[key] = value.value;
|
|
218
|
+
}
|
|
219
|
+
else if (typeof value === 'object' && value.identifier) {
|
|
220
|
+
// For identifiers, store as is to be evaluated at runtime
|
|
221
|
+
defaultAttributes[key] = value.value;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
defaultAttributes[key] = value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Check if this is a slot-only template (template inheritance pattern)
|
|
229
|
+
if (node.isSlotOnly && node.slotNames && node.slotNames.length > 0) {
|
|
230
|
+
// Generate slot-only render function with 1:1 line mapping
|
|
231
|
+
// Extract slot nodes from body
|
|
232
|
+
const slots = {};
|
|
233
|
+
for (const child of node.body) {
|
|
234
|
+
if (child.type === NodeType.SLOT) {
|
|
235
|
+
const slotNode = child;
|
|
236
|
+
slots[slotNode.name] = slotNode;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Filter out TEXT nodes (whitespace) from slot-only templates
|
|
240
|
+
// TEXT nodes cause _output.push() calls which are invalid in slot-only context
|
|
241
|
+
const slotsOnly = node.body.filter(child => child.type === NodeType.SLOT);
|
|
242
|
+
// Use 1:1 line mapping for slot-only templates
|
|
243
|
+
const bodyLines = this.generate_function_body_1to1(slotsOnly);
|
|
244
|
+
// Build the render function with line preservation
|
|
245
|
+
const lines = [];
|
|
246
|
+
// Line 1: function declaration returning slots object
|
|
247
|
+
lines.push(`function render(data, args, content, jqhtml) { return [{_slots: {`);
|
|
248
|
+
// Lines 2-N: Body lines with slot functions
|
|
249
|
+
lines.push(...bodyLines);
|
|
250
|
+
// Fix trailing comma on last slot function
|
|
251
|
+
// Find the last line that contains '.bind(this),' and remove the comma
|
|
252
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
253
|
+
if (lines[i] && lines[i].includes('.bind(this),')) {
|
|
254
|
+
lines[i] = lines[i].replace(/\.bind\(this\),([^,]*)$/, '.bind(this)$1');
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Last line: close slots object and return
|
|
259
|
+
if (lines[lines.length - 1]) {
|
|
260
|
+
lines[lines.length - 1] += ' }}, this]; }';
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
lines[lines.length - 1] = '}}, this]; }';
|
|
264
|
+
}
|
|
265
|
+
const render_function = lines.join('\n');
|
|
266
|
+
// Store component with slot-only metadata
|
|
267
|
+
this.components.set(node.name, {
|
|
268
|
+
name: node.name,
|
|
269
|
+
render_function,
|
|
270
|
+
dependencies: Array.from(dependencies),
|
|
271
|
+
tagName,
|
|
272
|
+
defaultAttributes,
|
|
273
|
+
defineArgs,
|
|
274
|
+
extends: extendsValue
|
|
275
|
+
});
|
|
276
|
+
this.current_component = null;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
// For 1:1 line mapping, we need to generate the body first to know how many lines it has
|
|
280
|
+
const bodyLines = this.generate_function_body_1to1(node.body);
|
|
281
|
+
// Now create the render function with 1:1 line mapping
|
|
282
|
+
// Line 1 of input (Define tag) becomes the function declaration
|
|
283
|
+
const lines = [];
|
|
284
|
+
// First line: function declaration and initial setup (corresponds to Define line)
|
|
285
|
+
lines.push(`function render(data, args, content, jqhtml) { let _output = []; const _cid = this._cid; const that = this;`);
|
|
286
|
+
// Body lines: each corresponds to a source line
|
|
287
|
+
lines.push(...bodyLines);
|
|
288
|
+
// Last line: closing (corresponds to closing Define tag)
|
|
289
|
+
if (lines[lines.length - 1]) {
|
|
290
|
+
lines[lines.length - 1] += ' return [_output, this]; }';
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
lines[lines.length - 1] = 'return [_output, this]; }';
|
|
294
|
+
}
|
|
295
|
+
const render_function = lines.join('\n');
|
|
296
|
+
// Validate the generated function syntax using vm.Script for better error reporting
|
|
297
|
+
try {
|
|
298
|
+
const generatedCode = `
|
|
299
|
+
let _output = [];
|
|
300
|
+
const _cid = this._cid;
|
|
301
|
+
const that = this;
|
|
302
|
+
${bodyLines.join('\n')}
|
|
303
|
+
`;
|
|
304
|
+
new vm.Script(generatedCode, {
|
|
305
|
+
filename: this.sourceFile || 'component.jqhtml'
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
// Extract JavaScript error details
|
|
310
|
+
const errorDetail = error.message || String(error);
|
|
311
|
+
let sourceLine = 1; // Default to component definition line
|
|
312
|
+
let sourceColumn = 0;
|
|
313
|
+
// Extract line number from vm.Script stack trace
|
|
314
|
+
// Stack format: "filename.jqhtml:LINE\n..." or "evalmachine.<anonymous>:LINE\n..."
|
|
315
|
+
if (error.stack && typeof error.stack === 'string') {
|
|
316
|
+
// Extract line number from first line of stack (before first newline)
|
|
317
|
+
const firstLine = error.stack.split('\n')[0];
|
|
318
|
+
const lineMatch = firstLine.match(/:(\d+)/);
|
|
319
|
+
if (lineMatch) {
|
|
320
|
+
const generatedLine = parseInt(lineMatch[1], 10);
|
|
321
|
+
// Map generated line to source line
|
|
322
|
+
// The bodyLines array has 1:1 correspondence with source lines
|
|
323
|
+
// Generated code includes 3-line header, so subtract that offset
|
|
324
|
+
const headerOffset = 3; // "let _output = [];\n const _cid = this._cid;\n const that = this;"
|
|
325
|
+
const bodyLineIndex = generatedLine - headerOffset - 1; // Convert to 0-based
|
|
326
|
+
if (bodyLineIndex >= 0 && bodyLineIndex < bodyLines.length) {
|
|
327
|
+
// Map to actual source line (bodyLines starts at source line 2)
|
|
328
|
+
// Subtract 1 because syntax errors typically get detected on the line AFTER the problem
|
|
329
|
+
sourceLine = Math.max(1, bodyLineIndex + 2 - 1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Build error message emphasizing unbalanced brackets
|
|
334
|
+
// Keep the helpful context about what to check for
|
|
335
|
+
const errorMessage = `Unbalanced bracket error in component "${node.name}": ${errorDetail}\n\nCheck for unclosed brackets, missing quotes, or invalid JavaScript syntax.`;
|
|
336
|
+
// Create proper JQHTMLParseError with file/line info for Laravel Ignition integration
|
|
337
|
+
throw new JQHTMLParseError(errorMessage, sourceLine, sourceColumn, this.sourceContent, this.sourceFile);
|
|
338
|
+
}
|
|
339
|
+
this.components.set(node.name, {
|
|
340
|
+
name: node.name,
|
|
341
|
+
render_function,
|
|
342
|
+
dependencies: Array.from(dependencies),
|
|
343
|
+
tagName, // Store metadata
|
|
344
|
+
defaultAttributes, // Store default attributes
|
|
345
|
+
defineArgs, // Store $ attributes from Define tag
|
|
346
|
+
extends: extendsValue // Store extends value for template inheritance
|
|
347
|
+
});
|
|
348
|
+
this.current_component = null;
|
|
349
|
+
}
|
|
350
|
+
generate_function_body(nodes, preserveLines = false) {
|
|
351
|
+
// For simplicity and better sourcemaps, generate everything on minimal lines
|
|
352
|
+
// This matches the rspade team's recommendation for 1:1 line mapping
|
|
353
|
+
const statements = [];
|
|
354
|
+
for (const node of nodes) {
|
|
355
|
+
const code = this.generate_node(node);
|
|
356
|
+
if (code) {
|
|
357
|
+
statements.push(code);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Join all statements on a single line to minimize output lines
|
|
361
|
+
// This makes sourcemap generation much simpler
|
|
362
|
+
return statements.join(' ');
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Generate function body with true 1:1 line mapping
|
|
366
|
+
* Each line in the source produces exactly one line in the output
|
|
367
|
+
*/
|
|
368
|
+
generate_function_body_1to1(nodes) {
|
|
369
|
+
// Find the max line number in the template body
|
|
370
|
+
let maxLine = 0;
|
|
371
|
+
const findMaxLine = (node) => {
|
|
372
|
+
if (node.line && node.line > maxLine) {
|
|
373
|
+
maxLine = node.line;
|
|
374
|
+
}
|
|
375
|
+
// Check various node types for children
|
|
376
|
+
if (node.body)
|
|
377
|
+
node.body.forEach(findMaxLine);
|
|
378
|
+
if (node.children)
|
|
379
|
+
node.children.forEach(findMaxLine);
|
|
380
|
+
if (node.consequent)
|
|
381
|
+
node.consequent.forEach(findMaxLine);
|
|
382
|
+
if (node.alternate)
|
|
383
|
+
node.alternate.forEach(findMaxLine);
|
|
384
|
+
};
|
|
385
|
+
nodes.forEach(findMaxLine);
|
|
386
|
+
// Initialize lines array with empty strings for each source line
|
|
387
|
+
const lines = [];
|
|
388
|
+
for (let i = 2; i <= maxLine; i++) {
|
|
389
|
+
lines.push('');
|
|
390
|
+
}
|
|
391
|
+
// Process nodes and build output line by line
|
|
392
|
+
// We need to handle nodes differently based on their type
|
|
393
|
+
const processNodeForLine = (node) => {
|
|
394
|
+
if (!node.line)
|
|
395
|
+
return;
|
|
396
|
+
const lineIndex = node.line - 2; // Adjust for array index (line 2 = index 0)
|
|
397
|
+
if (lineIndex < 0 || lineIndex >= lines.length)
|
|
398
|
+
return;
|
|
399
|
+
// Generate code based on node type
|
|
400
|
+
switch (node.type) {
|
|
401
|
+
case NodeType.HTML_TAG: {
|
|
402
|
+
this.lastOutput = ''; // Reset for non-text output
|
|
403
|
+
const tag = node;
|
|
404
|
+
// Check if this is a raw content tag (textarea, pre)
|
|
405
|
+
if (tag.preserveWhitespace && !tag.selfClosing && tag.children && tag.children.length > 0) {
|
|
406
|
+
// Validate: only TEXT, EXPRESSION, and CODE_BLOCK children allowed in raw content tags
|
|
407
|
+
// HTML tags and components are not allowed (they would break whitespace preservation)
|
|
408
|
+
for (const child of tag.children) {
|
|
409
|
+
if (child.type !== NodeType.TEXT &&
|
|
410
|
+
child.type !== NodeType.EXPRESSION &&
|
|
411
|
+
child.type !== NodeType.CODE_BLOCK) {
|
|
412
|
+
const error = new JQHTMLParseError(`Invalid content in <${tag.name}> tag`, tag.line, tag.column || 0, this.sourceContent, this.sourceFile);
|
|
413
|
+
error.suggestion =
|
|
414
|
+
`\n\nAll content within <textarea> and <pre> tags must be plain text or expressions.\n` +
|
|
415
|
+
`HTML tags and components are not allowed.\n\n` +
|
|
416
|
+
`Allowed:\n` +
|
|
417
|
+
` <textarea><%= this.data.value %></textarea> ← expressions OK\n` +
|
|
418
|
+
` <textarea>plain text</textarea> ← plain text OK\n\n` +
|
|
419
|
+
`Not allowed:\n` +
|
|
420
|
+
` <textarea><div>content</div></textarea> ← HTML tags not OK\n` +
|
|
421
|
+
` <textarea><MyComponent /></textarea> ← components not OK\n\n` +
|
|
422
|
+
`This ensures proper whitespace preservation.`;
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Generate rawtag instruction with raw content
|
|
427
|
+
const attrs_obj = this.generate_attributes_with_conditionals(tag.attributes, tag.conditionalAttributes);
|
|
428
|
+
// Collect raw content from children (all validated as TEXT)
|
|
429
|
+
let rawContent = '';
|
|
430
|
+
for (const child of tag.children) {
|
|
431
|
+
rawContent += child.content;
|
|
432
|
+
}
|
|
433
|
+
// Escape the raw content for JavaScript string
|
|
434
|
+
const escapedContent = this.escape_string(rawContent);
|
|
435
|
+
const rawtagInstruction = `_output.push({rawtag: ["${tag.name}", ${attrs_obj}, ${escapedContent}]});`;
|
|
436
|
+
lines[lineIndex] = (lines[lineIndex] || '') + rawtagInstruction;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// Normal HTML tag processing
|
|
440
|
+
// Opening tag goes on its line
|
|
441
|
+
const openTag = this.generate_tag_open(tag);
|
|
442
|
+
if (openTag) {
|
|
443
|
+
lines[lineIndex] = (lines[lineIndex] || '') + openTag;
|
|
444
|
+
}
|
|
445
|
+
// Process children
|
|
446
|
+
if (tag.children) {
|
|
447
|
+
tag.children.forEach(processNodeForLine);
|
|
448
|
+
}
|
|
449
|
+
// Closing tag might be on a different line
|
|
450
|
+
const closeTag = `_output.push("</${tag.name}>");`;
|
|
451
|
+
// For simplicity, put closing tag on the last child's line or same line
|
|
452
|
+
const closeLine = tag.children && tag.children.length > 0
|
|
453
|
+
? (tag.children[tag.children.length - 1].line || node.line)
|
|
454
|
+
: node.line;
|
|
455
|
+
const closeIndex = closeLine - 2;
|
|
456
|
+
if (closeIndex >= 0 && closeIndex < lines.length) {
|
|
457
|
+
lines[closeIndex] = (lines[closeIndex] || '') + ' ' + closeTag;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case NodeType.TEXT: {
|
|
463
|
+
const text = node;
|
|
464
|
+
// Apply padded trim to preserve intentional whitespace
|
|
465
|
+
const processed = this.padded_trim(text.content);
|
|
466
|
+
if (processed) {
|
|
467
|
+
const code = `_output.push(${this.escape_string(processed)});`;
|
|
468
|
+
// Optimization: skip consecutive identical space pushes
|
|
469
|
+
if (code === '_output.push(" ");' && this.lastOutput === '_output.push(" ");') {
|
|
470
|
+
// Skip duplicate - don't add to output
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
this.lastOutput = code; // Track for next comparison
|
|
474
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
// Empty text resets tracking so next space won't be skipped
|
|
478
|
+
this.lastOutput = '';
|
|
479
|
+
}
|
|
480
|
+
// Empty after processing: skip (no code generated)
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
case NodeType.CODE_BLOCK: {
|
|
484
|
+
this.lastOutput = ''; // Reset for non-text output
|
|
485
|
+
const codeBlock = node;
|
|
486
|
+
const code = this.generate_code_block(codeBlock);
|
|
487
|
+
if (code) {
|
|
488
|
+
// With preprocessing, empty lines are already marked with /* empty line */
|
|
489
|
+
// So we can just add the code as-is, line by line
|
|
490
|
+
const codeLines = code.split('\n');
|
|
491
|
+
for (let i = 0; i < codeLines.length; i++) {
|
|
492
|
+
const targetIndex = lineIndex + i;
|
|
493
|
+
if (targetIndex < lines.length) {
|
|
494
|
+
// Add all lines including comment placeholders
|
|
495
|
+
const codeLine = codeLines[i].trim();
|
|
496
|
+
if (codeLine) {
|
|
497
|
+
lines[targetIndex] = (lines[targetIndex] || '') + ' ' + codeLines[i];
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case NodeType.EXPRESSION: {
|
|
505
|
+
this.lastOutput = ''; // Reset for non-text output
|
|
506
|
+
const expr = node;
|
|
507
|
+
// Generate the expression wrapper on a single line
|
|
508
|
+
let code;
|
|
509
|
+
// Special handling for content() calls
|
|
510
|
+
// Strip trailing semicolon if present (optional in <%= %> blocks)
|
|
511
|
+
let trimmedCode = expr.code.trim();
|
|
512
|
+
if (trimmedCode.endsWith(';')) {
|
|
513
|
+
trimmedCode = trimmedCode.slice(0, -1).trim();
|
|
514
|
+
}
|
|
515
|
+
if (trimmedCode === 'content()') {
|
|
516
|
+
// Default slot/content - check _inner_html first
|
|
517
|
+
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]); } })();`;
|
|
518
|
+
}
|
|
519
|
+
else if (trimmedCode.startsWith('content.') && trimmedCode.endsWith('()')) {
|
|
520
|
+
// Named slot: content.header() (property access style)
|
|
521
|
+
const slotName = trimmedCode.slice(8, -2); // Extract "header" from "content.header()"
|
|
522
|
+
code = `(() => { if (typeof content === 'object' && typeof content.${slotName} === 'function') { const [contentInstructions] = content.${slotName}.call(this); _output.push(['_content', contentInstructions]); } })();`;
|
|
523
|
+
}
|
|
524
|
+
else if (trimmedCode.match(/^content\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(.+?))?\s*\)$/)) {
|
|
525
|
+
// Named slot: content('header') or content('header', data) (function call with string parameter and optional data)
|
|
526
|
+
// Use the standard result pattern for proper handling
|
|
527
|
+
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)); } })();`;
|
|
528
|
+
}
|
|
529
|
+
else if (expr.escaped) {
|
|
530
|
+
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)); } })();`;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
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(result); } })();`;
|
|
534
|
+
}
|
|
535
|
+
// Put the code on the starting line
|
|
536
|
+
if (code) {
|
|
537
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case NodeType.COMPONENT_INVOCATION: {
|
|
542
|
+
this.lastOutput = ''; // Reset for non-text output
|
|
543
|
+
const comp = node;
|
|
544
|
+
// For 1:1 mapping, generate compact component invocations
|
|
545
|
+
const attrs = this.generate_attributes_with_conditionals(comp.attributes, comp.conditionalAttributes);
|
|
546
|
+
if (comp.selfClosing || comp.children.length === 0) {
|
|
547
|
+
// Simple component without children
|
|
548
|
+
const code = `_output.push({comp: ["${comp.name}", ${attrs}]});`;
|
|
549
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
// Check if children contain slots
|
|
553
|
+
const slots = this.extract_slots_from_children(comp.children);
|
|
554
|
+
if (Object.keys(slots).length > 0) {
|
|
555
|
+
// Component with slots - use generate_component_invocation for proper slot handling
|
|
556
|
+
const code = this.generate_component_invocation(comp);
|
|
557
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Component with regular content (no slots)
|
|
561
|
+
// Generate inline content function
|
|
562
|
+
// Always include let _output = []; inside the function
|
|
563
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ` _output.push({comp: ["${comp.name}", ${attrs}, function(${comp.name}) { let _output = [];`;
|
|
564
|
+
// Process children
|
|
565
|
+
if (comp.children && comp.children.length > 0) {
|
|
566
|
+
// Check if all children are simple text/inline nodes that can go on one line
|
|
567
|
+
const allInline = comp.children.every(child => child.type === NodeType.TEXT ||
|
|
568
|
+
(child.type === NodeType.COMPONENT_INVOCATION && child.selfClosing));
|
|
569
|
+
if (allInline) {
|
|
570
|
+
// Put all inline children on the same line as the component
|
|
571
|
+
comp.children.forEach(child => {
|
|
572
|
+
const childCode = this.generate_node(child);
|
|
573
|
+
if (childCode) {
|
|
574
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + childCode;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
// Close on same line
|
|
578
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' return [_output, this]; }.bind(this)]});';
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
// Multi-line children - process on their respective lines
|
|
582
|
+
comp.children.forEach(child => {
|
|
583
|
+
const origLine = child.line;
|
|
584
|
+
if (origLine && origLine >= 2) {
|
|
585
|
+
const childIndex = origLine - 2;
|
|
586
|
+
if (childIndex >= 0 && childIndex < lines.length) {
|
|
587
|
+
const childCode = this.generate_node(child);
|
|
588
|
+
if (childCode) {
|
|
589
|
+
lines[childIndex] = (lines[childIndex] || '') + ' ' + childCode;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
// Return statement and closing on the last child's line
|
|
595
|
+
const returnLine = comp.children[comp.children.length - 1].line || node.line;
|
|
596
|
+
const returnIndex = returnLine - 2;
|
|
597
|
+
if (returnIndex >= 0 && returnIndex < lines.length) {
|
|
598
|
+
lines[returnIndex] = (lines[returnIndex] || '') + ' return [_output, this]; }.bind(this)]});';
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
// No children - close on same line
|
|
604
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' return [_output, this]; }.bind(this)]});';
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
case NodeType.SLOT: {
|
|
611
|
+
// Generate slot function definition with line mapping
|
|
612
|
+
const slot = node;
|
|
613
|
+
const slotName = slot.name;
|
|
614
|
+
// Start slot function on this line
|
|
615
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ` ${slotName}: function(${slotName}) { const _output = [];`;
|
|
616
|
+
// Process slot children on their respective lines
|
|
617
|
+
if (slot.children && slot.children.length > 0) {
|
|
618
|
+
slot.children.forEach(child => {
|
|
619
|
+
const childLine = child.line;
|
|
620
|
+
if (childLine && childLine >= 2) {
|
|
621
|
+
const childIndex = childLine - 2;
|
|
622
|
+
if (childIndex >= 0 && childIndex < lines.length) {
|
|
623
|
+
const childCode = this.generate_node(child);
|
|
624
|
+
if (childCode) {
|
|
625
|
+
lines[childIndex] = (lines[childIndex] || '') + ' ' + childCode;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
// Close slot function on the last child's line
|
|
631
|
+
const lastChild = slot.children[slot.children.length - 1];
|
|
632
|
+
const closeLine = lastChild.line || node.line;
|
|
633
|
+
const closeIndex = closeLine - 2;
|
|
634
|
+
if (closeIndex >= 0 && closeIndex < lines.length) {
|
|
635
|
+
lines[closeIndex] = (lines[closeIndex] || '') + ' return [_output, this]; }.bind(this),';
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
// Empty slot - close on same line
|
|
640
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' return [_output, this]; }.bind(this),';
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
default: {
|
|
645
|
+
// For other node types, use the standard generator
|
|
646
|
+
const code = this.generate_node(node);
|
|
647
|
+
if (code) {
|
|
648
|
+
lines[lineIndex] = (lines[lineIndex] || '') + ' ' + code;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
// Process all top-level nodes
|
|
654
|
+
for (const node of nodes) {
|
|
655
|
+
processNodeForLine(node);
|
|
656
|
+
}
|
|
657
|
+
// For true 1:1 mapping, we must preserve ALL lines, even empty ones
|
|
658
|
+
// Each source line from 2 to maxLine should produce exactly one output line
|
|
659
|
+
// Don't remove any empty lines - they're essential for maintaining line positions
|
|
660
|
+
return lines;
|
|
661
|
+
}
|
|
662
|
+
generate_node(node) {
|
|
663
|
+
switch (node.type) {
|
|
664
|
+
case NodeType.TEXT:
|
|
665
|
+
return this.generate_text(node);
|
|
666
|
+
case NodeType.EXPRESSION:
|
|
667
|
+
return this.generate_expression(node);
|
|
668
|
+
case NodeType.HTML_TAG:
|
|
669
|
+
return this.generate_html_tag(node);
|
|
670
|
+
case NodeType.COMPONENT_INVOCATION:
|
|
671
|
+
return this.generate_component_invocation(node);
|
|
672
|
+
case NodeType.IF_STATEMENT:
|
|
673
|
+
return this.generate_if(node);
|
|
674
|
+
case NodeType.FOR_STATEMENT:
|
|
675
|
+
return this.generate_for(node);
|
|
676
|
+
case NodeType.CODE_BLOCK:
|
|
677
|
+
return this.generate_code_block(node);
|
|
678
|
+
default:
|
|
679
|
+
console.warn(`Unknown node type: ${node.type}`);
|
|
680
|
+
return '';
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Padded trim: Collapse internal whitespace but preserve leading/trailing space
|
|
685
|
+
* Examples:
|
|
686
|
+
* " hello " → " hello "
|
|
687
|
+
* "hello" → "hello"
|
|
688
|
+
* " " → " "
|
|
689
|
+
* "\n\n \n" → " "
|
|
690
|
+
*/
|
|
691
|
+
padded_trim(text) {
|
|
692
|
+
const has_leading_space = /^\s/.test(text);
|
|
693
|
+
const has_trailing_space = /\s$/.test(text);
|
|
694
|
+
// Trim the text
|
|
695
|
+
let result = text.trim();
|
|
696
|
+
// Add back single space if original had leading/trailing whitespace
|
|
697
|
+
if (has_leading_space)
|
|
698
|
+
result = ' ' + result;
|
|
699
|
+
if (has_trailing_space)
|
|
700
|
+
result = result + ' ';
|
|
701
|
+
// Final pass: collapse all whitespace sequences to single space
|
|
702
|
+
return result.replace(/\s+/g, ' ');
|
|
703
|
+
}
|
|
704
|
+
generate_text(node) {
|
|
705
|
+
const content = node.content;
|
|
706
|
+
// Apply padded trim to preserve intentional whitespace
|
|
707
|
+
const processed = this.padded_trim(content);
|
|
708
|
+
// Skip if empty after processing
|
|
709
|
+
if (!processed) {
|
|
710
|
+
return '';
|
|
711
|
+
}
|
|
712
|
+
// Generate output code
|
|
713
|
+
const escaped = this.escape_string(processed);
|
|
714
|
+
const output = `_output.push(${escaped});`;
|
|
715
|
+
// Optimization: skip consecutive identical space pushes (but never skip newlines)
|
|
716
|
+
if (output === '_output.push(" ");' && this.lastOutput === '_output.push(" ");') {
|
|
717
|
+
return ''; // Skip duplicate space push
|
|
718
|
+
}
|
|
719
|
+
// Track this output for next comparison
|
|
720
|
+
this.lastOutput = output;
|
|
721
|
+
// Track the emitted position with source mapping
|
|
722
|
+
if (this.enablePositionTracking) {
|
|
723
|
+
this.emit(output, node);
|
|
724
|
+
}
|
|
725
|
+
return output;
|
|
726
|
+
}
|
|
727
|
+
generate_expression(node) {
|
|
728
|
+
this.lastOutput = ''; // Reset for non-text output
|
|
729
|
+
let output;
|
|
730
|
+
// Special handling for content() calls
|
|
731
|
+
// Strip trailing semicolon if present (optional in <%= %> blocks)
|
|
732
|
+
let trimmedCode = node.code.trim();
|
|
733
|
+
if (trimmedCode.endsWith(';')) {
|
|
734
|
+
trimmedCode = trimmedCode.slice(0, -1).trim();
|
|
735
|
+
}
|
|
736
|
+
if (trimmedCode === 'content()') {
|
|
737
|
+
// Default slot/content - check _inner_html first
|
|
738
|
+
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]); } })();`;
|
|
739
|
+
}
|
|
740
|
+
else if (trimmedCode.startsWith('content.') && trimmedCode.endsWith('()')) {
|
|
741
|
+
// Named slot: content.header() (property access style)
|
|
742
|
+
const slotName = trimmedCode.slice(8, -2); // Extract "header" from "content.header()"
|
|
743
|
+
output = `(() => { if (typeof content === 'object' && typeof content.${slotName} === 'function') { const [contentInstructions] = content.${slotName}.call(this); _output.push(['_content', contentInstructions]); } })();`;
|
|
744
|
+
}
|
|
745
|
+
else if (trimmedCode.match(/^content\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(.+?))?\s*\)$/)) {
|
|
746
|
+
// Named slot: content('header') or content('header', data) (function call with string parameter and optional data)
|
|
747
|
+
// Use the standard result pattern for proper handling
|
|
748
|
+
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)); } })();`;
|
|
749
|
+
}
|
|
750
|
+
else if (node.escaped) {
|
|
751
|
+
// Single-line expression handler for escaped output
|
|
752
|
+
output = `(() => { const result = ${node.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)); } })();`;
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
// Single-line expression handler for unescaped output
|
|
756
|
+
output = `(() => { const result = ${node.code}; if (Array.isArray(result)) { if (result.length === 2 && Array.isArray(result[0])) { _output.push(...result[0]); } else { _output.push(...result); } } else { _output.push(result); } })();`;
|
|
757
|
+
}
|
|
758
|
+
// Track the emitted position with source mapping
|
|
759
|
+
if (this.enablePositionTracking) {
|
|
760
|
+
this.emit(output, node);
|
|
761
|
+
}
|
|
762
|
+
return output;
|
|
763
|
+
}
|
|
764
|
+
generate_if(node) {
|
|
765
|
+
// Clean up condition - remove trailing opening brace
|
|
766
|
+
let condition = node.condition.trim();
|
|
767
|
+
condition = condition.replace(/\s*{\s*$/, ''); // Remove trailing brace
|
|
768
|
+
// Generate consequent body inline
|
|
769
|
+
const consequent_parts = [];
|
|
770
|
+
for (const child of node.consequent) {
|
|
771
|
+
const child_code = this.generate_node(child);
|
|
772
|
+
if (child_code) {
|
|
773
|
+
consequent_parts.push(child_code);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// Generate if statement - keep on single line for line preservation
|
|
777
|
+
let code = `if (${condition}) { ${consequent_parts.join(' ')} }`;
|
|
778
|
+
// Add else clause if present
|
|
779
|
+
if (node.alternate && node.alternate.length > 0) {
|
|
780
|
+
const alternate_parts = [];
|
|
781
|
+
for (const child of node.alternate) {
|
|
782
|
+
const child_code = this.generate_node(child);
|
|
783
|
+
if (child_code) {
|
|
784
|
+
alternate_parts.push(child_code);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
code += ` else { ${alternate_parts.join(' ')} }`;
|
|
788
|
+
}
|
|
789
|
+
// Track the emitted position with source mapping
|
|
790
|
+
if (this.enablePositionTracking) {
|
|
791
|
+
this.emit(code, node);
|
|
792
|
+
}
|
|
793
|
+
return code;
|
|
794
|
+
}
|
|
795
|
+
generate_for(node) {
|
|
796
|
+
const iterator = node.iterator.trim();
|
|
797
|
+
// Generate body inline
|
|
798
|
+
const body_parts = [];
|
|
799
|
+
for (const child of node.body) {
|
|
800
|
+
const child_code = this.generate_node(child);
|
|
801
|
+
if (child_code) {
|
|
802
|
+
body_parts.push(child_code);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// Generate for loop - keep on single line for line preservation
|
|
806
|
+
const code = `for ${iterator} { ${body_parts.join(' ')} }`;
|
|
807
|
+
// Track the emitted position with source mapping
|
|
808
|
+
if (this.enablePositionTracking) {
|
|
809
|
+
this.emit(code, node);
|
|
810
|
+
}
|
|
811
|
+
return code;
|
|
812
|
+
}
|
|
813
|
+
generate_code_block(node) {
|
|
814
|
+
// Handle legacy code property for backward compatibility
|
|
815
|
+
if (node.code !== undefined) {
|
|
816
|
+
return node.code;
|
|
817
|
+
}
|
|
818
|
+
// Process tokens to transform PHP-style syntax
|
|
819
|
+
if (!node.tokens || node.tokens.length === 0) {
|
|
820
|
+
return '';
|
|
821
|
+
}
|
|
822
|
+
const result = [];
|
|
823
|
+
for (let i = 0; i < node.tokens.length; i++) {
|
|
824
|
+
const token = node.tokens[i];
|
|
825
|
+
const nextToken = node.tokens[i + 1];
|
|
826
|
+
switch (token.type) {
|
|
827
|
+
case 'IF':
|
|
828
|
+
result.push('if');
|
|
829
|
+
break;
|
|
830
|
+
case 'ELSE':
|
|
831
|
+
// Check if next token is IF for "else if"
|
|
832
|
+
if (nextToken && nextToken.type === 'IF') {
|
|
833
|
+
result.push('} else');
|
|
834
|
+
// Don't skip the IF token, it will be processed next
|
|
835
|
+
}
|
|
836
|
+
else if (nextToken && nextToken.type === 'JAVASCRIPT') {
|
|
837
|
+
// Check if this is "else if" in the JavaScript token
|
|
838
|
+
if (nextToken.value.trim().startsWith('if ')) {
|
|
839
|
+
result.push('} else');
|
|
840
|
+
// The 'if' is part of the JAVASCRIPT token, will be handled there
|
|
841
|
+
}
|
|
842
|
+
else if (nextToken.value.trim() === ':') {
|
|
843
|
+
// Regular else with colon in next token
|
|
844
|
+
result.push('} else {');
|
|
845
|
+
i++; // Skip the colon token
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
// else with other JavaScript code
|
|
849
|
+
result.push('} else {');
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
// else without anything after (shouldn't happen in valid JQHTML)
|
|
854
|
+
result.push('} else {');
|
|
855
|
+
}
|
|
856
|
+
break;
|
|
857
|
+
case 'ELSEIF':
|
|
858
|
+
result.push('} else if');
|
|
859
|
+
break;
|
|
860
|
+
case 'ENDIF':
|
|
861
|
+
result.push('}');
|
|
862
|
+
// Skip optional semicolon in next JAVASCRIPT token
|
|
863
|
+
if (nextToken && nextToken.type === 'JAVASCRIPT' && nextToken.value.trim() === ';') {
|
|
864
|
+
i++; // Skip the semicolon token
|
|
865
|
+
}
|
|
866
|
+
break;
|
|
867
|
+
case 'FOR':
|
|
868
|
+
result.push('for');
|
|
869
|
+
break;
|
|
870
|
+
case 'ENDFOR':
|
|
871
|
+
result.push('}');
|
|
872
|
+
// Skip optional semicolon in next JAVASCRIPT token
|
|
873
|
+
if (nextToken && nextToken.type === 'JAVASCRIPT' && nextToken.value.trim() === ';') {
|
|
874
|
+
i++; // Skip the semicolon token
|
|
875
|
+
}
|
|
876
|
+
break;
|
|
877
|
+
case 'JAVASCRIPT':
|
|
878
|
+
// Transform colons to opening braces after conditions
|
|
879
|
+
let jsCode = token.value;
|
|
880
|
+
// Check if this is a condition ending with colon
|
|
881
|
+
const prevToken = i > 0 ? node.tokens[i - 1] : null;
|
|
882
|
+
if (jsCode.endsWith(':') &&
|
|
883
|
+
prevToken &&
|
|
884
|
+
(prevToken.type === 'IF' || prevToken.type === 'FOR' || prevToken.type === 'ELSEIF' ||
|
|
885
|
+
(prevToken.type === 'ELSE' && jsCode.startsWith('if ')))) {
|
|
886
|
+
jsCode = jsCode.slice(0, -1) + ' {';
|
|
887
|
+
}
|
|
888
|
+
result.push(jsCode);
|
|
889
|
+
break;
|
|
890
|
+
default:
|
|
891
|
+
// Pass through any other token types
|
|
892
|
+
result.push(token.value);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const code = result.join(' ');
|
|
896
|
+
// Track the emitted position with source mapping
|
|
897
|
+
if (this.enablePositionTracking) {
|
|
898
|
+
this.emit(code, node);
|
|
899
|
+
}
|
|
900
|
+
return code;
|
|
901
|
+
}
|
|
902
|
+
generate_tag_open(node) {
|
|
903
|
+
// Generate just the opening tag
|
|
904
|
+
const attrs = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
|
|
905
|
+
return `_output.push({tag: ["${node.name}", ${attrs}, ${node.selfClosing || false}]});`;
|
|
906
|
+
}
|
|
907
|
+
generate_html_tag(node) {
|
|
908
|
+
this.lastOutput = ''; // Reset for non-text output
|
|
909
|
+
// Check if this tag needs raw content preservation
|
|
910
|
+
if (node.preserveWhitespace && !node.selfClosing && node.children.length > 0) {
|
|
911
|
+
// Validate: only TEXT, EXPRESSION, and CODE_BLOCK children allowed in raw content tags
|
|
912
|
+
// HTML tags and components are not allowed (they would break whitespace preservation)
|
|
913
|
+
for (const child of node.children) {
|
|
914
|
+
if (child.type !== NodeType.TEXT &&
|
|
915
|
+
child.type !== NodeType.EXPRESSION &&
|
|
916
|
+
child.type !== NodeType.CODE_BLOCK) {
|
|
917
|
+
const error = new JQHTMLParseError(`Invalid content in <${node.name}> tag`, node.line, node.column || 0, this.sourceContent, this.sourceFile);
|
|
918
|
+
error.suggestion =
|
|
919
|
+
`\n\nAll content within <textarea> and <pre> tags must be plain text or expressions.\n` +
|
|
920
|
+
`HTML tags and components are not allowed.\n\n` +
|
|
921
|
+
`Allowed:\n` +
|
|
922
|
+
` <textarea><%= this.data.value %></textarea> ← expressions OK\n` +
|
|
923
|
+
` <textarea>plain text</textarea> ← plain text OK\n\n` +
|
|
924
|
+
`Not allowed:\n` +
|
|
925
|
+
` <textarea><div>content</div></textarea> ← HTML tags not OK\n` +
|
|
926
|
+
` <textarea><MyComponent /></textarea> ← components not OK\n\n` +
|
|
927
|
+
`This ensures proper whitespace preservation.`;
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
// Generate rawtag instruction with raw content
|
|
932
|
+
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
|
|
933
|
+
// Collect raw content from children (all validated as TEXT)
|
|
934
|
+
let rawContent = '';
|
|
935
|
+
for (const child of node.children) {
|
|
936
|
+
rawContent += child.content;
|
|
937
|
+
}
|
|
938
|
+
// Escape the raw content for JavaScript string
|
|
939
|
+
const escapedContent = this.escape_string(rawContent);
|
|
940
|
+
return `_output.push({rawtag: ["${node.name}", ${attrs_obj}, ${escapedContent}]});`;
|
|
941
|
+
}
|
|
942
|
+
// Normal tag generation
|
|
943
|
+
const parts = [];
|
|
944
|
+
// Generate opening tag instruction
|
|
945
|
+
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
|
|
946
|
+
parts.push(`_output.push({tag: ["${node.name}", ${attrs_obj}, ${node.selfClosing}]});`);
|
|
947
|
+
if (!node.selfClosing) {
|
|
948
|
+
// Generate children inline
|
|
949
|
+
if (node.children.length > 0) {
|
|
950
|
+
for (const child of node.children) {
|
|
951
|
+
const child_code = this.generate_node(child);
|
|
952
|
+
if (child_code) {
|
|
953
|
+
// Skip empty text nodes
|
|
954
|
+
if (!child_code.match(/^_output\.push\(""\);?$/)) {
|
|
955
|
+
parts.push(child_code);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// Generate closing tag
|
|
961
|
+
parts.push(`_output.push("</${node.name}>");`);
|
|
962
|
+
}
|
|
963
|
+
// Join parts with space for single-line tags, but this will be split later
|
|
964
|
+
return parts.join(' ');
|
|
965
|
+
}
|
|
966
|
+
generate_component_invocation(node) {
|
|
967
|
+
this.lastOutput = ''; // Reset for non-text output
|
|
968
|
+
const instructions = [];
|
|
969
|
+
const attrs_obj = this.generate_attributes_with_conditionals(node.attributes, node.conditionalAttributes);
|
|
970
|
+
if (node.selfClosing || node.children.length === 0) {
|
|
971
|
+
// Simple component without children
|
|
972
|
+
const componentCall = `_output.push({comp: ["${node.name}", ${attrs_obj}]});`;
|
|
973
|
+
instructions.push(componentCall);
|
|
974
|
+
// Track component invocation position
|
|
975
|
+
if (this.enablePositionTracking) {
|
|
976
|
+
this.emit(componentCall, node);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
// Check if children contain slots
|
|
981
|
+
const slots = this.extract_slots_from_children(node.children);
|
|
982
|
+
if (Object.keys(slots).length > 0) {
|
|
983
|
+
// Component with slots - generate content as object with slot functions ON A SINGLE LINE
|
|
984
|
+
const slotEntries = [];
|
|
985
|
+
for (const [slotName, slotNode] of Object.entries(slots)) {
|
|
986
|
+
const slot_code = slotNode.children.length > 0
|
|
987
|
+
? this.generate_function_body(slotNode.children)
|
|
988
|
+
: '';
|
|
989
|
+
// Add slot name as parameter: function(slotName) { ... }
|
|
990
|
+
slotEntries.push(`${slotName}: function(${slotName}) { const _output = []; ${slot_code} return [_output, this]; }.bind(this)`);
|
|
991
|
+
}
|
|
992
|
+
// Everything on one line
|
|
993
|
+
instructions.push(`_output.push({comp: ["${node.name}", ${attrs_obj}, {${slotEntries.join(', ')}}]});`);
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
// Component with regular content (no slots)
|
|
997
|
+
instructions.push(`_output.push({comp: ["${node.name}", ${attrs_obj}, function(${node.name}) {`);
|
|
998
|
+
instructions.push(` const _output = [];`);
|
|
999
|
+
const children_code = this.generate_function_body(node.children);
|
|
1000
|
+
if (children_code) {
|
|
1001
|
+
instructions.push(this.indent(children_code, 1));
|
|
1002
|
+
}
|
|
1003
|
+
instructions.push(` return [_output, this];`);
|
|
1004
|
+
instructions.push(`}.bind(this)]});`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return instructions.join('\n');
|
|
1008
|
+
}
|
|
1009
|
+
parse_attributes(attrs_string) {
|
|
1010
|
+
const attrs = {};
|
|
1011
|
+
// Simple attribute parser - handles key="value" and key=value
|
|
1012
|
+
const attr_regex = /([a-zA-Z$][a-zA-Z0-9-]*)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/g;
|
|
1013
|
+
let match;
|
|
1014
|
+
while ((match = attr_regex.exec(attrs_string)) !== null) {
|
|
1015
|
+
const [, name, value] = match;
|
|
1016
|
+
// Remove quotes if present
|
|
1017
|
+
attrs[name] = value.replace(/^["']|["']$/g, '');
|
|
1018
|
+
}
|
|
1019
|
+
return attrs;
|
|
1020
|
+
}
|
|
1021
|
+
// Generate attribute object including conditional attributes
|
|
1022
|
+
generate_attributes_with_conditionals(attrs, conditionalAttrs) {
|
|
1023
|
+
// If no conditional attributes, use simple object
|
|
1024
|
+
if (!conditionalAttrs || conditionalAttrs.length === 0) {
|
|
1025
|
+
return this.generate_attributes_object(attrs);
|
|
1026
|
+
}
|
|
1027
|
+
// We have conditional attributes - need to merge them at runtime
|
|
1028
|
+
const baseAttrs = this.generate_attributes_object(attrs);
|
|
1029
|
+
// Generate code that conditionally adds attributes
|
|
1030
|
+
let result = baseAttrs;
|
|
1031
|
+
for (const condAttr of conditionalAttrs) {
|
|
1032
|
+
const condAttrsObj = this.generate_attributes_object(condAttr.attributes);
|
|
1033
|
+
// Use Object.assign to merge conditional attributes
|
|
1034
|
+
result = `Object.assign({}, ${result}, (${condAttr.condition}) ? ${condAttrsObj} : {})`;
|
|
1035
|
+
}
|
|
1036
|
+
return result;
|
|
1037
|
+
}
|
|
1038
|
+
generate_attributes_object(attrs) {
|
|
1039
|
+
if (Object.keys(attrs).length === 0) {
|
|
1040
|
+
return '{}';
|
|
1041
|
+
}
|
|
1042
|
+
const entries = Object.entries(attrs).flatMap(([key, value]) => {
|
|
1043
|
+
// Convert 'tag' to '_tag' for component invocations
|
|
1044
|
+
const attrKey = key === 'tag' ? '_tag' : key;
|
|
1045
|
+
// Special handling for data-sid attribute (from $sid) - create scoped id
|
|
1046
|
+
// NOTE: Parser converts $sid="foo" → data-sid="foo" so we can distinguish from regular id
|
|
1047
|
+
// This generates: id="foo:PARENT_CID" data-sid="foo"
|
|
1048
|
+
// The :PARENT_CID scoping happens at runtime in instruction-processor.ts
|
|
1049
|
+
if (key === 'data-sid') {
|
|
1050
|
+
const id_entries = [];
|
|
1051
|
+
if (value && typeof value === 'object' && value.interpolated) {
|
|
1052
|
+
// Interpolated $sid like $sid="user<%= index %>"
|
|
1053
|
+
const parts = value.parts.map((part) => {
|
|
1054
|
+
if (part.type === 'text') {
|
|
1055
|
+
return this.escape_string(part.value);
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
return part.value;
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
const base_id = parts.join(' + ');
|
|
1062
|
+
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
|
|
1063
|
+
id_entries.push(`"data-sid": ${base_id}`);
|
|
1064
|
+
}
|
|
1065
|
+
else if (value && typeof value === 'object' && value.quoted) {
|
|
1066
|
+
// Quoted $sid like $sid="static"
|
|
1067
|
+
const base_id = this.escape_string(value.value);
|
|
1068
|
+
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
|
|
1069
|
+
id_entries.push(`"data-sid": ${base_id}`);
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
// Simple $sid like $sid="username" or expression like $sid=someVar
|
|
1073
|
+
const base_id = this.escape_string(String(value));
|
|
1074
|
+
id_entries.push(`"id": ${base_id} + ":" + this._cid`);
|
|
1075
|
+
id_entries.push(`"data-sid": ${base_id}`);
|
|
1076
|
+
}
|
|
1077
|
+
return id_entries;
|
|
1078
|
+
}
|
|
1079
|
+
// Regular id attribute - pass through unchanged
|
|
1080
|
+
// id="foo" remains id="foo" (no scoping)
|
|
1081
|
+
if (key === 'id') {
|
|
1082
|
+
if (value && typeof value === 'object' && value.quoted) {
|
|
1083
|
+
return `"id": ${this.escape_string(value.value)}`;
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
return `"id": ${this.escape_string(String(value))}`;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
// Check if this is an interpolated attribute value
|
|
1090
|
+
if (value && typeof value === 'object' && value.interpolated) {
|
|
1091
|
+
// Build concatenation expression
|
|
1092
|
+
const parts = value.parts.map((part) => {
|
|
1093
|
+
if (part.type === 'text') {
|
|
1094
|
+
return this.escape_string(part.value);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
// Expression - wrap in parentheses to preserve operator precedence
|
|
1098
|
+
// This ensures "a" + (x ? 'b' : 'c') instead of "a" + x ? 'b' : 'c'
|
|
1099
|
+
return `(${part.value})`;
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
return `"${attrKey}": ${parts.join(' + ')}`;
|
|
1103
|
+
}
|
|
1104
|
+
// Check if value is marked as quoted
|
|
1105
|
+
if (value && typeof value === 'object' && value.quoted) {
|
|
1106
|
+
// This was a quoted string in the source - always treat as string
|
|
1107
|
+
return `"${attrKey}": ${this.escape_string(value.value)}`;
|
|
1108
|
+
}
|
|
1109
|
+
// Check if it's a parenthesized expression: $attr=(expr)
|
|
1110
|
+
if (value && typeof value === 'object' && value.expression) {
|
|
1111
|
+
// It's an expression - output without quotes
|
|
1112
|
+
return `"${attrKey}": ${value.value}`;
|
|
1113
|
+
}
|
|
1114
|
+
// Check if it's a bare identifier: $attr=identifier
|
|
1115
|
+
if (value && typeof value === 'object' && value.identifier) {
|
|
1116
|
+
// It's an identifier - output as JavaScript expression
|
|
1117
|
+
return `"${attrKey}": ${value.value}`;
|
|
1118
|
+
}
|
|
1119
|
+
// Check if it's an event handler binding (data-__-on-*)
|
|
1120
|
+
if (key.startsWith('data-__-on-')) {
|
|
1121
|
+
// Handle based on whether value was quoted or not
|
|
1122
|
+
if (typeof value === 'object' && value !== null) {
|
|
1123
|
+
if (value.quoted) {
|
|
1124
|
+
// Was quoted: @click="handler" -> string literal
|
|
1125
|
+
return `"${key}": ${this.escape_string(value.value)}`;
|
|
1126
|
+
}
|
|
1127
|
+
else if (value.identifier || value.expression) {
|
|
1128
|
+
// Was unquoted: @click=this.handler -> JavaScript expression
|
|
1129
|
+
return `"${key}": ${value.value}`;
|
|
1130
|
+
}
|
|
1131
|
+
else if (value.interpolated) {
|
|
1132
|
+
// Has interpolation - needs special handling
|
|
1133
|
+
return `"${key}": ${this.compile_interpolated_value(value)}`;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else if (typeof value === 'string') {
|
|
1137
|
+
// Fallback for simple strings (backwards compatibility)
|
|
1138
|
+
return `"${key}": ${this.escape_string(value)}`;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
// Check if it's a data attribute ($foo or :binding)
|
|
1142
|
+
if (key.startsWith('data-')) {
|
|
1143
|
+
// Handle based on whether value was quoted or not
|
|
1144
|
+
if (typeof value === 'object' && value !== null) {
|
|
1145
|
+
if (value.quoted) {
|
|
1146
|
+
// Was quoted: $foo="bar" -> string literal
|
|
1147
|
+
return `"${key}": ${this.escape_string(value.value)}`;
|
|
1148
|
+
}
|
|
1149
|
+
else if (value.identifier || value.expression) {
|
|
1150
|
+
// Was unquoted: $foo=this.data.bar -> JavaScript expression
|
|
1151
|
+
return `"${key}": ${value.value}`;
|
|
1152
|
+
}
|
|
1153
|
+
else if (value.interpolated) {
|
|
1154
|
+
// Has interpolation - needs special handling
|
|
1155
|
+
return `"${key}": ${this.compile_interpolated_value(value)}`;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
else if (typeof value === 'string') {
|
|
1159
|
+
// Fallback for simple strings
|
|
1160
|
+
return `"${key}": ${this.escape_string(value)}`;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
// Regular attributes
|
|
1164
|
+
if (typeof value === 'object' && value !== null) {
|
|
1165
|
+
if (value.quoted) {
|
|
1166
|
+
// Explicitly quoted value
|
|
1167
|
+
return `"${attrKey}": ${this.escape_string(value.value)}`;
|
|
1168
|
+
}
|
|
1169
|
+
else if (value.identifier || value.expression) {
|
|
1170
|
+
// Unquoted JavaScript expression
|
|
1171
|
+
return `"${attrKey}": ${value.value}`;
|
|
1172
|
+
}
|
|
1173
|
+
else if (value.interpolated) {
|
|
1174
|
+
// Has interpolation
|
|
1175
|
+
return `"${attrKey}": ${this.compile_interpolated_value(value)}`;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// Default: treat as string
|
|
1179
|
+
return `"${attrKey}": ${this.escape_string(String(value))}`;
|
|
1180
|
+
});
|
|
1181
|
+
return `{${entries.join(', ')}}`;
|
|
1182
|
+
}
|
|
1183
|
+
is_self_closing_tag(tag_name) {
|
|
1184
|
+
const self_closing = [
|
|
1185
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
1186
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
1187
|
+
];
|
|
1188
|
+
return self_closing.includes(tag_name.toLowerCase());
|
|
1189
|
+
}
|
|
1190
|
+
compile_interpolated_value(value) {
|
|
1191
|
+
// Handle interpolated values with embedded expressions
|
|
1192
|
+
if (!value.parts || !Array.isArray(value.parts)) {
|
|
1193
|
+
return this.escape_string(String(value));
|
|
1194
|
+
}
|
|
1195
|
+
// Build template literal from parts
|
|
1196
|
+
const parts = value.parts.map((part) => {
|
|
1197
|
+
if (part.type === 'text') {
|
|
1198
|
+
// Escape text parts for template literal
|
|
1199
|
+
const escaped = part.value
|
|
1200
|
+
.replace(/\\/g, '\\\\')
|
|
1201
|
+
.replace(/`/g, '\\`')
|
|
1202
|
+
.replace(/\${/g, '\\${');
|
|
1203
|
+
return escaped;
|
|
1204
|
+
}
|
|
1205
|
+
else if (part.type === 'expression') {
|
|
1206
|
+
// Embed expression
|
|
1207
|
+
return `\${${part.value}}`;
|
|
1208
|
+
}
|
|
1209
|
+
return part.value;
|
|
1210
|
+
});
|
|
1211
|
+
// Return as template literal
|
|
1212
|
+
return '`' + parts.join('') + '`';
|
|
1213
|
+
}
|
|
1214
|
+
escape_string(str) {
|
|
1215
|
+
// Escape for JavaScript string literal
|
|
1216
|
+
const escaped = str
|
|
1217
|
+
.replace(/\\/g, '\\\\')
|
|
1218
|
+
.replace(/"/g, '\\"')
|
|
1219
|
+
.replace(/\n/g, '\\n')
|
|
1220
|
+
.replace(/\r/g, '\\r')
|
|
1221
|
+
.replace(/\t/g, '\\t');
|
|
1222
|
+
return `"${escaped}"`;
|
|
1223
|
+
}
|
|
1224
|
+
indent(code, level) {
|
|
1225
|
+
const indent = ' '.repeat(level);
|
|
1226
|
+
return code.split('\n').map(line => line ? indent + line : line).join('\n');
|
|
1227
|
+
}
|
|
1228
|
+
extract_slots_from_children(children) {
|
|
1229
|
+
const slots = {};
|
|
1230
|
+
for (const child of children) {
|
|
1231
|
+
if (child.type === NodeType.SLOT) {
|
|
1232
|
+
const slotNode = child;
|
|
1233
|
+
slots[slotNode.name] = slotNode;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return slots;
|
|
1237
|
+
}
|
|
1238
|
+
// -------------------------------------------------------------------------
|
|
1239
|
+
// Position Tracking Methods
|
|
1240
|
+
// -------------------------------------------------------------------------
|
|
1241
|
+
/**
|
|
1242
|
+
* Emit text and track position for source maps
|
|
1243
|
+
*/
|
|
1244
|
+
emit(text, node) {
|
|
1245
|
+
// Store the starting position before emitting
|
|
1246
|
+
const startLine = this.outputLine;
|
|
1247
|
+
const startColumn = this.outputColumn;
|
|
1248
|
+
// Update position tracking
|
|
1249
|
+
for (let i = 0; i < text.length; i++) {
|
|
1250
|
+
if (text[i] === '\n') {
|
|
1251
|
+
this.outputLine++;
|
|
1252
|
+
this.outputColumn = 0;
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
this.outputColumn++;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
// Log position if tracking enabled
|
|
1259
|
+
if (this.enablePositionTracking) {
|
|
1260
|
+
this.positionLog.push({
|
|
1261
|
+
line: this.outputLine,
|
|
1262
|
+
column: this.outputColumn,
|
|
1263
|
+
text: text.substring(0, 20), // First 20 chars for debugging
|
|
1264
|
+
node: node ? `${node.type}:L${node.line}:C${node.column}` : undefined
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
// ARCHIVED: Token-by-token mapping approach - not currently used
|
|
1268
|
+
// We're using 1:1 line mapping instead, but keeping this code for potential future use
|
|
1269
|
+
/* istanbul ignore next */
|
|
1270
|
+
if (false) {
|
|
1271
|
+
// TypeScript can't analyze dead code properly, so we cast to any
|
|
1272
|
+
const deadCode = () => {
|
|
1273
|
+
if (this.sourceMapGenerator && node?.loc && this.sourceFile) {
|
|
1274
|
+
this.sourceMapGenerator.addMapping({
|
|
1275
|
+
generated: { line: startLine, column: startColumn },
|
|
1276
|
+
source: this.sourceFile,
|
|
1277
|
+
original: { line: node.loc.start.line, column: node.loc.start.column - 1 },
|
|
1278
|
+
name: undefined
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
this.outputBuffer.push(text);
|
|
1284
|
+
return text;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Emit a line of text (adds newline)
|
|
1288
|
+
*/
|
|
1289
|
+
emitLine(text, node) {
|
|
1290
|
+
return this.emit(text + '\n', node);
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Reset position tracking
|
|
1294
|
+
*/
|
|
1295
|
+
resetPositionTracking() {
|
|
1296
|
+
this.outputLine = 1;
|
|
1297
|
+
this.outputColumn = 0;
|
|
1298
|
+
this.outputBuffer = [];
|
|
1299
|
+
this.positionLog = [];
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Get position tracking log for debugging
|
|
1303
|
+
*/
|
|
1304
|
+
getPositionLog() {
|
|
1305
|
+
return this.positionLog;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Enable/disable position tracking
|
|
1309
|
+
*/
|
|
1310
|
+
setPositionTracking(enabled) {
|
|
1311
|
+
this.enablePositionTracking = enabled;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Serialize attribute object with proper handling of identifiers and expressions
|
|
1315
|
+
* Quoted values become strings, identifiers/expressions become raw JavaScript
|
|
1316
|
+
*/
|
|
1317
|
+
serializeAttributeObject(obj) {
|
|
1318
|
+
if (!obj || Object.keys(obj).length === 0) {
|
|
1319
|
+
return '{}';
|
|
1320
|
+
}
|
|
1321
|
+
const entries = [];
|
|
1322
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1323
|
+
// Check if value is a parsed attribute object with type info
|
|
1324
|
+
if (value && typeof value === 'object' && (value.identifier || value.expression)) {
|
|
1325
|
+
// Identifier or expression - output as raw JavaScript (no quotes)
|
|
1326
|
+
entries.push(`"${key}": ${value.value}`);
|
|
1327
|
+
}
|
|
1328
|
+
else if (value && typeof value === 'object' && value.quoted) {
|
|
1329
|
+
// Quoted string - output as string literal
|
|
1330
|
+
entries.push(`"${key}": ${JSON.stringify(value.value)}`);
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
// Simple value - output as-is via JSON.stringify
|
|
1334
|
+
entries.push(`"${key}": ${JSON.stringify(value)}`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return `{${entries.join(', ')}}`;
|
|
1338
|
+
}
|
|
1339
|
+
build_module_code() {
|
|
1340
|
+
let code = '// Generated by JQHTML v2 Code Generator\n';
|
|
1341
|
+
code += '// Parser version: 2.2.56 (with 1:1 sourcemap fixes)\n';
|
|
1342
|
+
code += '// Produces v1-compatible instruction arrays\n\n';
|
|
1343
|
+
// Add html escaping function (assumed to be available in runtime)
|
|
1344
|
+
code += '// Runtime should provide: function html(str) { return escapeHtml(str); }\n\n';
|
|
1345
|
+
// Add component registration
|
|
1346
|
+
code += 'const jqhtml_components = new Map();\n\n';
|
|
1347
|
+
// Add each component
|
|
1348
|
+
for (const [name, component] of this.components) {
|
|
1349
|
+
code += `// Component: ${name}\n`;
|
|
1350
|
+
code += `jqhtml_components.set('${name}', {\n`;
|
|
1351
|
+
code += ` _jqhtml_version: '2.2.222',\n`; // Version will be replaced during build
|
|
1352
|
+
code += ` name: '${name}',\n`;
|
|
1353
|
+
code += ` tag: '${component.tagName}',\n`;
|
|
1354
|
+
code += ` defaultAttributes: ${this.serializeAttributeObject(component.defaultAttributes)},\n`;
|
|
1355
|
+
// Add defineArgs if present ($ attributes on Define tag)
|
|
1356
|
+
if (component.defineArgs) {
|
|
1357
|
+
code += ` defineArgs: ${this.serializeAttributeObject(component.defineArgs)},\n`;
|
|
1358
|
+
}
|
|
1359
|
+
// Add extends if present (template inheritance)
|
|
1360
|
+
if (component.extends) {
|
|
1361
|
+
code += ` extends: '${component.extends}',\n`;
|
|
1362
|
+
}
|
|
1363
|
+
code += ` render: ${this.indent(component.render_function, 1).trim()},\n`;
|
|
1364
|
+
code += ` dependencies: [${component.dependencies.map(d => `'${d}'`).join(', ')}]\n`;
|
|
1365
|
+
code += '});\n\n';
|
|
1366
|
+
}
|
|
1367
|
+
// Add export
|
|
1368
|
+
code += 'export { jqhtml_components };\n';
|
|
1369
|
+
return code;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
// Helper function for standalone usage
|
|
1373
|
+
export function generate(ast, sourceFile, sourceContent) {
|
|
1374
|
+
const generator = new CodeGenerator();
|
|
1375
|
+
return generator.generate(ast, sourceFile, sourceContent);
|
|
1376
|
+
}
|
|
1377
|
+
//# sourceMappingURL=codegen.js.map
|