@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/docs/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "devDependencies": {
12
- "@thyn/core": "^0.0.345",
12
+ "@thyn/core": "^0.0.350",
13
13
  "vite": "^6.3.5"
14
14
  }
15
15
  }
@@ -17,47 +17,49 @@
17
17
  setTimeout(() => {
18
18
  Prism.highlightAll();
19
19
  });
20
- </script>
21
20
 
22
- <div class="main">
23
- <h1>
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
- &lt;script&gt;
21
+ const codeSnippet = `// App.thyn
22
+ <script>
35
23
  const count = $signal(0);
36
- &lt;/script&gt;
24
+ </script>
37
25
 
38
- &lt;button onclick={() =&gt; count(c =&gt; c + 1)}&gt;
26
+ <button onclick={() => count(c => c + 1)}>
39
27
  Count: \{{ count() \}}
40
- &lt;/button&gt;
28
+ </button>
41
29
 
42
- &lt;style&gt;
30
+ <style>
43
31
  button {
44
32
  background: #333;
45
33
  }
46
- &lt;/style&gt;
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
- </code>
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(()=&gt;{for(const n of r)f(n);r.length=0,s=!1}))}function p(t){const n=new Set;return(...e)=&gt;{if(!e.length)return u&amp;&amp;(n.add(u),u.deps.add(n)),t;const o=e[0],i=typeof o==&quot;function&quot;?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&amp;&amp;(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(&quot;button&quot;),c.className=&quot;thyn-0&quot;;const t=document.createTextNode(&quot;&quot;);return c.appendChild(t),c}return c.cloneNode(!0)}function N(t){const n=p(0),e=m();return e.onclick=()=&gt;n(o=&gt;o+1),_(()=&gt;{e.firstChild.nodeValue=`Count: ${n()}`}),e}h(N,document.body);
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&amp;&amp;(n.add(u),u.deps.add(n)),t;const o=e[0],i=typeof o==&quot;function&quot;?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&amp;&amp;(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(&quot;button&quot;),c.className=&quot;thyn-0&quot;;const t=document.createTextNode(&quot;&quot;);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.349",
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,
@@ -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
  }
@@ -1,23 +1,163 @@
1
1
  export function extractParts(code: string) {
2
- const scriptMatch = code.match(/<script([^>]*?)>([\s\S]*?)<\/script>/);
3
- const styleMatch = code.match(/<style[^>]*>([\s\S]*?)<\/style>/);
4
- const html = code
5
- .replace(/<script[^>]*>[\s\S]*?<\/script>/, "")
6
- .replace(/<style[^>]*>[\s\S]*?<\/style>/, "")
7
- .trim();
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
- if (scriptMatch && scriptMatch[1]) {
11
- const langMatch = scriptMatch[1].match(/lang\s*=\s*["']([^"']+)["']/);
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: scriptMatch?.[2]?.trim() ?? "",
158
+ script,
19
159
  scriptLang,
20
- style: styleMatch?.[1]?.trim() ?? "",
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>