@thyn/core 0.0.349 → 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/.github/workflows/test.yml +0 -10
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/utils.js +131 -10
- package/docs/package-lock.json +1163 -557
- package/docs/package.json +1 -1
- package/docs/src/pages/Home.thyn +24 -22
- package/package.json +4 -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/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
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thyn/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.351",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"node": "./dist/node.js",
|
|
9
9
|
"default": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./router": {
|
|
12
|
+
"default": "./dist/core/router.js"
|
|
10
13
|
}
|
|
11
14
|
},
|
|
12
15
|
"sideEffects": false,
|
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>
|