@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.
@@ -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
- function isInsideStringOrComment(code, pos) {
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 = 0; i < pos; 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 first real <script> tag (not inside a string)
56
- function findScriptSection(code) {
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
- if (!isInsideStringOrComment(code, openMatch.index)) {
61
- const contentStart = openMatch.index + openMatch[0].length;
62
- // Find the first </script> that is not inside a string
63
- const closeRegex = /<\/script>/gi;
64
- let closeMatch;
65
- while ((closeMatch = closeRegex.exec(code)) !== null) {
66
- if (closeMatch.index >= contentStart && !isInsideStringOrComment(code, closeMatch.index)) {
67
- return {
68
- start: openMatch.index,
69
- contentStart: contentStart,
70
- contentEnd: closeMatch.index,
71
- end: closeMatch.index + closeMatch[0].length,
72
- attrs: openMatch[1] || ''
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 first real <style> tag (not inside a string)
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
- if (!isInsideStringOrComment(code, openMatch.index)) {
86
- const contentStart = openMatch.index + openMatch[0].length;
87
- // Find the first </style> that is not inside a string
88
- const closeRegex = /<\/style>/gi;
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
- if (closeMatch.index >= contentStart && !isInsideStringOrComment(code, closeMatch.index)) {
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: openMatch.index,
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(line, braceCount, inString) {
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
- const nextLineIndex = lines.indexOf(line) + 1;
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
- if (line.includes("*/")) {
203
- inMultiLineComment = false;
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("/*") && !inString) {
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 (!line.includes("*/")) {
217
- continue;
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
- else {
220
- inMultiLineComment = false;
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("//") && !inImport) {
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
- // Start of import statement
234
- if (!inImport && trimmed.startsWith("import")) {
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 = 0;
238
- inString = false;
239
- // Count braces and track strings in the import line
240
- for (let j = 0; j < line.length; j++) {
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(trimmed, braceCount, inString)) &&
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
- // Count braces and track strings in the current line
273
- for (let j = 0; j < line.length; j++) {
274
- const char = line[j];
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(trimmed, braceCount, inString)) &&
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
- imports.push(currentImport.join("\n"));
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()),
@@ -8,7 +8,7 @@
8
8
  "name": "thyn-app",
9
9
  "version": "0.0.0",
10
10
  "devDependencies": {
11
- "@thyn/core": "^0.0.350",
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.350",
746
- "resolved": "https://registry.npmjs.org/@thyn/core/-/core-0.0.350.tgz",
747
- "integrity": "sha512-SZTB1jjjsXEkZg1BlH7s4Lhe8kWN/74ufNlNMnI86x+9tiyc0yvMLh+GFhe7VkIS//NrEwdgjR3ktpJlGeMEfA==",
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
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "devDependencies": {
12
- "@thyn/core": "^0.0.350",
12
+ "@thyn/core": "^0.0.352",
13
13
  "vite": "^6.3.5"
14
14
  }
15
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thyn/core",
3
- "version": "0.0.351",
3
+ "version": "0.0.353",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -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
- function isInsideStringOrComment(code: string, pos: number): boolean {
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 = 0; i < pos; 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 first real <script> tag (not inside a string)
65
- function findScriptSection(code: string): { start: number; contentStart: number; contentEnd: number; end: number; attrs: string } | null {
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
- if (!isInsideStringOrComment(code, openMatch.index)) {
71
- const contentStart = openMatch.index + openMatch[0].length;
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
- // Find the first </script> that is not inside a string
74
- const closeRegex = /<\/script>/gi;
75
- let closeMatch;
76
- while ((closeMatch = closeRegex.exec(code)) !== null) {
77
- if (closeMatch.index >= contentStart && !isInsideStringOrComment(code, closeMatch.index)) {
78
- return {
79
- start: openMatch.index,
80
- contentStart: contentStart,
81
- contentEnd: closeMatch.index,
82
- end: closeMatch.index + closeMatch[0].length,
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 first real <style> tag (not inside a string)
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
- if (!isInsideStringOrComment(code, openMatch.index)) {
99
- const contentStart = openMatch.index + openMatch[0].length;
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
- // Find the first </style> that is not inside a string
102
- const closeRegex = /<\/style>/gi;
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
- if (closeMatch.index >= contentStart && !isInsideStringOrComment(code, closeMatch.index)) {
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: openMatch.index,
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(line, braceCount, inString) {
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
- const nextLineIndex = lines.indexOf(line) + 1;
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
- if (line.includes("*/")) {
232
- inMultiLineComment = false;
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("/*") && !inString) {
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 (!line.includes("*/")) {
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("//") && !inImport) {
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
- // Start of import statement
266
- if (!inImport && trimmed.startsWith("import")) {
267
- inImport = true;
268
- currentImport = [line];
269
- braceCount = 0;
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
- // Count braces and track strings in the import line
273
- for (let j = 0; j < line.length; j++) {
274
- const char = line[j];
391
+ for (let j = 0; j < line.length; j++) {
392
+ const char = line[j];
275
393
 
276
- if (inString) {
277
- if (char === stringChar && line[j - 1] !== "\\") {
278
- inString = false;
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
- // Check if import is complete
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(trimmed, braceCount, inString)) &&
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
- // Count braces and track strings in the current line
308
- for (let j = 0; j < line.length; j++) {
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(trimmed, braceCount, inString)) &&
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
- imports.push(currentImport.join("\n"));
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>