@thyn/core 0.0.350 → 0.0.351
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 +131 -10
- 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 +150 -10
- package/tests/CodeSnippet.test.ts +28 -0
- package/tests/CodeSnippet.thyn +31 -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
|
}
|
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.350",
|
|
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.350",
|
|
746
|
+
"resolved": "https://registry.npmjs.org/@thyn/core/-/core-0.0.350.tgz",
|
|
747
|
+
"integrity": "sha512-SZTB1jjjsXEkZg1BlH7s4Lhe8kWN/74ufNlNMnI86x+9tiyc0yvMLh+GFhe7VkIS//NrEwdgjR3ktpJlGeMEfA==",
|
|
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
|
+
}
|
|
118
|
+
|
|
119
|
+
// Extract sections
|
|
120
|
+
const scriptSection = findScriptSection(code);
|
|
121
|
+
const styleSection = findStyleSection(code);
|
|
8
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
|
};
|
|
@@ -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>
|