@thyn/core 0.0.352 → 0.0.354

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/README.md CHANGED
@@ -35,7 +35,7 @@ npm run dev
35
35
  </button>
36
36
 
37
37
  <style>
38
- /* Styles are scoped by default to prevent style bleeding 🎉 */
38
+ /* Styles are scoped by default to prevent style bleeding */
39
39
  button {
40
40
  background: #333;
41
41
  border: 0;
@@ -879,7 +879,7 @@ export async function transformSFC(source, id) {
879
879
  s.append([
880
880
  "",
881
881
  `export default function ${name}($props) {`,
882
- ...body.map((l) => " " + l),
882
+ ...body.map((l) => (l.shouldIndent ? " " + l.text : l.text)),
883
883
  removeUnusedThynVars(` ${transformed} return ${root};`),
884
884
  `}`,
885
885
  ].join("\n"));
@@ -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
@@ -198,7 +268,7 @@ export function splitScript(script) {
198
268
  currentImport.push(line);
199
269
  }
200
270
  else {
201
- body.push(line);
271
+ body.push({ text: line, shouldIndent: true });
202
272
  }
203
273
  // Check for end of multi-line comment, tracking strings
204
274
  for (let j = 0; j < line.length; j++) {
@@ -233,7 +303,7 @@ export function splitScript(script) {
233
303
  currentImport.push(line);
234
304
  }
235
305
  else {
236
- body.push(line);
306
+ body.push({ text: line, shouldIndent: !inString });
237
307
  }
238
308
  // Check if comment ends on same line
239
309
  for (let j = 0; j < line.length; j++) {
@@ -266,12 +336,12 @@ export function splitScript(script) {
266
336
  }
267
337
  // Skip single-line comments when not in import (only if not in string)
268
338
  if (!inString && !inImport && trimmed.startsWith("//")) {
269
- body.push(line);
339
+ body.push({ text: line, shouldIndent: true });
270
340
  continue;
271
341
  }
272
342
  // Skip empty lines when not in import
273
343
  if (!trimmed && !inImport) {
274
- body.push(line);
344
+ body.push({ text: line, shouldIndent: true });
275
345
  continue;
276
346
  }
277
347
  // Process the line character by character to maintain string state
@@ -335,7 +405,7 @@ export function splitScript(script) {
335
405
  }
336
406
  } // Regular body content
337
407
  else {
338
- body.push(line);
408
+ body.push({ text: line, shouldIndent: !inString });
339
409
  // Update global string state
340
410
  inString = lineInString;
341
411
  stringChar = lineStringChar;
@@ -349,11 +419,15 @@ export function splitScript(script) {
349
419
  }
350
420
  else {
351
421
  // Still in string or incomplete, treat as body
352
- body.push(...currentImport);
422
+ // We can't know for sure about indentation here, but if it was part of an import or string,
423
+ // it likely follows previous logic. For safety, if we are in string, don't indent.
424
+ for (const l of currentImport) {
425
+ body.push({ text: l, shouldIndent: !inString });
426
+ }
353
427
  }
354
428
  }
355
429
  return {
356
430
  imports: imports.filter((imp) => imp.trim()),
357
- body: body.length > 0 ? body : [""],
431
+ body: body.length > 0 ? body : [{ text: "", shouldIndent: true }],
358
432
  };
359
433
  }
@@ -8,7 +8,7 @@
8
8
  "name": "thyn-app",
9
9
  "version": "0.0.0",
10
10
  "devDependencies": {
11
- "@thyn/core": "^0.0.351",
11
+ "@thyn/core": "^0.0.353",
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.351",
746
- "resolved": "https://registry.npmjs.org/@thyn/core/-/core-0.0.351.tgz",
747
- "integrity": "sha512-67M0/0wrdz1mNIMZ9V7Nq1jEO1C1iLdoG9rnjFcDGlDQRFM/HlNizvVWOaq2RfapVpgtRqIsKAA5iBloLE2GIA==",
745
+ "version": "0.0.353",
746
+ "resolved": "https://registry.npmjs.org/@thyn/core/-/core-0.0.353.tgz",
747
+ "integrity": "sha512-vg8X99bz1H+cCJtwPY8chIRBiRA+R26uI5XOOqXxL+jW2qWzZd7PID0EzkYi3zbfbCX0I3u5A+kfjbBJucFYAw==",
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.351",
12
+ "@thyn/core": "^0.0.353",
13
13
  "vite": "^6.3.5"
14
14
  }
15
15
  }
@@ -49,11 +49,7 @@ mount(App, document.body);`;
49
49
  <div class="compiled-wrapper">
50
50
  <div>
51
51
  <h4>source</h4>
52
- <pre>
53
- <code class="code language-javascript">
54
- {{ codeSnippet }}
55
- </code>
56
- </pre>
52
+ <pre><code class="code language-javascript">{{ codeSnippet }}</code></pre>
57
53
  </div>
58
54
  <div>
59
55
  <h4>compiled</h4>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thyn/core",
3
- "version": "0.0.352",
3
+ "version": "0.0.354",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -977,7 +977,7 @@ export async function transformSFC(source: string, id: string) {
977
977
  s.append([
978
978
  "",
979
979
  `export default function ${name}($props) {`,
980
- ...body.map((l) => " " + l),
980
+ ...body.map((l: any) => (l.shouldIndent ? " " + l.text : l.text)),
981
981
  removeUnusedThynVars(` ${transformed} return ${root};`),
982
982
  `}`,
983
983
  ].join("\n"));
@@ -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
@@ -226,7 +304,7 @@ export function splitScript(script: string) {
226
304
  if (inImport) {
227
305
  currentImport.push(line);
228
306
  } else {
229
- body.push(line);
307
+ body.push({ text: line, shouldIndent: true });
230
308
  }
231
309
 
232
310
  // Check for end of multi-line comment, tracking strings
@@ -260,7 +338,7 @@ export function splitScript(script: string) {
260
338
  if (inImport) {
261
339
  currentImport.push(line);
262
340
  } else {
263
- body.push(line);
341
+ body.push({ text: line, shouldIndent: !inString });
264
342
  }
265
343
 
266
344
  // Check if comment ends on same line
@@ -294,13 +372,13 @@ export function splitScript(script: string) {
294
372
 
295
373
  // Skip single-line comments when not in import (only if not in string)
296
374
  if (!inString && !inImport && trimmed.startsWith("//")) {
297
- body.push(line);
375
+ body.push({ text: line, shouldIndent: true });
298
376
  continue;
299
377
  }
300
378
 
301
379
  // Skip empty lines when not in import
302
380
  if (!trimmed && !inImport) {
303
- body.push(line);
381
+ body.push({ text: line, shouldIndent: true });
304
382
  continue;
305
383
  }
306
384
 
@@ -373,7 +451,7 @@ export function splitScript(script: string) {
373
451
  }
374
452
  } // Regular body content
375
453
  else {
376
- body.push(line);
454
+ body.push({ text: line, shouldIndent: !inString });
377
455
  // Update global string state
378
456
  inString = lineInString;
379
457
  stringChar = lineStringChar;
@@ -387,12 +465,16 @@ export function splitScript(script: string) {
387
465
  imports.push(currentImport.join("\n"));
388
466
  } else {
389
467
  // Still in string or incomplete, treat as body
390
- body.push(...currentImport);
468
+ // We can't know for sure about indentation here, but if it was part of an import or string,
469
+ // it likely follows previous logic. For safety, if we are in string, don't indent.
470
+ for(const l of currentImport) {
471
+ body.push({ text: l, shouldIndent: !inString });
472
+ }
391
473
  }
392
474
  }
393
475
 
394
476
  return {
395
477
  imports: imports.filter((imp) => imp.trim()),
396
- body: body.length > 0 ? body : [""],
478
+ body: body.length > 0 ? body : [{ text: "", shouldIndent: true }],
397
479
  };
398
480
  }
@@ -16,6 +16,17 @@ describe("CodeSnippet component", () => {
16
16
  expect(display.textContent).toContain("button {");
17
17
  });
18
18
 
19
+ it("should not add extra indentation to template literals", () => {
20
+ const root = CodeSnippet();
21
+ const display = root.querySelector('.display');
22
+ const text = display.textContent;
23
+
24
+ // Check that <script> is at the start of the line (no extra indentation)
25
+ expect(text).toContain("\n<script>");
26
+ // Check that const count has exactly 2 spaces (as in source), not 4
27
+ expect(text).toContain("\n const count");
28
+ });
29
+
19
30
  it("should apply the component's actual style, not style from codeSnippet", () => {
20
31
  const root = CodeSnippet();
21
32
  const display = root.querySelector('.display');