@thyn/core 0.0.350 → 0.0.352
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/index.js +1 -1
- package/dist/plugin/utils.js +241 -76
- package/docs/package-lock.json +4 -4
- package/docs/package.json +1 -1
- package/docs/src/pages/Home.thyn +24 -22
- package/package.json +1 -1
- package/src/plugin/index.ts +1 -1
- package/src/plugin/utils.ts +258 -73
- package/tests/CodeSnippet.test.ts +28 -0
- package/tests/CodeSnippet.thyn +31 -0
- package/tests/ImportInString.test.ts +15 -0
- package/tests/ImportInString.thyn +15 -0
package/dist/plugin/index.js
CHANGED
|
@@ -815,7 +815,7 @@ async function transformHTMLtoJSX(html, style) {
|
|
|
815
815
|
const template = parseHTML("<template>" + processedHTML + "</template>");
|
|
816
816
|
const rootElement = template.content.firstElementChild;
|
|
817
817
|
let scopedStyle = null;
|
|
818
|
-
if (style) {
|
|
818
|
+
if (style && rootElement) {
|
|
819
819
|
addScopeId(rootElement, scopeId);
|
|
820
820
|
scopedStyle = await scopeSelectors(style, scopeId);
|
|
821
821
|
}
|
package/dist/plugin/utils.js
CHANGED
|
@@ -1,21 +1,142 @@
|
|
|
1
1
|
export function extractParts(code) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
// Helper to check if a position is inside a string literal or comment
|
|
3
|
+
function isInsideStringOrComment(code, pos) {
|
|
4
|
+
let inString = false;
|
|
5
|
+
let stringChar = '';
|
|
6
|
+
let escaped = false;
|
|
7
|
+
let inLineComment = false;
|
|
8
|
+
let inBlockComment = false;
|
|
9
|
+
for (let i = 0; i < pos; i++) {
|
|
10
|
+
const char = code[i];
|
|
11
|
+
const nextChar = code[i + 1];
|
|
12
|
+
if (inLineComment) {
|
|
13
|
+
if (char === '\n') {
|
|
14
|
+
inLineComment = false;
|
|
15
|
+
}
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (inBlockComment) {
|
|
19
|
+
if (char === '*' && nextChar === '/') {
|
|
20
|
+
inBlockComment = false;
|
|
21
|
+
i++; // skip the '/'
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (escaped) {
|
|
26
|
+
escaped = false;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (char === '\\') {
|
|
30
|
+
escaped = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Check for comment start
|
|
34
|
+
if (char === '/' && nextChar === '/') {
|
|
35
|
+
inLineComment = true;
|
|
36
|
+
i++; // skip the second '/'
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (char === '/' && nextChar === '*') {
|
|
40
|
+
inBlockComment = true;
|
|
41
|
+
i++; // skip the '*'
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
45
|
+
inString = true;
|
|
46
|
+
stringChar = char;
|
|
47
|
+
}
|
|
48
|
+
else if (inString && char === stringChar) {
|
|
49
|
+
inString = false;
|
|
50
|
+
stringChar = '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return inString || inLineComment || inBlockComment;
|
|
54
|
+
}
|
|
55
|
+
// Find first real <script> tag (not inside a string)
|
|
56
|
+
function findScriptSection(code) {
|
|
57
|
+
const openRegex = /<script([^>]*)>/gi;
|
|
58
|
+
let openMatch;
|
|
59
|
+
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
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Find first real <style> tag (not inside a string)
|
|
81
|
+
function findStyleSection(code) {
|
|
82
|
+
const openRegex = /<style[^>]*>/gi;
|
|
83
|
+
let openMatch;
|
|
84
|
+
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;
|
|
89
|
+
let closeMatch;
|
|
90
|
+
while ((closeMatch = closeRegex.exec(code)) !== null) {
|
|
91
|
+
if (closeMatch.index >= contentStart && !isInsideStringOrComment(code, closeMatch.index)) {
|
|
92
|
+
return {
|
|
93
|
+
start: openMatch.index,
|
|
94
|
+
contentStart: contentStart,
|
|
95
|
+
contentEnd: closeMatch.index,
|
|
96
|
+
end: closeMatch.index + closeMatch[0].length
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
// Extract sections
|
|
105
|
+
const scriptSection = findScriptSection(code);
|
|
106
|
+
const styleSection = findStyleSection(code);
|
|
107
|
+
let script = "";
|
|
8
108
|
let scriptLang = "js";
|
|
9
|
-
if (
|
|
10
|
-
|
|
109
|
+
if (scriptSection) {
|
|
110
|
+
script = code.slice(scriptSection.contentStart, scriptSection.contentEnd).trim();
|
|
111
|
+
const langMatch = scriptSection.attrs.match(/lang\s*=\s*["']([^"']+)["']/);
|
|
11
112
|
if (langMatch) {
|
|
12
113
|
scriptLang = langMatch[1];
|
|
13
114
|
}
|
|
14
115
|
}
|
|
116
|
+
let style = "";
|
|
117
|
+
if (styleSection) {
|
|
118
|
+
style = code.slice(styleSection.contentStart, styleSection.contentEnd).trim();
|
|
119
|
+
}
|
|
120
|
+
// Build HTML by removing script and style sections
|
|
121
|
+
// Remove from highest index to lowest to preserve indices
|
|
122
|
+
let html = code;
|
|
123
|
+
const sections = [];
|
|
124
|
+
if (scriptSection) {
|
|
125
|
+
sections.push({ start: scriptSection.start, end: scriptSection.end });
|
|
126
|
+
}
|
|
127
|
+
if (styleSection) {
|
|
128
|
+
sections.push({ start: styleSection.start, end: styleSection.end });
|
|
129
|
+
}
|
|
130
|
+
// Sort by start position descending (remove from end first)
|
|
131
|
+
sections.sort((a, b) => b.start - a.start);
|
|
132
|
+
for (const section of sections) {
|
|
133
|
+
html = html.slice(0, section.start) + html.slice(section.end);
|
|
134
|
+
}
|
|
135
|
+
html = html.trim();
|
|
15
136
|
return {
|
|
16
|
-
script
|
|
137
|
+
script,
|
|
17
138
|
scriptLang,
|
|
18
|
-
style
|
|
139
|
+
style,
|
|
19
140
|
html,
|
|
20
141
|
};
|
|
21
142
|
}
|
|
@@ -47,13 +168,13 @@ export function splitScript(script) {
|
|
|
47
168
|
let inString = false;
|
|
48
169
|
let stringChar = "";
|
|
49
170
|
let inMultiLineComment = false;
|
|
171
|
+
let escaped = false;
|
|
50
172
|
// Helper function to check if import is complete without semicolon
|
|
51
|
-
function isImportComplete(
|
|
173
|
+
function isImportComplete(lineIndex, braceCount, inString) {
|
|
52
174
|
// If we have balanced braces and not in a string, check if next non-empty line starts a new statement
|
|
53
175
|
if (braceCount === 0 && !inString) {
|
|
54
176
|
// Look ahead to see if next line starts a new statement/declaration
|
|
55
|
-
|
|
56
|
-
for (let i = nextLineIndex; i < lines.length; i++) {
|
|
177
|
+
for (let i = lineIndex + 1; i < lines.length; i++) {
|
|
57
178
|
const nextLine = lines[i].trim();
|
|
58
179
|
if (!nextLine || nextLine.startsWith("//") || nextLine.startsWith("/*")) {
|
|
59
180
|
continue; // Skip empty lines and comments
|
|
@@ -67,6 +188,7 @@ export function splitScript(script) {
|
|
|
67
188
|
}
|
|
68
189
|
return false;
|
|
69
190
|
}
|
|
191
|
+
// Process each line, maintaining string/comment state
|
|
70
192
|
for (let i = 0; i < lines.length; i++) {
|
|
71
193
|
const line = lines[i];
|
|
72
194
|
const trimmed = line.trim();
|
|
@@ -78,13 +200,34 @@ export function splitScript(script) {
|
|
|
78
200
|
else {
|
|
79
201
|
body.push(line);
|
|
80
202
|
}
|
|
81
|
-
|
|
82
|
-
|
|
203
|
+
// Check for end of multi-line comment, tracking strings
|
|
204
|
+
for (let j = 0; j < line.length; j++) {
|
|
205
|
+
const char = line[j];
|
|
206
|
+
if (escaped) {
|
|
207
|
+
escaped = false;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (char === '\\' && inString) {
|
|
211
|
+
escaped = true;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
215
|
+
inString = true;
|
|
216
|
+
stringChar = char;
|
|
217
|
+
}
|
|
218
|
+
else if (inString && char === stringChar) {
|
|
219
|
+
inString = false;
|
|
220
|
+
stringChar = "";
|
|
221
|
+
}
|
|
222
|
+
else if (!inString && char === '*' && line[j + 1] === '/') {
|
|
223
|
+
inMultiLineComment = false;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
83
226
|
}
|
|
84
227
|
continue;
|
|
85
228
|
}
|
|
86
|
-
// Check for start of multi-line comment
|
|
87
|
-
if (line.includes("/*")
|
|
229
|
+
// Check for start of multi-line comment (only if not in string)
|
|
230
|
+
if (!inString && line.includes("/*")) {
|
|
88
231
|
inMultiLineComment = true;
|
|
89
232
|
if (inImport) {
|
|
90
233
|
currentImport.push(line);
|
|
@@ -92,15 +235,37 @@ export function splitScript(script) {
|
|
|
92
235
|
else {
|
|
93
236
|
body.push(line);
|
|
94
237
|
}
|
|
95
|
-
if
|
|
96
|
-
|
|
238
|
+
// Check if comment ends on same line
|
|
239
|
+
for (let j = 0; j < line.length; j++) {
|
|
240
|
+
const char = line[j];
|
|
241
|
+
if (escaped) {
|
|
242
|
+
escaped = false;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (char === '\\' && inString) {
|
|
246
|
+
escaped = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
250
|
+
inString = true;
|
|
251
|
+
stringChar = char;
|
|
252
|
+
}
|
|
253
|
+
else if (inString && char === stringChar) {
|
|
254
|
+
inString = false;
|
|
255
|
+
stringChar = "";
|
|
256
|
+
}
|
|
257
|
+
else if (!inString && char === '*' && line[j + 1] === '/') {
|
|
258
|
+
inMultiLineComment = false;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
97
261
|
}
|
|
98
|
-
|
|
99
|
-
|
|
262
|
+
if (!inMultiLineComment) {
|
|
263
|
+
// Comment ended on same line
|
|
264
|
+
continue;
|
|
100
265
|
}
|
|
101
266
|
}
|
|
102
|
-
// Skip single-line comments when not in import
|
|
103
|
-
if (trimmed.startsWith("//")
|
|
267
|
+
// Skip single-line comments when not in import (only if not in string)
|
|
268
|
+
if (!inString && !inImport && trimmed.startsWith("//")) {
|
|
104
269
|
body.push(line);
|
|
105
270
|
continue;
|
|
106
271
|
}
|
|
@@ -109,37 +274,46 @@ export function splitScript(script) {
|
|
|
109
274
|
body.push(line);
|
|
110
275
|
continue;
|
|
111
276
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
277
|
+
// Process the line character by character to maintain string state
|
|
278
|
+
let lineBraceCount = 0;
|
|
279
|
+
let lineInString = inString;
|
|
280
|
+
let lineStringChar = stringChar;
|
|
281
|
+
let lineEscaped = false;
|
|
282
|
+
for (let j = 0; j < line.length; j++) {
|
|
283
|
+
const char = line[j];
|
|
284
|
+
if (lineEscaped) {
|
|
285
|
+
lineEscaped = false;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (char === '\\' && lineInString) {
|
|
289
|
+
lineEscaped = true;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (!lineInString && (char === '"' || char === "'" || char === '`')) {
|
|
293
|
+
lineInString = true;
|
|
294
|
+
lineStringChar = char;
|
|
295
|
+
}
|
|
296
|
+
else if (lineInString && char === lineStringChar) {
|
|
297
|
+
lineInString = false;
|
|
298
|
+
lineStringChar = "";
|
|
299
|
+
}
|
|
300
|
+
else if (!lineInString && char === '{') {
|
|
301
|
+
lineBraceCount++;
|
|
302
|
+
}
|
|
303
|
+
else if (!lineInString && char === '}') {
|
|
304
|
+
lineBraceCount--;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Start of import statement (only if not inside a string)
|
|
308
|
+
if (!inImport && !inString && trimmed.startsWith("import")) {
|
|
114
309
|
inImport = true;
|
|
115
310
|
currentImport = [line];
|
|
116
|
-
braceCount =
|
|
117
|
-
inString =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const char = line[j];
|
|
121
|
-
if (inString) {
|
|
122
|
-
if (char === stringChar && line[j - 1] !== "\\") {
|
|
123
|
-
inString = false;
|
|
124
|
-
stringChar = "";
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
129
|
-
inString = true;
|
|
130
|
-
stringChar = char;
|
|
131
|
-
}
|
|
132
|
-
else if (char === "{") {
|
|
133
|
-
braceCount++;
|
|
134
|
-
}
|
|
135
|
-
else if (char === "}") {
|
|
136
|
-
braceCount--;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Check if import is complete
|
|
311
|
+
braceCount = lineBraceCount;
|
|
312
|
+
inString = lineInString;
|
|
313
|
+
stringChar = lineStringChar;
|
|
314
|
+
// Check if import is complete on this line
|
|
141
315
|
if ((trimmed.endsWith(";") ||
|
|
142
|
-
isImportComplete(
|
|
316
|
+
isImportComplete(i, braceCount, inString)) &&
|
|
143
317
|
braceCount === 0 && !inString) {
|
|
144
318
|
imports.push(currentImport.join("\n"));
|
|
145
319
|
currentImport = [];
|
|
@@ -148,31 +322,12 @@ export function splitScript(script) {
|
|
|
148
322
|
} // Continue import statement
|
|
149
323
|
else if (inImport) {
|
|
150
324
|
currentImport.push(line);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (inString) {
|
|
155
|
-
if (char === stringChar && line[j - 1] !== "\\") {
|
|
156
|
-
inString = false;
|
|
157
|
-
stringChar = "";
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
162
|
-
inString = true;
|
|
163
|
-
stringChar = char;
|
|
164
|
-
}
|
|
165
|
-
else if (char === "{") {
|
|
166
|
-
braceCount++;
|
|
167
|
-
}
|
|
168
|
-
else if (char === "}") {
|
|
169
|
-
braceCount--;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
325
|
+
braceCount += lineBraceCount;
|
|
326
|
+
inString = lineInString;
|
|
327
|
+
stringChar = lineStringChar;
|
|
173
328
|
// Check if import is complete
|
|
174
329
|
if ((trimmed.endsWith(";") ||
|
|
175
|
-
isImportComplete(
|
|
330
|
+
isImportComplete(i, braceCount, inString)) &&
|
|
176
331
|
braceCount === 0 && !inString) {
|
|
177
332
|
imports.push(currentImport.join("\n"));
|
|
178
333
|
currentImport = [];
|
|
@@ -181,11 +336,21 @@ export function splitScript(script) {
|
|
|
181
336
|
} // Regular body content
|
|
182
337
|
else {
|
|
183
338
|
body.push(line);
|
|
339
|
+
// Update global string state
|
|
340
|
+
inString = lineInString;
|
|
341
|
+
stringChar = lineStringChar;
|
|
184
342
|
}
|
|
185
343
|
}
|
|
186
|
-
// Handle unterminated import (likely malformed)
|
|
344
|
+
// Handle unterminated import (likely malformed or still in string)
|
|
187
345
|
if (currentImport.length > 0) {
|
|
188
|
-
|
|
346
|
+
if (inImport && !inString) {
|
|
347
|
+
// Import seems complete but wasn't captured properly
|
|
348
|
+
imports.push(currentImport.join("\n"));
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Still in string or incomplete, treat as body
|
|
352
|
+
body.push(...currentImport);
|
|
353
|
+
}
|
|
189
354
|
}
|
|
190
355
|
return {
|
|
191
356
|
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.351",
|
|
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.351",
|
|
746
|
+
"resolved": "https://registry.npmjs.org/@thyn/core/-/core-0.0.351.tgz",
|
|
747
|
+
"integrity": "sha512-67M0/0wrdz1mNIMZ9V7Nq1jEO1C1iLdoG9rnjFcDGlDQRFM/HlNizvVWOaq2RfapVpgtRqIsKAA5iBloLE2GIA==",
|
|
748
748
|
"dev": true,
|
|
749
749
|
"license": "MIT",
|
|
750
750
|
"dependencies": {
|
package/docs/package.json
CHANGED
package/docs/src/pages/Home.thyn
CHANGED
|
@@ -17,47 +17,49 @@
|
|
|
17
17
|
setTimeout(() => {
|
|
18
18
|
Prism.highlightAll();
|
|
19
19
|
});
|
|
20
|
-
</script>
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<img src="/thyn.svg" />
|
|
25
|
-
thyn
|
|
26
|
-
</h1>
|
|
27
|
-
<p class="tagline">{{ taglines[tagline() % taglines.length] }}</p>
|
|
28
|
-
<div class="compiled-wrapper">
|
|
29
|
-
<div>
|
|
30
|
-
<h4>source</h4>
|
|
31
|
-
<pre>
|
|
32
|
-
<code class="code language-javascript">
|
|
33
|
-
// App.thyn
|
|
34
|
-
<script>
|
|
21
|
+
const codeSnippet = `// App.thyn
|
|
22
|
+
<script>
|
|
35
23
|
const count = $signal(0);
|
|
36
|
-
|
|
24
|
+
</script>
|
|
37
25
|
|
|
38
|
-
|
|
26
|
+
<button onclick={() => count(c => c + 1)}>
|
|
39
27
|
Count: \{{ count() \}}
|
|
40
|
-
|
|
28
|
+
</button>
|
|
41
29
|
|
|
42
|
-
|
|
30
|
+
<style>
|
|
43
31
|
button {
|
|
44
32
|
background: #333;
|
|
45
33
|
}
|
|
46
|
-
|
|
34
|
+
</style>
|
|
47
35
|
|
|
48
36
|
// main.js
|
|
49
37
|
import { mount } from '@thyn/core';
|
|
50
38
|
import App from './App.thyn';
|
|
51
39
|
|
|
52
|
-
mount(App, document.body)
|
|
53
|
-
|
|
40
|
+
mount(App, document.body);`;
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div class="main">
|
|
44
|
+
<h1>
|
|
45
|
+
<img src="/thyn.svg" />
|
|
46
|
+
thyn
|
|
47
|
+
</h1>
|
|
48
|
+
<p class="tagline">{{ taglines[tagline() % taglines.length] }}</p>
|
|
49
|
+
<div class="compiled-wrapper">
|
|
50
|
+
<div>
|
|
51
|
+
<h4>source</h4>
|
|
52
|
+
<pre>
|
|
53
|
+
<code class="code language-javascript">
|
|
54
|
+
{{ codeSnippet }}
|
|
55
|
+
</code>
|
|
54
56
|
</pre>
|
|
55
57
|
</div>
|
|
56
58
|
<div>
|
|
57
59
|
<h4>compiled</h4>
|
|
58
60
|
<pre>
|
|
59
61
|
<code class="compiled language-javascript">
|
|
60
|
-
let u,s;const r=[];function l(t){r.push(t),s||(s=!0,queueMicrotask(()
|
|
62
|
+
let u,s;const r=[];function l(t){r.push(t),s||(s=!0,queueMicrotask(()=>{for(const n of r)f(n);r.length=0,s=!1}))}function p(t){const n=new Set;return(...e)=>{if(!e.length)return u&&(n.add(u),u.deps.add(n)),t;const o=e[0],i=typeof o=="function"?o(t):o;if(i!==t){t=i;for(const d of n)l(d)}}}function f(t,n){n||a(t);const e=u;u=t;const o=t.run();o&&(t.td?t.td.push(o):t.td=[o]),u=e}function _(t,n){const e={run:t,deps:new Set,show:n};return f(e,!0),e}function a(t){const{deps:n,td:e}=t;if(n.size){for(const o of n)o.delete(t);n.clear()}if(e){for(const o of e)o();t.td=null}}function h(t,n){n.appendChild(t())}let c;function m(){if(!c){c=document.createElement("button"),c.className="thyn-0";const t=document.createTextNode("");return c.appendChild(t),c}return c.cloneNode(!0)}function N(t){const n=p(0),e=m();return e.onclick=()=>n(o=>o+1),_(()=>{e.firstChild.nodeValue=`Count: ${n()}`}),e}h(N,document.body);
|
|
61
63
|
</code>
|
|
62
64
|
</pre>
|
|
63
65
|
</div>
|
package/package.json
CHANGED
package/src/plugin/index.ts
CHANGED
|
@@ -906,7 +906,7 @@ async function transformHTMLtoJSX(html: string, style: string) {
|
|
|
906
906
|
const rootElement = template.content.firstElementChild;
|
|
907
907
|
|
|
908
908
|
let scopedStyle = null;
|
|
909
|
-
if (style) {
|
|
909
|
+
if (style && rootElement) {
|
|
910
910
|
addScopeId(rootElement, scopeId);
|
|
911
911
|
scopedStyle = await scopeSelectors(style, scopeId);
|
|
912
912
|
}
|
package/src/plugin/utils.ts
CHANGED
|
@@ -1,23 +1,163 @@
|
|
|
1
1
|
export function extractParts(code: string) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
// Helper to check if a position is inside a string literal or comment
|
|
3
|
+
function isInsideStringOrComment(code: string, pos: number): boolean {
|
|
4
|
+
let inString = false;
|
|
5
|
+
let stringChar = '';
|
|
6
|
+
let escaped = false;
|
|
7
|
+
let inLineComment = false;
|
|
8
|
+
let inBlockComment = false;
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < pos; i++) {
|
|
11
|
+
const char = code[i];
|
|
12
|
+
const nextChar = code[i + 1];
|
|
13
|
+
|
|
14
|
+
if (inLineComment) {
|
|
15
|
+
if (char === '\n') {
|
|
16
|
+
inLineComment = false;
|
|
17
|
+
}
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (inBlockComment) {
|
|
22
|
+
if (char === '*' && nextChar === '/') {
|
|
23
|
+
inBlockComment = false;
|
|
24
|
+
i++; // skip the '/'
|
|
25
|
+
}
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (escaped) {
|
|
30
|
+
escaped = false;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (char === '\\') {
|
|
35
|
+
escaped = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check for comment start
|
|
40
|
+
if (char === '/' && nextChar === '/') {
|
|
41
|
+
inLineComment = true;
|
|
42
|
+
i++; // skip the second '/'
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (char === '/' && nextChar === '*') {
|
|
47
|
+
inBlockComment = true;
|
|
48
|
+
i++; // skip the '*'
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
53
|
+
inString = true;
|
|
54
|
+
stringChar = char;
|
|
55
|
+
} else if (inString && char === stringChar) {
|
|
56
|
+
inString = false;
|
|
57
|
+
stringChar = '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return inString || inLineComment || inBlockComment;
|
|
62
|
+
}
|
|
63
|
+
|
|
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 {
|
|
66
|
+
const openRegex = /<script([^>]*)>/gi;
|
|
67
|
+
let openMatch;
|
|
68
|
+
|
|
69
|
+
while ((openMatch = openRegex.exec(code)) !== null) {
|
|
70
|
+
if (!isInsideStringOrComment(code, openMatch.index)) {
|
|
71
|
+
const contentStart = openMatch.index + openMatch[0].length;
|
|
72
|
+
|
|
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
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Find first real <style> tag (not inside a string)
|
|
93
|
+
function findStyleSection(code: string): { start: number; contentStart: number; contentEnd: number; end: number } | null {
|
|
94
|
+
const openRegex = /<style[^>]*>/gi;
|
|
95
|
+
let openMatch;
|
|
96
|
+
|
|
97
|
+
while ((openMatch = openRegex.exec(code)) !== null) {
|
|
98
|
+
if (!isInsideStringOrComment(code, openMatch.index)) {
|
|
99
|
+
const contentStart = openMatch.index + openMatch[0].length;
|
|
100
|
+
|
|
101
|
+
// Find the first </style> that is not inside a string
|
|
102
|
+
const closeRegex = /<\/style>/gi;
|
|
103
|
+
let closeMatch;
|
|
104
|
+
while ((closeMatch = closeRegex.exec(code)) !== null) {
|
|
105
|
+
if (closeMatch.index >= contentStart && !isInsideStringOrComment(code, closeMatch.index)) {
|
|
106
|
+
return {
|
|
107
|
+
start: openMatch.index,
|
|
108
|
+
contentStart: contentStart,
|
|
109
|
+
contentEnd: closeMatch.index,
|
|
110
|
+
end: closeMatch.index + closeMatch[0].length
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
8
118
|
|
|
119
|
+
// Extract sections
|
|
120
|
+
const scriptSection = findScriptSection(code);
|
|
121
|
+
const styleSection = findStyleSection(code);
|
|
122
|
+
|
|
123
|
+
let script = "";
|
|
9
124
|
let scriptLang = "js";
|
|
10
|
-
|
|
11
|
-
|
|
125
|
+
|
|
126
|
+
if (scriptSection) {
|
|
127
|
+
script = code.slice(scriptSection.contentStart, scriptSection.contentEnd).trim();
|
|
128
|
+
const langMatch = scriptSection.attrs.match(/lang\s*=\s*["']([^"']+)["']/);
|
|
12
129
|
if (langMatch) {
|
|
13
130
|
scriptLang = langMatch[1];
|
|
14
131
|
}
|
|
15
132
|
}
|
|
16
133
|
|
|
134
|
+
let style = "";
|
|
135
|
+
if (styleSection) {
|
|
136
|
+
style = code.slice(styleSection.contentStart, styleSection.contentEnd).trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Build HTML by removing script and style sections
|
|
140
|
+
// Remove from highest index to lowest to preserve indices
|
|
141
|
+
let html = code;
|
|
142
|
+
const sections = [];
|
|
143
|
+
if (scriptSection) {
|
|
144
|
+
sections.push({ start: scriptSection.start, end: scriptSection.end });
|
|
145
|
+
}
|
|
146
|
+
if (styleSection) {
|
|
147
|
+
sections.push({ start: styleSection.start, end: styleSection.end });
|
|
148
|
+
}
|
|
149
|
+
// Sort by start position descending (remove from end first)
|
|
150
|
+
sections.sort((a, b) => b.start - a.start);
|
|
151
|
+
|
|
152
|
+
for (const section of sections) {
|
|
153
|
+
html = html.slice(0, section.start) + html.slice(section.end);
|
|
154
|
+
}
|
|
155
|
+
html = html.trim();
|
|
156
|
+
|
|
17
157
|
return {
|
|
18
|
-
script
|
|
158
|
+
script,
|
|
19
159
|
scriptLang,
|
|
20
|
-
style
|
|
160
|
+
style,
|
|
21
161
|
html,
|
|
22
162
|
};
|
|
23
163
|
};
|
|
@@ -46,20 +186,20 @@ export function splitScript(script: string) {
|
|
|
46
186
|
const lines = script.split("\n");
|
|
47
187
|
const imports = [];
|
|
48
188
|
const body = [];
|
|
49
|
-
let currentImport = [];
|
|
189
|
+
let currentImport: string[] = [];
|
|
50
190
|
let inImport = false;
|
|
51
191
|
let braceCount = 0;
|
|
52
192
|
let inString = false;
|
|
53
193
|
let stringChar = "";
|
|
54
194
|
let inMultiLineComment = false;
|
|
195
|
+
let escaped = false;
|
|
55
196
|
|
|
56
197
|
// Helper function to check if import is complete without semicolon
|
|
57
|
-
function isImportComplete(
|
|
198
|
+
function isImportComplete(lineIndex: number, braceCount: number, inString: boolean): boolean {
|
|
58
199
|
// If we have balanced braces and not in a string, check if next non-empty line starts a new statement
|
|
59
200
|
if (braceCount === 0 && !inString) {
|
|
60
201
|
// Look ahead to see if next line starts a new statement/declaration
|
|
61
|
-
|
|
62
|
-
for (let i = nextLineIndex; i < lines.length; i++) {
|
|
202
|
+
for (let i = lineIndex + 1; i < lines.length; i++) {
|
|
63
203
|
const nextLine = lines[i].trim();
|
|
64
204
|
if (
|
|
65
205
|
!nextLine || nextLine.startsWith("//") || nextLine.startsWith("/*")
|
|
@@ -76,6 +216,7 @@ export function splitScript(script: string) {
|
|
|
76
216
|
return false;
|
|
77
217
|
}
|
|
78
218
|
|
|
219
|
+
// Process each line, maintaining string/comment state
|
|
79
220
|
for (let i = 0; i < lines.length; i++) {
|
|
80
221
|
const line = lines[i];
|
|
81
222
|
const trimmed = line.trim();
|
|
@@ -88,14 +229,33 @@ export function splitScript(script: string) {
|
|
|
88
229
|
body.push(line);
|
|
89
230
|
}
|
|
90
231
|
|
|
91
|
-
|
|
92
|
-
|
|
232
|
+
// Check for end of multi-line comment, tracking strings
|
|
233
|
+
for (let j = 0; j < line.length; j++) {
|
|
234
|
+
const char = line[j];
|
|
235
|
+
if (escaped) {
|
|
236
|
+
escaped = false;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (char === '\\' && inString) {
|
|
240
|
+
escaped = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
244
|
+
inString = true;
|
|
245
|
+
stringChar = char;
|
|
246
|
+
} else if (inString && char === stringChar) {
|
|
247
|
+
inString = false;
|
|
248
|
+
stringChar = "";
|
|
249
|
+
} else if (!inString && char === '*' && line[j + 1] === '/') {
|
|
250
|
+
inMultiLineComment = false;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
93
253
|
}
|
|
94
254
|
continue;
|
|
95
255
|
}
|
|
96
256
|
|
|
97
|
-
// Check for start of multi-line comment
|
|
98
|
-
if (line.includes("/*")
|
|
257
|
+
// Check for start of multi-line comment (only if not in string)
|
|
258
|
+
if (!inString && line.includes("/*")) {
|
|
99
259
|
inMultiLineComment = true;
|
|
100
260
|
if (inImport) {
|
|
101
261
|
currentImport.push(line);
|
|
@@ -103,15 +263,37 @@ export function splitScript(script: string) {
|
|
|
103
263
|
body.push(line);
|
|
104
264
|
}
|
|
105
265
|
|
|
106
|
-
if
|
|
266
|
+
// Check if comment ends on same line
|
|
267
|
+
for (let j = 0; j < line.length; j++) {
|
|
268
|
+
const char = line[j];
|
|
269
|
+
if (escaped) {
|
|
270
|
+
escaped = false;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (char === '\\' && inString) {
|
|
274
|
+
escaped = true;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
278
|
+
inString = true;
|
|
279
|
+
stringChar = char;
|
|
280
|
+
} else if (inString && char === stringChar) {
|
|
281
|
+
inString = false;
|
|
282
|
+
stringChar = "";
|
|
283
|
+
} else if (!inString && char === '*' && line[j + 1] === '/') {
|
|
284
|
+
inMultiLineComment = false;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!inMultiLineComment) {
|
|
290
|
+
// Comment ended on same line
|
|
107
291
|
continue;
|
|
108
|
-
} else {
|
|
109
|
-
inMultiLineComment = false;
|
|
110
292
|
}
|
|
111
293
|
}
|
|
112
294
|
|
|
113
|
-
// Skip single-line comments when not in import
|
|
114
|
-
if (trimmed.startsWith("//")
|
|
295
|
+
// Skip single-line comments when not in import (only if not in string)
|
|
296
|
+
if (!inString && !inImport && trimmed.startsWith("//")) {
|
|
115
297
|
body.push(line);
|
|
116
298
|
continue;
|
|
117
299
|
}
|
|
@@ -122,38 +304,50 @@ export function splitScript(script: string) {
|
|
|
122
304
|
continue;
|
|
123
305
|
}
|
|
124
306
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
inString = false;
|
|
307
|
+
// Process the line character by character to maintain string state
|
|
308
|
+
let lineBraceCount = 0;
|
|
309
|
+
let lineInString = inString;
|
|
310
|
+
let lineStringChar = stringChar;
|
|
311
|
+
let lineEscaped = false;
|
|
131
312
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const char = line[j];
|
|
313
|
+
for (let j = 0; j < line.length; j++) {
|
|
314
|
+
const char = line[j];
|
|
135
315
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
stringChar = "";
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
143
|
-
inString = true;
|
|
144
|
-
stringChar = char;
|
|
145
|
-
} else if (char === "{") {
|
|
146
|
-
braceCount++;
|
|
147
|
-
} else if (char === "}") {
|
|
148
|
-
braceCount--;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
316
|
+
if (lineEscaped) {
|
|
317
|
+
lineEscaped = false;
|
|
318
|
+
continue;
|
|
151
319
|
}
|
|
152
320
|
|
|
153
|
-
|
|
321
|
+
if (char === '\\' && lineInString) {
|
|
322
|
+
lineEscaped = true;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!lineInString && (char === '"' || char === "'" || char === '`')) {
|
|
327
|
+
lineInString = true;
|
|
328
|
+
lineStringChar = char;
|
|
329
|
+
} else if (lineInString && char === lineStringChar) {
|
|
330
|
+
lineInString = false;
|
|
331
|
+
lineStringChar = "";
|
|
332
|
+
} else if (!lineInString && char === '{') {
|
|
333
|
+
lineBraceCount++;
|
|
334
|
+
} else if (!lineInString && char === '}') {
|
|
335
|
+
lineBraceCount--;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Start of import statement (only if not inside a string)
|
|
340
|
+
if (!inImport && !inString && trimmed.startsWith("import")) {
|
|
341
|
+
inImport = true;
|
|
342
|
+
currentImport = [line];
|
|
343
|
+
braceCount = lineBraceCount;
|
|
344
|
+
inString = lineInString;
|
|
345
|
+
stringChar = lineStringChar;
|
|
346
|
+
|
|
347
|
+
// Check if import is complete on this line
|
|
154
348
|
if (
|
|
155
349
|
(trimmed.endsWith(";") ||
|
|
156
|
-
isImportComplete(
|
|
350
|
+
isImportComplete(i, braceCount, inString)) &&
|
|
157
351
|
braceCount === 0 && !inString
|
|
158
352
|
) {
|
|
159
353
|
imports.push(currentImport.join("\n"));
|
|
@@ -163,32 +357,14 @@ export function splitScript(script: string) {
|
|
|
163
357
|
} // Continue import statement
|
|
164
358
|
else if (inImport) {
|
|
165
359
|
currentImport.push(line);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const char = line[j];
|
|
170
|
-
|
|
171
|
-
if (inString) {
|
|
172
|
-
if (char === stringChar && line[j - 1] !== "\\") {
|
|
173
|
-
inString = false;
|
|
174
|
-
stringChar = "";
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
178
|
-
inString = true;
|
|
179
|
-
stringChar = char;
|
|
180
|
-
} else if (char === "{") {
|
|
181
|
-
braceCount++;
|
|
182
|
-
} else if (char === "}") {
|
|
183
|
-
braceCount--;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
360
|
+
braceCount += lineBraceCount;
|
|
361
|
+
inString = lineInString;
|
|
362
|
+
stringChar = lineStringChar;
|
|
187
363
|
|
|
188
364
|
// Check if import is complete
|
|
189
365
|
if (
|
|
190
366
|
(trimmed.endsWith(";") ||
|
|
191
|
-
isImportComplete(
|
|
367
|
+
isImportComplete(i, braceCount, inString)) &&
|
|
192
368
|
braceCount === 0 && !inString
|
|
193
369
|
) {
|
|
194
370
|
imports.push(currentImport.join("\n"));
|
|
@@ -198,12 +374,21 @@ export function splitScript(script: string) {
|
|
|
198
374
|
} // Regular body content
|
|
199
375
|
else {
|
|
200
376
|
body.push(line);
|
|
377
|
+
// Update global string state
|
|
378
|
+
inString = lineInString;
|
|
379
|
+
stringChar = lineStringChar;
|
|
201
380
|
}
|
|
202
381
|
}
|
|
203
382
|
|
|
204
|
-
// Handle unterminated import (likely malformed)
|
|
383
|
+
// Handle unterminated import (likely malformed or still in string)
|
|
205
384
|
if (currentImport.length > 0) {
|
|
206
|
-
|
|
385
|
+
if (inImport && !inString) {
|
|
386
|
+
// Import seems complete but wasn't captured properly
|
|
387
|
+
imports.push(currentImport.join("\n"));
|
|
388
|
+
} else {
|
|
389
|
+
// Still in string or incomplete, treat as body
|
|
390
|
+
body.push(...currentImport);
|
|
391
|
+
}
|
|
207
392
|
}
|
|
208
393
|
|
|
209
394
|
return {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import CodeSnippet from "./CodeSnippet.thyn";
|
|
3
|
+
|
|
4
|
+
describe("CodeSnippet component", () => {
|
|
5
|
+
it("should render codeSnippet string containing script/style tags", () => {
|
|
6
|
+
const root = CodeSnippet();
|
|
7
|
+
const display = root.querySelector('.display');
|
|
8
|
+
|
|
9
|
+
// The component should render the codeSnippet content
|
|
10
|
+
expect(display).toBeTruthy();
|
|
11
|
+
expect(display.textContent).toContain("// App.thyn");
|
|
12
|
+
expect(display.textContent).toContain("<script>");
|
|
13
|
+
expect(display.textContent).toContain("</script>");
|
|
14
|
+
expect(display.textContent).toContain("<style>");
|
|
15
|
+
expect(display.textContent).toContain("</style>");
|
|
16
|
+
expect(display.textContent).toContain("button {");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should apply the component's actual style, not style from codeSnippet", () => {
|
|
20
|
+
const root = CodeSnippet();
|
|
21
|
+
const display = root.querySelector('.display');
|
|
22
|
+
|
|
23
|
+
// The display element should have the component's style applied
|
|
24
|
+
// (white-space: pre and font-family: monospace from actual <style> section)
|
|
25
|
+
// Note: scoped CSS adds a class like 'thyn-e', so we check it contains 'display'
|
|
26
|
+
expect(display.className).toContain('display');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
const count = $signal(0);
|
|
3
|
+
|
|
4
|
+
// This code snippet string contains <script> and <style> tags
|
|
5
|
+
// which should NOT be extracted as actual sections
|
|
6
|
+
const codeSnippet = `// App.thyn
|
|
7
|
+
<script>
|
|
8
|
+
const count = $signal(0);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<button onclick={() => count(c => c + 1)}>
|
|
12
|
+
Count: {{ count() }}
|
|
13
|
+
</button>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
button {
|
|
17
|
+
background: #333;
|
|
18
|
+
}
|
|
19
|
+
</style>`;
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div>
|
|
23
|
+
<p class="display">{{ codeSnippet }}</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.display {
|
|
28
|
+
white-space: pre;
|
|
29
|
+
font-family: monospace;
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
@@ -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>
|