@thyn/core 0.0.351 → 0.0.353
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/dist/plugin/utils.js +205 -91
- package/docs/package-lock.json +4 -4
- package/docs/package.json +1 -1
- package/package.json +1 -1
- package/src/plugin/utils.ts +211 -88
- package/tests/ImportInString.test.ts +15 -0
- package/tests/ImportInString.thyn +15 -0
package/dist/plugin/utils.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export function extractParts(code) {
|
|
2
2
|
// Helper to check if a position is inside a string literal or comment
|
|
3
|
-
|
|
3
|
+
// within a specific range (used for checking script content only)
|
|
4
|
+
function isInsideStringOrComment(code, startPos, endPos) {
|
|
4
5
|
let inString = false;
|
|
5
6
|
let stringChar = '';
|
|
6
7
|
let escaped = false;
|
|
7
8
|
let inLineComment = false;
|
|
8
9
|
let inBlockComment = false;
|
|
9
|
-
for (let i =
|
|
10
|
+
for (let i = startPos; i < endPos; i++) {
|
|
10
11
|
const char = code[i];
|
|
11
12
|
const nextChar = code[i + 1];
|
|
12
13
|
if (inLineComment) {
|
|
@@ -52,45 +53,114 @@ export function extractParts(code) {
|
|
|
52
53
|
}
|
|
53
54
|
return inString || inLineComment || inBlockComment;
|
|
54
55
|
}
|
|
55
|
-
// Find
|
|
56
|
-
function
|
|
56
|
+
// Find all script tag positions with their boundaries
|
|
57
|
+
function findScriptBoundaries(code) {
|
|
58
|
+
const boundaries = [];
|
|
57
59
|
const openRegex = /<script([^>]*)>/gi;
|
|
60
|
+
const closeRegex = /<\/script>/gi;
|
|
58
61
|
let openMatch;
|
|
59
62
|
while ((openMatch = openRegex.exec(code)) !== null) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
const openIndex = openMatch.index;
|
|
64
|
+
const openLength = openMatch[0].length;
|
|
65
|
+
const attrs = openMatch[1] || '';
|
|
66
|
+
// Find matching close tag
|
|
67
|
+
closeRegex.lastIndex = openIndex + openLength;
|
|
68
|
+
let closeMatch;
|
|
69
|
+
while ((closeMatch = closeRegex.exec(code)) !== null) {
|
|
70
|
+
// Check if this close tag is inside a JS string within this script
|
|
71
|
+
const contentStart = openIndex + openLength;
|
|
72
|
+
if (!isInsideStringOrComment(code, contentStart, closeMatch.index)) {
|
|
73
|
+
boundaries.push({
|
|
74
|
+
start: openIndex,
|
|
75
|
+
end: closeMatch.index + closeMatch[0].length,
|
|
76
|
+
attrs
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return boundaries;
|
|
83
|
+
}
|
|
84
|
+
// Find the real script section (not inside a string of another script)
|
|
85
|
+
function findScriptSection(code) {
|
|
86
|
+
const allBoundaries = findScriptBoundaries(code);
|
|
87
|
+
if (allBoundaries.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
// The first script tag is the real one if it's not inside any other script's string
|
|
90
|
+
// Check each boundary to see if it's inside another script's content
|
|
91
|
+
for (const boundary of allBoundaries) {
|
|
92
|
+
let isInsideAnotherScript = false;
|
|
93
|
+
for (const other of allBoundaries) {
|
|
94
|
+
if (other === boundary)
|
|
95
|
+
continue;
|
|
96
|
+
// Check if this boundary is inside another script's content
|
|
97
|
+
const otherContentStart = other.start + code.slice(other.start, other.end).indexOf('>') + 1;
|
|
98
|
+
const otherContentEnd = other.end - code.slice(other.end - 10, other.end).indexOf('<') - 10 + code.slice(other.end - 10, other.end).indexOf('<');
|
|
99
|
+
if (boundary.start > otherContentStart && boundary.start < other.end) {
|
|
100
|
+
// This boundary is inside another script's content area
|
|
101
|
+
// Check if it's inside a JS string
|
|
102
|
+
if (isInsideStringOrComment(code, otherContentStart, boundary.start)) {
|
|
103
|
+
isInsideAnotherScript = true;
|
|
104
|
+
break;
|
|
74
105
|
}
|
|
75
106
|
}
|
|
76
107
|
}
|
|
108
|
+
if (!isInsideAnotherScript) {
|
|
109
|
+
// This is the real script section
|
|
110
|
+
const openTagEnd = code.indexOf('>', boundary.start) + 1;
|
|
111
|
+
const closeTagStart = code.lastIndexOf('<', boundary.end - 1);
|
|
112
|
+
return {
|
|
113
|
+
start: boundary.start,
|
|
114
|
+
contentStart: openTagEnd,
|
|
115
|
+
contentEnd: closeTagStart,
|
|
116
|
+
end: boundary.end,
|
|
117
|
+
attrs: boundary.attrs
|
|
118
|
+
};
|
|
119
|
+
}
|
|
77
120
|
}
|
|
78
121
|
return null;
|
|
79
122
|
}
|
|
80
|
-
// Find
|
|
123
|
+
// Find the real style section (outside of script sections)
|
|
81
124
|
function findStyleSection(code) {
|
|
125
|
+
const scriptBoundaries = findScriptBoundaries(code);
|
|
82
126
|
const openRegex = /<style[^>]*>/gi;
|
|
127
|
+
const closeRegex = /<\/style>/gi;
|
|
83
128
|
let openMatch;
|
|
84
129
|
while ((openMatch = openRegex.exec(code)) !== null) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
130
|
+
const openIndex = openMatch.index;
|
|
131
|
+
// Check if this style tag is inside any script section
|
|
132
|
+
let isInsideScript = false;
|
|
133
|
+
for (const script of scriptBoundaries) {
|
|
134
|
+
const contentStart = script.start + code.slice(script.start, script.end).indexOf('>') + 1;
|
|
135
|
+
const contentEnd = script.end - code.slice(script.end - 10, script.end).indexOf('<') - 10 + code.slice(script.end - 10, script.end).indexOf('<');
|
|
136
|
+
if (openIndex >= contentStart && openIndex < script.end) {
|
|
137
|
+
// It's in the script section, check if inside a JS string
|
|
138
|
+
if (isInsideStringOrComment(code, contentStart, openIndex)) {
|
|
139
|
+
isInsideScript = true;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!isInsideScript) {
|
|
145
|
+
// Found a real style tag, now find its close tag
|
|
146
|
+
const contentStart = openIndex + openMatch[0].length;
|
|
147
|
+
closeRegex.lastIndex = contentStart;
|
|
89
148
|
let closeMatch;
|
|
90
149
|
while ((closeMatch = closeRegex.exec(code)) !== null) {
|
|
91
|
-
|
|
150
|
+
// Make sure close tag is also outside scripts
|
|
151
|
+
let closeIsInsideScript = false;
|
|
152
|
+
for (const script of scriptBoundaries) {
|
|
153
|
+
const scriptContentStart = script.start + code.slice(script.start, script.end).indexOf('>') + 1;
|
|
154
|
+
if (closeMatch.index >= scriptContentStart && closeMatch.index < script.end) {
|
|
155
|
+
if (isInsideStringOrComment(code, scriptContentStart, closeMatch.index)) {
|
|
156
|
+
closeIsInsideScript = true;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!closeIsInsideScript) {
|
|
92
162
|
return {
|
|
93
|
-
start:
|
|
163
|
+
start: openIndex,
|
|
94
164
|
contentStart: contentStart,
|
|
95
165
|
contentEnd: closeMatch.index,
|
|
96
166
|
end: closeMatch.index + closeMatch[0].length
|
|
@@ -168,13 +238,13 @@ export function splitScript(script) {
|
|
|
168
238
|
let inString = false;
|
|
169
239
|
let stringChar = "";
|
|
170
240
|
let inMultiLineComment = false;
|
|
241
|
+
let escaped = false;
|
|
171
242
|
// Helper function to check if import is complete without semicolon
|
|
172
|
-
function isImportComplete(
|
|
243
|
+
function isImportComplete(lineIndex, braceCount, inString) {
|
|
173
244
|
// If we have balanced braces and not in a string, check if next non-empty line starts a new statement
|
|
174
245
|
if (braceCount === 0 && !inString) {
|
|
175
246
|
// Look ahead to see if next line starts a new statement/declaration
|
|
176
|
-
|
|
177
|
-
for (let i = nextLineIndex; i < lines.length; i++) {
|
|
247
|
+
for (let i = lineIndex + 1; i < lines.length; i++) {
|
|
178
248
|
const nextLine = lines[i].trim();
|
|
179
249
|
if (!nextLine || nextLine.startsWith("//") || nextLine.startsWith("/*")) {
|
|
180
250
|
continue; // Skip empty lines and comments
|
|
@@ -188,6 +258,7 @@ export function splitScript(script) {
|
|
|
188
258
|
}
|
|
189
259
|
return false;
|
|
190
260
|
}
|
|
261
|
+
// Process each line, maintaining string/comment state
|
|
191
262
|
for (let i = 0; i < lines.length; i++) {
|
|
192
263
|
const line = lines[i];
|
|
193
264
|
const trimmed = line.trim();
|
|
@@ -199,13 +270,34 @@ export function splitScript(script) {
|
|
|
199
270
|
else {
|
|
200
271
|
body.push(line);
|
|
201
272
|
}
|
|
202
|
-
|
|
203
|
-
|
|
273
|
+
// Check for end of multi-line comment, tracking strings
|
|
274
|
+
for (let j = 0; j < line.length; j++) {
|
|
275
|
+
const char = line[j];
|
|
276
|
+
if (escaped) {
|
|
277
|
+
escaped = false;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (char === '\\' && inString) {
|
|
281
|
+
escaped = true;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
285
|
+
inString = true;
|
|
286
|
+
stringChar = char;
|
|
287
|
+
}
|
|
288
|
+
else if (inString && char === stringChar) {
|
|
289
|
+
inString = false;
|
|
290
|
+
stringChar = "";
|
|
291
|
+
}
|
|
292
|
+
else if (!inString && char === '*' && line[j + 1] === '/') {
|
|
293
|
+
inMultiLineComment = false;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
204
296
|
}
|
|
205
297
|
continue;
|
|
206
298
|
}
|
|
207
|
-
// Check for start of multi-line comment
|
|
208
|
-
if (line.includes("/*")
|
|
299
|
+
// Check for start of multi-line comment (only if not in string)
|
|
300
|
+
if (!inString && line.includes("/*")) {
|
|
209
301
|
inMultiLineComment = true;
|
|
210
302
|
if (inImport) {
|
|
211
303
|
currentImport.push(line);
|
|
@@ -213,15 +305,37 @@ export function splitScript(script) {
|
|
|
213
305
|
else {
|
|
214
306
|
body.push(line);
|
|
215
307
|
}
|
|
216
|
-
if
|
|
217
|
-
|
|
308
|
+
// Check if comment ends on same line
|
|
309
|
+
for (let j = 0; j < line.length; j++) {
|
|
310
|
+
const char = line[j];
|
|
311
|
+
if (escaped) {
|
|
312
|
+
escaped = false;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (char === '\\' && inString) {
|
|
316
|
+
escaped = true;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
320
|
+
inString = true;
|
|
321
|
+
stringChar = char;
|
|
322
|
+
}
|
|
323
|
+
else if (inString && char === stringChar) {
|
|
324
|
+
inString = false;
|
|
325
|
+
stringChar = "";
|
|
326
|
+
}
|
|
327
|
+
else if (!inString && char === '*' && line[j + 1] === '/') {
|
|
328
|
+
inMultiLineComment = false;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
218
331
|
}
|
|
219
|
-
|
|
220
|
-
|
|
332
|
+
if (!inMultiLineComment) {
|
|
333
|
+
// Comment ended on same line
|
|
334
|
+
continue;
|
|
221
335
|
}
|
|
222
336
|
}
|
|
223
|
-
// Skip single-line comments when not in import
|
|
224
|
-
if (trimmed.startsWith("//")
|
|
337
|
+
// Skip single-line comments when not in import (only if not in string)
|
|
338
|
+
if (!inString && !inImport && trimmed.startsWith("//")) {
|
|
225
339
|
body.push(line);
|
|
226
340
|
continue;
|
|
227
341
|
}
|
|
@@ -230,37 +344,46 @@ export function splitScript(script) {
|
|
|
230
344
|
body.push(line);
|
|
231
345
|
continue;
|
|
232
346
|
}
|
|
233
|
-
//
|
|
234
|
-
|
|
347
|
+
// Process the line character by character to maintain string state
|
|
348
|
+
let lineBraceCount = 0;
|
|
349
|
+
let lineInString = inString;
|
|
350
|
+
let lineStringChar = stringChar;
|
|
351
|
+
let lineEscaped = false;
|
|
352
|
+
for (let j = 0; j < line.length; j++) {
|
|
353
|
+
const char = line[j];
|
|
354
|
+
if (lineEscaped) {
|
|
355
|
+
lineEscaped = false;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (char === '\\' && lineInString) {
|
|
359
|
+
lineEscaped = true;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (!lineInString && (char === '"' || char === "'" || char === '`')) {
|
|
363
|
+
lineInString = true;
|
|
364
|
+
lineStringChar = char;
|
|
365
|
+
}
|
|
366
|
+
else if (lineInString && char === lineStringChar) {
|
|
367
|
+
lineInString = false;
|
|
368
|
+
lineStringChar = "";
|
|
369
|
+
}
|
|
370
|
+
else if (!lineInString && char === '{') {
|
|
371
|
+
lineBraceCount++;
|
|
372
|
+
}
|
|
373
|
+
else if (!lineInString && char === '}') {
|
|
374
|
+
lineBraceCount--;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Start of import statement (only if not inside a string)
|
|
378
|
+
if (!inImport && !inString && trimmed.startsWith("import")) {
|
|
235
379
|
inImport = true;
|
|
236
380
|
currentImport = [line];
|
|
237
|
-
braceCount =
|
|
238
|
-
inString =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const char = line[j];
|
|
242
|
-
if (inString) {
|
|
243
|
-
if (char === stringChar && line[j - 1] !== "\\") {
|
|
244
|
-
inString = false;
|
|
245
|
-
stringChar = "";
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
250
|
-
inString = true;
|
|
251
|
-
stringChar = char;
|
|
252
|
-
}
|
|
253
|
-
else if (char === "{") {
|
|
254
|
-
braceCount++;
|
|
255
|
-
}
|
|
256
|
-
else if (char === "}") {
|
|
257
|
-
braceCount--;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// Check if import is complete
|
|
381
|
+
braceCount = lineBraceCount;
|
|
382
|
+
inString = lineInString;
|
|
383
|
+
stringChar = lineStringChar;
|
|
384
|
+
// Check if import is complete on this line
|
|
262
385
|
if ((trimmed.endsWith(";") ||
|
|
263
|
-
isImportComplete(
|
|
386
|
+
isImportComplete(i, braceCount, inString)) &&
|
|
264
387
|
braceCount === 0 && !inString) {
|
|
265
388
|
imports.push(currentImport.join("\n"));
|
|
266
389
|
currentImport = [];
|
|
@@ -269,31 +392,12 @@ export function splitScript(script) {
|
|
|
269
392
|
} // Continue import statement
|
|
270
393
|
else if (inImport) {
|
|
271
394
|
currentImport.push(line);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (inString) {
|
|
276
|
-
if (char === stringChar && line[j - 1] !== "\\") {
|
|
277
|
-
inString = false;
|
|
278
|
-
stringChar = "";
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
283
|
-
inString = true;
|
|
284
|
-
stringChar = char;
|
|
285
|
-
}
|
|
286
|
-
else if (char === "{") {
|
|
287
|
-
braceCount++;
|
|
288
|
-
}
|
|
289
|
-
else if (char === "}") {
|
|
290
|
-
braceCount--;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
395
|
+
braceCount += lineBraceCount;
|
|
396
|
+
inString = lineInString;
|
|
397
|
+
stringChar = lineStringChar;
|
|
294
398
|
// Check if import is complete
|
|
295
399
|
if ((trimmed.endsWith(";") ||
|
|
296
|
-
isImportComplete(
|
|
400
|
+
isImportComplete(i, braceCount, inString)) &&
|
|
297
401
|
braceCount === 0 && !inString) {
|
|
298
402
|
imports.push(currentImport.join("\n"));
|
|
299
403
|
currentImport = [];
|
|
@@ -302,11 +406,21 @@ export function splitScript(script) {
|
|
|
302
406
|
} // Regular body content
|
|
303
407
|
else {
|
|
304
408
|
body.push(line);
|
|
409
|
+
// Update global string state
|
|
410
|
+
inString = lineInString;
|
|
411
|
+
stringChar = lineStringChar;
|
|
305
412
|
}
|
|
306
413
|
}
|
|
307
|
-
// Handle unterminated import (likely malformed)
|
|
414
|
+
// Handle unterminated import (likely malformed or still in string)
|
|
308
415
|
if (currentImport.length > 0) {
|
|
309
|
-
|
|
416
|
+
if (inImport && !inString) {
|
|
417
|
+
// Import seems complete but wasn't captured properly
|
|
418
|
+
imports.push(currentImport.join("\n"));
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// Still in string or incomplete, treat as body
|
|
422
|
+
body.push(...currentImport);
|
|
423
|
+
}
|
|
310
424
|
}
|
|
311
425
|
return {
|
|
312
426
|
imports: imports.filter((imp) => imp.trim()),
|
package/docs/package-lock.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"name": "thyn-app",
|
|
9
9
|
"version": "0.0.0",
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"@thyn/core": "^0.0.
|
|
11
|
+
"@thyn/core": "^0.0.352",
|
|
12
12
|
"vite": "^6.3.5"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
@@ -742,9 +742,9 @@
|
|
|
742
742
|
]
|
|
743
743
|
},
|
|
744
744
|
"node_modules/@thyn/core": {
|
|
745
|
-
"version": "0.0.
|
|
746
|
-
"resolved": "https://registry.npmjs.org/@thyn/core/-/core-0.0.
|
|
747
|
-
"integrity": "sha512-
|
|
745
|
+
"version": "0.0.352",
|
|
746
|
+
"resolved": "https://registry.npmjs.org/@thyn/core/-/core-0.0.352.tgz",
|
|
747
|
+
"integrity": "sha512-XvVkZ63CdWxF+OIOnLoxPOesQ0OFlfr/1CY4/LNrZO2QTK4/I92cRlbDaW1ajN3YDSuvf7BeEuWZe5hYn8Jd/Q==",
|
|
748
748
|
"dev": true,
|
|
749
749
|
"license": "MIT",
|
|
750
750
|
"dependencies": {
|
package/docs/package.json
CHANGED
package/package.json
CHANGED
package/src/plugin/utils.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
export function extractParts(code: string) {
|
|
2
2
|
// Helper to check if a position is inside a string literal or comment
|
|
3
|
-
|
|
3
|
+
// within a specific range (used for checking script content only)
|
|
4
|
+
function isInsideStringOrComment(code: string, startPos: number, endPos: number): boolean {
|
|
4
5
|
let inString = false;
|
|
5
6
|
let stringChar = '';
|
|
6
7
|
let escaped = false;
|
|
7
8
|
let inLineComment = false;
|
|
8
9
|
let inBlockComment = false;
|
|
9
10
|
|
|
10
|
-
for (let i =
|
|
11
|
+
for (let i = startPos; i < endPos; i++) {
|
|
11
12
|
const char = code[i];
|
|
12
13
|
const nextChar = code[i + 1];
|
|
13
14
|
|
|
@@ -61,50 +62,127 @@ export function extractParts(code: string) {
|
|
|
61
62
|
return inString || inLineComment || inBlockComment;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
// Find
|
|
65
|
-
function
|
|
65
|
+
// Find all script tag positions with their boundaries
|
|
66
|
+
function findScriptBoundaries(code: string): Array<{ start: number; end: number; attrs: string }> {
|
|
67
|
+
const boundaries = [];
|
|
66
68
|
const openRegex = /<script([^>]*)>/gi;
|
|
69
|
+
const closeRegex = /<\/script>/gi;
|
|
67
70
|
let openMatch;
|
|
68
71
|
|
|
69
72
|
while ((openMatch = openRegex.exec(code)) !== null) {
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const openIndex = openMatch.index;
|
|
74
|
+
const openLength = openMatch[0].length;
|
|
75
|
+
const attrs = openMatch[1] || '';
|
|
76
|
+
|
|
77
|
+
// Find matching close tag
|
|
78
|
+
closeRegex.lastIndex = openIndex + openLength;
|
|
79
|
+
let closeMatch;
|
|
80
|
+
while ((closeMatch = closeRegex.exec(code)) !== null) {
|
|
81
|
+
// Check if this close tag is inside a JS string within this script
|
|
82
|
+
const contentStart = openIndex + openLength;
|
|
83
|
+
if (!isInsideStringOrComment(code, contentStart, closeMatch.index)) {
|
|
84
|
+
boundaries.push({
|
|
85
|
+
start: openIndex,
|
|
86
|
+
end: closeMatch.index + closeMatch[0].length,
|
|
87
|
+
attrs
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return boundaries;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find the real script section (not inside a string of another script)
|
|
98
|
+
function findScriptSection(code: string): { start: number; contentStart: number; contentEnd: number; end: number; attrs: string } | null {
|
|
99
|
+
const allBoundaries = findScriptBoundaries(code);
|
|
100
|
+
if (allBoundaries.length === 0) return null;
|
|
101
|
+
|
|
102
|
+
// The first script tag is the real one if it's not inside any other script's string
|
|
103
|
+
// Check each boundary to see if it's inside another script's content
|
|
104
|
+
for (const boundary of allBoundaries) {
|
|
105
|
+
let isInsideAnotherScript = false;
|
|
106
|
+
|
|
107
|
+
for (const other of allBoundaries) {
|
|
108
|
+
if (other === boundary) continue;
|
|
72
109
|
|
|
73
|
-
//
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
attrs: openMatch[1] || ''
|
|
84
|
-
};
|
|
110
|
+
// Check if this boundary is inside another script's content
|
|
111
|
+
const otherContentStart = other.start + code.slice(other.start, other.end).indexOf('>') + 1;
|
|
112
|
+
const otherContentEnd = other.end - code.slice(other.end - 10, other.end).indexOf('<') - 10 + code.slice(other.end - 10, other.end).indexOf('<');
|
|
113
|
+
|
|
114
|
+
if (boundary.start > otherContentStart && boundary.start < other.end) {
|
|
115
|
+
// This boundary is inside another script's content area
|
|
116
|
+
// Check if it's inside a JS string
|
|
117
|
+
if (isInsideStringOrComment(code, otherContentStart, boundary.start)) {
|
|
118
|
+
isInsideAnotherScript = true;
|
|
119
|
+
break;
|
|
85
120
|
}
|
|
86
121
|
}
|
|
87
122
|
}
|
|
123
|
+
|
|
124
|
+
if (!isInsideAnotherScript) {
|
|
125
|
+
// This is the real script section
|
|
126
|
+
const openTagEnd = code.indexOf('>', boundary.start) + 1;
|
|
127
|
+
const closeTagStart = code.lastIndexOf('<', boundary.end - 1);
|
|
128
|
+
return {
|
|
129
|
+
start: boundary.start,
|
|
130
|
+
contentStart: openTagEnd,
|
|
131
|
+
contentEnd: closeTagStart,
|
|
132
|
+
end: boundary.end,
|
|
133
|
+
attrs: boundary.attrs
|
|
134
|
+
};
|
|
135
|
+
}
|
|
88
136
|
}
|
|
137
|
+
|
|
89
138
|
return null;
|
|
90
139
|
}
|
|
91
140
|
|
|
92
|
-
// Find
|
|
141
|
+
// Find the real style section (outside of script sections)
|
|
93
142
|
function findStyleSection(code: string): { start: number; contentStart: number; contentEnd: number; end: number } | null {
|
|
143
|
+
const scriptBoundaries = findScriptBoundaries(code);
|
|
94
144
|
const openRegex = /<style[^>]*>/gi;
|
|
145
|
+
const closeRegex = /<\/style>/gi;
|
|
95
146
|
let openMatch;
|
|
96
147
|
|
|
97
148
|
while ((openMatch = openRegex.exec(code)) !== null) {
|
|
98
|
-
|
|
99
|
-
|
|
149
|
+
const openIndex = openMatch.index;
|
|
150
|
+
|
|
151
|
+
// Check if this style tag is inside any script section
|
|
152
|
+
let isInsideScript = false;
|
|
153
|
+
for (const script of scriptBoundaries) {
|
|
154
|
+
const contentStart = script.start + code.slice(script.start, script.end).indexOf('>') + 1;
|
|
155
|
+
const contentEnd = script.end - code.slice(script.end - 10, script.end).indexOf('<') - 10 + code.slice(script.end - 10, script.end).indexOf('<');
|
|
100
156
|
|
|
101
|
-
|
|
102
|
-
|
|
157
|
+
if (openIndex >= contentStart && openIndex < script.end) {
|
|
158
|
+
// It's in the script section, check if inside a JS string
|
|
159
|
+
if (isInsideStringOrComment(code, contentStart, openIndex)) {
|
|
160
|
+
isInsideScript = true;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!isInsideScript) {
|
|
167
|
+
// Found a real style tag, now find its close tag
|
|
168
|
+
const contentStart = openIndex + openMatch[0].length;
|
|
169
|
+
closeRegex.lastIndex = contentStart;
|
|
103
170
|
let closeMatch;
|
|
104
171
|
while ((closeMatch = closeRegex.exec(code)) !== null) {
|
|
105
|
-
|
|
172
|
+
// Make sure close tag is also outside scripts
|
|
173
|
+
let closeIsInsideScript = false;
|
|
174
|
+
for (const script of scriptBoundaries) {
|
|
175
|
+
const scriptContentStart = script.start + code.slice(script.start, script.end).indexOf('>') + 1;
|
|
176
|
+
if (closeMatch.index >= scriptContentStart && closeMatch.index < script.end) {
|
|
177
|
+
if (isInsideStringOrComment(code, scriptContentStart, closeMatch.index)) {
|
|
178
|
+
closeIsInsideScript = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!closeIsInsideScript) {
|
|
106
184
|
return {
|
|
107
|
-
start:
|
|
185
|
+
start: openIndex,
|
|
108
186
|
contentStart: contentStart,
|
|
109
187
|
contentEnd: closeMatch.index,
|
|
110
188
|
end: closeMatch.index + closeMatch[0].length
|
|
@@ -186,20 +264,20 @@ export function splitScript(script: string) {
|
|
|
186
264
|
const lines = script.split("\n");
|
|
187
265
|
const imports = [];
|
|
188
266
|
const body = [];
|
|
189
|
-
let currentImport = [];
|
|
267
|
+
let currentImport: string[] = [];
|
|
190
268
|
let inImport = false;
|
|
191
269
|
let braceCount = 0;
|
|
192
270
|
let inString = false;
|
|
193
271
|
let stringChar = "";
|
|
194
272
|
let inMultiLineComment = false;
|
|
273
|
+
let escaped = false;
|
|
195
274
|
|
|
196
275
|
// Helper function to check if import is complete without semicolon
|
|
197
|
-
function isImportComplete(
|
|
276
|
+
function isImportComplete(lineIndex: number, braceCount: number, inString: boolean): boolean {
|
|
198
277
|
// If we have balanced braces and not in a string, check if next non-empty line starts a new statement
|
|
199
278
|
if (braceCount === 0 && !inString) {
|
|
200
279
|
// Look ahead to see if next line starts a new statement/declaration
|
|
201
|
-
|
|
202
|
-
for (let i = nextLineIndex; i < lines.length; i++) {
|
|
280
|
+
for (let i = lineIndex + 1; i < lines.length; i++) {
|
|
203
281
|
const nextLine = lines[i].trim();
|
|
204
282
|
if (
|
|
205
283
|
!nextLine || nextLine.startsWith("//") || nextLine.startsWith("/*")
|
|
@@ -216,6 +294,7 @@ export function splitScript(script: string) {
|
|
|
216
294
|
return false;
|
|
217
295
|
}
|
|
218
296
|
|
|
297
|
+
// Process each line, maintaining string/comment state
|
|
219
298
|
for (let i = 0; i < lines.length; i++) {
|
|
220
299
|
const line = lines[i];
|
|
221
300
|
const trimmed = line.trim();
|
|
@@ -228,14 +307,33 @@ export function splitScript(script: string) {
|
|
|
228
307
|
body.push(line);
|
|
229
308
|
}
|
|
230
309
|
|
|
231
|
-
|
|
232
|
-
|
|
310
|
+
// Check for end of multi-line comment, tracking strings
|
|
311
|
+
for (let j = 0; j < line.length; j++) {
|
|
312
|
+
const char = line[j];
|
|
313
|
+
if (escaped) {
|
|
314
|
+
escaped = false;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (char === '\\' && inString) {
|
|
318
|
+
escaped = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
322
|
+
inString = true;
|
|
323
|
+
stringChar = char;
|
|
324
|
+
} else if (inString && char === stringChar) {
|
|
325
|
+
inString = false;
|
|
326
|
+
stringChar = "";
|
|
327
|
+
} else if (!inString && char === '*' && line[j + 1] === '/') {
|
|
328
|
+
inMultiLineComment = false;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
233
331
|
}
|
|
234
332
|
continue;
|
|
235
333
|
}
|
|
236
334
|
|
|
237
|
-
// Check for start of multi-line comment
|
|
238
|
-
if (line.includes("/*")
|
|
335
|
+
// Check for start of multi-line comment (only if not in string)
|
|
336
|
+
if (!inString && line.includes("/*")) {
|
|
239
337
|
inMultiLineComment = true;
|
|
240
338
|
if (inImport) {
|
|
241
339
|
currentImport.push(line);
|
|
@@ -243,15 +341,37 @@ export function splitScript(script: string) {
|
|
|
243
341
|
body.push(line);
|
|
244
342
|
}
|
|
245
343
|
|
|
246
|
-
if
|
|
344
|
+
// Check if comment ends on same line
|
|
345
|
+
for (let j = 0; j < line.length; j++) {
|
|
346
|
+
const char = line[j];
|
|
347
|
+
if (escaped) {
|
|
348
|
+
escaped = false;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (char === '\\' && inString) {
|
|
352
|
+
escaped = true;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
356
|
+
inString = true;
|
|
357
|
+
stringChar = char;
|
|
358
|
+
} else if (inString && char === stringChar) {
|
|
359
|
+
inString = false;
|
|
360
|
+
stringChar = "";
|
|
361
|
+
} else if (!inString && char === '*' && line[j + 1] === '/') {
|
|
362
|
+
inMultiLineComment = false;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!inMultiLineComment) {
|
|
368
|
+
// Comment ended on same line
|
|
247
369
|
continue;
|
|
248
|
-
} else {
|
|
249
|
-
inMultiLineComment = false;
|
|
250
370
|
}
|
|
251
371
|
}
|
|
252
372
|
|
|
253
|
-
// Skip single-line comments when not in import
|
|
254
|
-
if (trimmed.startsWith("//")
|
|
373
|
+
// Skip single-line comments when not in import (only if not in string)
|
|
374
|
+
if (!inString && !inImport && trimmed.startsWith("//")) {
|
|
255
375
|
body.push(line);
|
|
256
376
|
continue;
|
|
257
377
|
}
|
|
@@ -262,38 +382,50 @@ export function splitScript(script: string) {
|
|
|
262
382
|
continue;
|
|
263
383
|
}
|
|
264
384
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
inString = false;
|
|
385
|
+
// Process the line character by character to maintain string state
|
|
386
|
+
let lineBraceCount = 0;
|
|
387
|
+
let lineInString = inString;
|
|
388
|
+
let lineStringChar = stringChar;
|
|
389
|
+
let lineEscaped = false;
|
|
271
390
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const char = line[j];
|
|
391
|
+
for (let j = 0; j < line.length; j++) {
|
|
392
|
+
const char = line[j];
|
|
275
393
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
stringChar = "";
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
283
|
-
inString = true;
|
|
284
|
-
stringChar = char;
|
|
285
|
-
} else if (char === "{") {
|
|
286
|
-
braceCount++;
|
|
287
|
-
} else if (char === "}") {
|
|
288
|
-
braceCount--;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
394
|
+
if (lineEscaped) {
|
|
395
|
+
lineEscaped = false;
|
|
396
|
+
continue;
|
|
291
397
|
}
|
|
292
398
|
|
|
293
|
-
|
|
399
|
+
if (char === '\\' && lineInString) {
|
|
400
|
+
lineEscaped = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!lineInString && (char === '"' || char === "'" || char === '`')) {
|
|
405
|
+
lineInString = true;
|
|
406
|
+
lineStringChar = char;
|
|
407
|
+
} else if (lineInString && char === lineStringChar) {
|
|
408
|
+
lineInString = false;
|
|
409
|
+
lineStringChar = "";
|
|
410
|
+
} else if (!lineInString && char === '{') {
|
|
411
|
+
lineBraceCount++;
|
|
412
|
+
} else if (!lineInString && char === '}') {
|
|
413
|
+
lineBraceCount--;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Start of import statement (only if not inside a string)
|
|
418
|
+
if (!inImport && !inString && trimmed.startsWith("import")) {
|
|
419
|
+
inImport = true;
|
|
420
|
+
currentImport = [line];
|
|
421
|
+
braceCount = lineBraceCount;
|
|
422
|
+
inString = lineInString;
|
|
423
|
+
stringChar = lineStringChar;
|
|
424
|
+
|
|
425
|
+
// Check if import is complete on this line
|
|
294
426
|
if (
|
|
295
427
|
(trimmed.endsWith(";") ||
|
|
296
|
-
isImportComplete(
|
|
428
|
+
isImportComplete(i, braceCount, inString)) &&
|
|
297
429
|
braceCount === 0 && !inString
|
|
298
430
|
) {
|
|
299
431
|
imports.push(currentImport.join("\n"));
|
|
@@ -303,32 +435,14 @@ export function splitScript(script: string) {
|
|
|
303
435
|
} // Continue import statement
|
|
304
436
|
else if (inImport) {
|
|
305
437
|
currentImport.push(line);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const char = line[j];
|
|
310
|
-
|
|
311
|
-
if (inString) {
|
|
312
|
-
if (char === stringChar && line[j - 1] !== "\\") {
|
|
313
|
-
inString = false;
|
|
314
|
-
stringChar = "";
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
318
|
-
inString = true;
|
|
319
|
-
stringChar = char;
|
|
320
|
-
} else if (char === "{") {
|
|
321
|
-
braceCount++;
|
|
322
|
-
} else if (char === "}") {
|
|
323
|
-
braceCount--;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
438
|
+
braceCount += lineBraceCount;
|
|
439
|
+
inString = lineInString;
|
|
440
|
+
stringChar = lineStringChar;
|
|
327
441
|
|
|
328
442
|
// Check if import is complete
|
|
329
443
|
if (
|
|
330
444
|
(trimmed.endsWith(";") ||
|
|
331
|
-
isImportComplete(
|
|
445
|
+
isImportComplete(i, braceCount, inString)) &&
|
|
332
446
|
braceCount === 0 && !inString
|
|
333
447
|
) {
|
|
334
448
|
imports.push(currentImport.join("\n"));
|
|
@@ -338,12 +452,21 @@ export function splitScript(script: string) {
|
|
|
338
452
|
} // Regular body content
|
|
339
453
|
else {
|
|
340
454
|
body.push(line);
|
|
455
|
+
// Update global string state
|
|
456
|
+
inString = lineInString;
|
|
457
|
+
stringChar = lineStringChar;
|
|
341
458
|
}
|
|
342
459
|
}
|
|
343
460
|
|
|
344
|
-
// Handle unterminated import (likely malformed)
|
|
461
|
+
// Handle unterminated import (likely malformed or still in string)
|
|
345
462
|
if (currentImport.length > 0) {
|
|
346
|
-
|
|
463
|
+
if (inImport && !inString) {
|
|
464
|
+
// Import seems complete but wasn't captured properly
|
|
465
|
+
imports.push(currentImport.join("\n"));
|
|
466
|
+
} else {
|
|
467
|
+
// Still in string or incomplete, treat as body
|
|
468
|
+
body.push(...currentImport);
|
|
469
|
+
}
|
|
347
470
|
}
|
|
348
471
|
|
|
349
472
|
return {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import ImportInString from "./ImportInString.thyn";
|
|
3
|
+
|
|
4
|
+
describe("ImportInString component", () => {
|
|
5
|
+
it("should not treat imports inside string literals as real imports", () => {
|
|
6
|
+
const root = ImportInString();
|
|
7
|
+
expect(root.textContent).toBe("42");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should include the import statements in the codeSnippet", () => {
|
|
11
|
+
const root = ImportInString();
|
|
12
|
+
// The codeSnippet should be available (this tests the script was parsed correctly)
|
|
13
|
+
expect(root).toBeTruthy();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
const codeSnippet = `// main.js
|
|
3
|
+
import { mount } from '@thyn/core';
|
|
4
|
+
import App from './App.thyn';
|
|
5
|
+
|
|
6
|
+
mount(App, document.body);`;
|
|
7
|
+
|
|
8
|
+
const value = 42;
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div>{{ value }}</div>
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
div { color: red; }
|
|
15
|
+
</style>
|