thyscript 1.0.0

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.
Files changed (49) hide show
  1. package/bin/thy-node.js +332 -0
  2. package/bin/thy.js +828 -0
  3. package/bin/thys.js +25 -0
  4. package/examples/calculator.thy +274 -0
  5. package/examples/calculator.thy.js +628 -0
  6. package/examples/hello.thy +34 -0
  7. package/examples/login.thy +382 -0
  8. package/examples/login.thy.js +710 -0
  9. package/package.json +54 -0
  10. package/scripts/_class.js +18 -0
  11. package/scripts/_class2.js +11 -0
  12. package/scripts/_class3.js +21 -0
  13. package/scripts/_dstruct.js +20 -0
  14. package/scripts/_dstruct2.js +24 -0
  15. package/scripts/_scaffold.js +16 -0
  16. package/scripts/_scaffold2.js +16 -0
  17. package/scripts/_scaffold3.js +26 -0
  18. package/scripts/_tpl_compile.js +13 -0
  19. package/scripts/_tpl_test.js +9 -0
  20. package/scripts/_typeof.js +26 -0
  21. package/scripts/_typeof2.js +25 -0
  22. package/scripts/patch_genmethod.js +44 -0
  23. package/scripts/smoke_test.js +167 -0
  24. package/scripts/smoke_test2.js +151 -0
  25. package/scripts/test_control_panel.js +58 -0
  26. package/src/compiler/CodeGenerator.js +1449 -0
  27. package/src/compiler/JITCompiler.js +325 -0
  28. package/src/database/DatabaseEngine.js +931 -0
  29. package/src/errors/ErrorDetector.js +305 -0
  30. package/src/errors/ErrorMessages.js +497 -0
  31. package/src/index.js +169 -0
  32. package/src/lexer/Lexer.js +342 -0
  33. package/src/lexer/TokenTypes.js +1234 -0
  34. package/src/memory/MemoryManager.js +283 -0
  35. package/src/parser/AST.js +606 -0
  36. package/src/parser/Parser.js +1942 -0
  37. package/src/runtime/ThyRuntime.js +265 -0
  38. package/src/runtime/stdlib/ArrayLib.js +207 -0
  39. package/src/runtime/stdlib/FileLib.js +168 -0
  40. package/src/runtime/stdlib/MathLib.js +411 -0
  41. package/src/runtime/stdlib/StringLib.js +387 -0
  42. package/src/server/ControlPanel.js +1188 -0
  43. package/src/server/HTMLCSSEngine.js +866 -0
  44. package/src/server/SecurityEngine.js +714 -0
  45. package/src/server/ServerEngine.js +738 -0
  46. package/tests/test1_math.thy +82 -0
  47. package/tests/test2_oop.thy +133 -0
  48. package/tests/test3_server.thy +143 -0
  49. package/tests/test4_functions.thy +159 -0
@@ -0,0 +1,866 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ThyScript HTMLCSSEngine
5
+ * HTML/CSS generation, templating, minification, SSR, and web component tooling.
6
+ */
7
+ class HTMLCSSEngine {
8
+ constructor() {
9
+ // Vendor prefix map: CSS property -> prefixes needed
10
+ this._vendorPrefixMap = {
11
+ 'animation': ['-webkit-animation', 'animation'],
12
+ 'animation-name': ['-webkit-animation-name', 'animation-name'],
13
+ 'animation-duration': ['-webkit-animation-duration', 'animation-duration'],
14
+ 'animation-delay': ['-webkit-animation-delay', 'animation-delay'],
15
+ 'animation-iteration-count': ['-webkit-animation-iteration-count', 'animation-iteration-count'],
16
+ 'animation-timing-function': ['-webkit-animation-timing-function', 'animation-timing-function'],
17
+ 'transition': ['-webkit-transition', '-moz-transition', '-o-transition', 'transition'],
18
+ 'transform': ['-webkit-transform', '-moz-transform', '-ms-transform', 'transform'],
19
+ 'transform-origin': ['-webkit-transform-origin', '-moz-transform-origin', 'transform-origin'],
20
+ 'box-shadow': ['-webkit-box-shadow', 'box-shadow'],
21
+ 'border-radius': ['-webkit-border-radius', 'border-radius'],
22
+ 'user-select': ['-webkit-user-select', '-moz-user-select', '-ms-user-select', 'user-select'],
23
+ 'appearance': ['-webkit-appearance', '-moz-appearance', 'appearance'],
24
+ 'flex': ['-webkit-flex', 'flex'],
25
+ 'flex-direction': ['-webkit-flex-direction', 'flex-direction'],
26
+ 'flex-wrap': ['-webkit-flex-wrap', 'flex-wrap'],
27
+ 'align-items': ['-webkit-align-items', 'align-items'],
28
+ 'justify-content': ['-webkit-justify-content', 'justify-content'],
29
+ 'backdrop-filter': ['-webkit-backdrop-filter', 'backdrop-filter'],
30
+ 'clip-path': ['-webkit-clip-path', 'clip-path'],
31
+ };
32
+
33
+ // ThyScript CSS syntax keywords
34
+ this._thyCSSSyntax = {
35
+ 'center': 'display: flex; align-items: center; justify-content: center;',
36
+ 'flex-row': 'display: flex; flex-direction: row;',
37
+ 'flex-col': 'display: flex; flex-direction: column;',
38
+ 'full': 'width: 100%; height: 100%;',
39
+ 'full-w': 'width: 100%;',
40
+ 'full-h': 'height: 100%;',
41
+ 'hidden': 'display: none;',
42
+ 'absolute-fill': 'position: absolute; top: 0; left: 0; right: 0; bottom: 0;',
43
+ 'fixed-fill': 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999;',
44
+ 'ellipsis': 'overflow: hidden; white-space: nowrap; text-overflow: ellipsis;',
45
+ 'no-select': 'user-select: none; -webkit-user-select: none;',
46
+ 'smooth': 'transition: all 0.3s ease;',
47
+ 'shadow': 'box-shadow: 0 2px 8px rgba(0,0,0,0.12);',
48
+ 'shadow-lg': 'box-shadow: 0 8px 32px rgba(0,0,0,0.2);',
49
+ 'rounded': 'border-radius: 8px;',
50
+ 'rounded-full': 'border-radius: 9999px;',
51
+ 'card': 'background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 24px;',
52
+ 'glass': 'background: rgba(255,255,255,0.15); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.2);',
53
+ };
54
+
55
+ // In-memory component registry for reactive rendering
56
+ this._components = new Map();
57
+ this._componentStates = new Map();
58
+ }
59
+
60
+ // ─────────────────────────────────────────────
61
+ // Template Rendering
62
+ // ─────────────────────────────────────────────
63
+
64
+ /**
65
+ * Renders an HTML template string with Mustache-like {{ var }} interpolation.
66
+ * Supports: {{ var }}, {{ var | default }}, {{# if cond }}...{{/ if }},
67
+ * {{# each items }}...{{/ each }}, {{! comment }}
68
+ * @param {string} htmlString
69
+ * @param {object} context
70
+ * @returns {string}
71
+ */
72
+ render(htmlString, context = {}) {
73
+ if (typeof htmlString !== 'string') throw new Error('htmlString must be a string');
74
+
75
+ let result = htmlString;
76
+
77
+ // Strip comments: {{! ... }}
78
+ result = result.replace(/\{\{![\s\S]*?\}\}/g, '');
79
+
80
+ // Process {{# each items }}...{{/ each }}
81
+ result = result.replace(/\{\{#\s*each\s+(\w[\w.]*)\s*\}\}([\s\S]*?)\{\{\/\s*each\s*\}\}/g, (_, key, template) => {
82
+ const items = this._resolve(context, key);
83
+ if (!Array.isArray(items)) return '';
84
+ return items.map((item, index) => {
85
+ const itemCtx = typeof item === 'object' ? { ...context, ...item, $index: index, $first: index === 0, $last: index === items.length - 1 } : { ...context, this: item, $index: index };
86
+ return this.render(template, itemCtx);
87
+ }).join('');
88
+ });
89
+
90
+ // Process {{# if condition }}...{{/ if }} (with optional {{else}})
91
+ result = result.replace(/\{\{#\s*if\s+([\w.!]+)\s*\}\}([\s\S]*?)\{\{\/\s*if\s*\}\}/g, (_, cond, content) => {
92
+ const negated = cond.startsWith('!');
93
+ const key = negated ? cond.slice(1) : cond;
94
+ let val = this._resolve(context, key);
95
+ if (negated) val = !val;
96
+ const [trueBlock, falseBlock = ''] = content.split(/\{\{else\}\}/);
97
+ return val ? trueBlock : falseBlock;
98
+ });
99
+
100
+ // Process {{ var | defaultValue }} and {{ var }}
101
+ result = result.replace(/\{\{\s*([\w.$]+)\s*(?:\|\s*(.*?))?\s*\}\}/g, (_, key, defaultVal) => {
102
+ const val = this._resolve(context, key);
103
+ if (val === undefined || val === null) return defaultVal !== undefined ? defaultVal.trim() : '';
104
+ return this._escapeHTML(String(val));
105
+ });
106
+
107
+ return result;
108
+ }
109
+
110
+ _resolve(context, keyPath) {
111
+ const parts = keyPath.split('.');
112
+ let val = context;
113
+ for (const part of parts) {
114
+ if (val === undefined || val === null) return undefined;
115
+ val = val[part];
116
+ }
117
+ return val;
118
+ }
119
+
120
+ _escapeHTML(str) {
121
+ return str
122
+ .replace(/&/g, '&')
123
+ .replace(/</g, '&lt;')
124
+ .replace(/>/g, '&gt;')
125
+ .replace(/"/g, '&quot;')
126
+ .replace(/'/g, '&#x27;');
127
+ }
128
+
129
+ // ─────────────────────────────────────────────
130
+ // Full Page Generation
131
+ // ─────────────────────────────────────────────
132
+
133
+ /**
134
+ * Generates a complete HTML page from a ThyScript config object.
135
+ * @param {object} config
136
+ * @param {string} [config.title]
137
+ * @param {string} [config.description]
138
+ * @param {string} [config.charset]
139
+ * @param {string} [config.lang]
140
+ * @param {string[]} [config.cssLinks] - External CSS URLs
141
+ * @param {string[]} [config.scriptSrcs] - External script URLs
142
+ * @param {object} [config.meta] - Extra meta tags { name: content }
143
+ * @param {object} [config.ogMeta] - OpenGraph meta { property: content }
144
+ * @param {string} [config.inlineCSS]
145
+ * @param {string} [config.inlineScript]
146
+ * @param {string} [config.body]
147
+ * @param {string} [config.favicon]
148
+ * @param {string} [config.theme] - 'light' | 'dark' | 'auto'
149
+ * @param {boolean} [config.responsive]
150
+ * @returns {string}
151
+ */
152
+ generatePage(config = {}) {
153
+ const {
154
+ title = 'ThyScript App',
155
+ description = '',
156
+ charset = 'UTF-8',
157
+ lang = 'en',
158
+ cssLinks = [],
159
+ scriptSrcs = [],
160
+ meta = {},
161
+ ogMeta = {},
162
+ inlineCSS = '',
163
+ inlineScript = '',
164
+ body = '',
165
+ favicon = '',
166
+ theme = 'auto',
167
+ responsive = true,
168
+ } = config;
169
+
170
+ const metaTags = Object.entries(meta)
171
+ .map(([name, content]) => ` <meta name="${name}" content="${this._escapeHTML(content)}">`)
172
+ .join('\n');
173
+
174
+ const ogTags = Object.entries(ogMeta)
175
+ .map(([prop, content]) => ` <meta property="${prop}" content="${this._escapeHTML(content)}">`)
176
+ .join('\n');
177
+
178
+ const cssLinkTags = cssLinks.map((href) => ` <link rel="stylesheet" href="${href}">`).join('\n');
179
+ const scriptTags = scriptSrcs.map((src) => ` <script src="${src}" defer></script>`).join('\n');
180
+ const faviconTag = favicon ? ` <link rel="icon" href="${favicon}">` : '';
181
+ const responsiveTag = responsive ? ` <meta name="viewport" content="width=device-width, initial-scale=1.0">` : '';
182
+
183
+ const themeCSS = this._generateThemeCSS(theme);
184
+
185
+ return `<!DOCTYPE html>
186
+ <html lang="${lang}">
187
+ <head>
188
+ <meta charset="${charset}">
189
+ ${responsiveTag}
190
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
191
+ <title>${this._escapeHTML(title)}</title>
192
+ <meta name="description" content="${this._escapeHTML(description)}">
193
+ ${metaTags}
194
+ ${ogTags}
195
+ ${faviconTag}
196
+ ${cssLinkTags}
197
+ ${scriptTags}
198
+ <style>
199
+ ${themeCSS}
200
+ ${inlineCSS}
201
+ </style>
202
+ </head>
203
+ <body>
204
+ ${body}
205
+ ${inlineScript ? `<script>\n${inlineScript}\n</script>` : ''}
206
+ </body>
207
+ </html>`;
208
+ }
209
+
210
+ // ─────────────────────────────────────────────
211
+ // CSS Generation
212
+ // ─────────────────────────────────────────────
213
+
214
+ /**
215
+ * Generates CSS from a JS object (CSS-in-JS).
216
+ * Supports nested selectors, media queries, keyframes, pseudo-classes.
217
+ * @param {object} styles - { selector: { property: value } | nested object }
218
+ * @param {string} [indent='']
219
+ * @returns {string}
220
+ */
221
+ generateCSS(styles, indent = '') {
222
+ if (typeof styles !== 'object' || !styles) return '';
223
+ const parts = [];
224
+
225
+ for (const [selector, rules] of Object.entries(styles)) {
226
+ if (typeof rules !== 'object') continue;
227
+
228
+ // Check if rules contain nested objects (nested selectors) or are all primitives
229
+ const primitiveRules = {};
230
+ const nestedRules = {};
231
+
232
+ for (const [key, val] of Object.entries(rules)) {
233
+ if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
234
+ nestedRules[key] = val;
235
+ } else {
236
+ primitiveRules[key] = val;
237
+ }
238
+ }
239
+
240
+ // Emit the current selector's properties
241
+ if (Object.keys(primitiveRules).length > 0) {
242
+ const props = this._renderCSSProperties(primitiveRules, indent + ' ');
243
+ parts.push(`${indent}${selector} {\n${props}\n${indent}}`);
244
+ }
245
+
246
+ // Emit nested rules
247
+ for (const [nestedSel, nestedRules2] of Object.entries(nestedRules)) {
248
+ // @media / @keyframes etc.
249
+ if (nestedSel.startsWith('@')) {
250
+ const inner = this.generateCSS({ [selector]: nestedRules2 }, indent + ' ');
251
+ parts.push(`${indent}${nestedSel} {\n${inner}\n${indent}}`);
252
+ } else {
253
+ // Nested selector: combine with parent
254
+ const combined = nestedSel.includes('&')
255
+ ? nestedSel.replace(/&/g, selector)
256
+ : `${selector} ${nestedSel}`;
257
+ const inner = this.generateCSS({ [combined]: nestedRules2 }, indent);
258
+ parts.push(inner);
259
+ }
260
+ }
261
+ }
262
+
263
+ return parts.join('\n');
264
+ }
265
+
266
+ _renderCSSProperties(props, indent) {
267
+ return Object.entries(props)
268
+ .map(([k, v]) => {
269
+ const cssKey = k.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`);
270
+ const cssVal = Array.isArray(v) ? v.join(', ') : v;
271
+ return `${indent}${cssKey}: ${cssVal};`;
272
+ })
273
+ .join('\n');
274
+ }
275
+
276
+ // ─────────────────────────────────────────────
277
+ // Web Component Generator
278
+ // ─────────────────────────────────────────────
279
+
280
+ /**
281
+ * Generates a Custom Element (Web Component) definition.
282
+ * @param {string} name - Tag name (must contain a hyphen).
283
+ * @param {string} template - HTML template string.
284
+ * @param {object} styles - CSS-in-JS styles object.
285
+ * @param {string} [script=''] - Additional JS for the component class body.
286
+ * @returns {string} JS string defining the web component.
287
+ */
288
+ generateComponent(name, template, styles, script = '') {
289
+ if (!name.includes('-')) throw new Error('Web component name must contain a hyphen');
290
+ const css = this.generateCSS(styles);
291
+ const escapedTemplate = template.replace(/`/g, '\\`').replace(/\$/g, '\\$');
292
+ const escapedCSS = css.replace(/`/g, '\\`').replace(/\$/g, '\\$');
293
+
294
+ return `// ThyScript Web Component: <${name}>
295
+ (function() {
296
+ 'use strict';
297
+
298
+ const template = document.createElement('template');
299
+ template.innerHTML = \`
300
+ <style>
301
+ :host {
302
+ display: block;
303
+ box-sizing: border-box;
304
+ }
305
+ ${escapedCSS}
306
+ </style>
307
+ ${escapedTemplate}
308
+ \`;
309
+
310
+ class ${this._toPascalCase(name)} extends HTMLElement {
311
+ static get observedAttributes() {
312
+ return Array.from(this._observedAttrs || []);
313
+ }
314
+
315
+ constructor() {
316
+ super();
317
+ this._shadow = this.attachShadow({ mode: 'open' });
318
+ this._shadow.appendChild(template.content.cloneNode(true));
319
+ this._state = {};
320
+ this._init();
321
+ }
322
+
323
+ _init() {
324
+ ${script}
325
+ }
326
+
327
+ connectedCallback() {
328
+ if (typeof this.onMount === 'function') this.onMount();
329
+ }
330
+
331
+ disconnectedCallback() {
332
+ if (typeof this.onDestroy === 'function') this.onDestroy();
333
+ }
334
+
335
+ attributeChangedCallback(name, oldVal, newVal) {
336
+ if (typeof this.onAttributeChange === 'function') {
337
+ this.onAttributeChange(name, oldVal, newVal);
338
+ }
339
+ }
340
+
341
+ setState(newState) {
342
+ this._state = Object.assign({}, this._state, newState);
343
+ if (typeof this.render === 'function') this.render();
344
+ }
345
+
346
+ getState() { return { ...this._state }; }
347
+
348
+ $(selector) { return this._shadow.querySelector(selector); }
349
+ $$(selector) { return this._shadow.querySelectorAll(selector); }
350
+ }
351
+
352
+ customElements.define('${name}', ${this._toPascalCase(name)});
353
+ })();
354
+ `;
355
+ }
356
+
357
+ _toPascalCase(str) {
358
+ return str.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('');
359
+ }
360
+
361
+ // ─────────────────────────────────────────────
362
+ // Minification
363
+ // ─────────────────────────────────────────────
364
+
365
+ /**
366
+ * Minifies an HTML string.
367
+ * @param {string} html
368
+ * @returns {string}
369
+ */
370
+ minifyHTML(html) {
371
+ if (typeof html !== 'string') return html;
372
+ return html
373
+ // Remove HTML comments (not conditional comments)
374
+ .replace(/<!--(?![\s\S]*?\[if)[\s\S]*?-->/g, '')
375
+ // Collapse whitespace between tags
376
+ .replace(/>\s+</g, '><')
377
+ // Trim whitespace inside tags
378
+ .replace(/\s{2,}/g, ' ')
379
+ // Remove whitespace around block-level tags
380
+ .replace(/\s*(<\/?(html|head|body|div|section|article|header|footer|nav|main|aside|ul|ol|li|table|thead|tbody|tr|th|td|form|fieldset|p|h[1-6]|blockquote|pre)[^>]*>)\s*/gi, '$1')
381
+ .trim();
382
+ }
383
+
384
+ /**
385
+ * Minifies a CSS string.
386
+ * @param {string} css
387
+ * @returns {string}
388
+ */
389
+ minifyCSS(css) {
390
+ if (typeof css !== 'string') return css;
391
+ return css
392
+ // Remove comments
393
+ .replace(/\/\*[\s\S]*?\*\//g, '')
394
+ // Remove whitespace
395
+ .replace(/\s+/g, ' ')
396
+ // Remove spaces around special chars
397
+ .replace(/\s*([:;,{}])\s*/g, '$1')
398
+ // Remove trailing semicolons before }
399
+ .replace(/;}/g, '}')
400
+ // Remove empty rules
401
+ .replace(/[^{}]+\{\}/g, '')
402
+ .trim();
403
+ }
404
+
405
+ // ─────────────────────────────────────────────
406
+ // Vendor Autoprefixing
407
+ // ─────────────────────────────────────────────
408
+
409
+ /**
410
+ * Adds vendor prefixes to CSS.
411
+ * @param {string} css
412
+ * @returns {string}
413
+ */
414
+ autoPrefix(css) {
415
+ if (typeof css !== 'string') return css;
416
+ let result = css;
417
+
418
+ for (const [property, prefixed] of Object.entries(this._vendorPrefixMap)) {
419
+ // Match lines: " property: value;"
420
+ const regex = new RegExp(`(^|[{;])\\s*(${property.replace(/-/g, '\\-')})\\s*:\\s*([^;}{]+);`, 'gm');
421
+ result = result.replace(regex, (match, before, prop, value) => {
422
+ const declarations = prefixed
423
+ .map((p) => `${p}: ${value};`)
424
+ .join('\n ');
425
+ return `${before}\n ${declarations}`;
426
+ });
427
+ }
428
+
429
+ return result;
430
+ }
431
+
432
+ // ─────────────────────────────────────────────
433
+ // ThyScript CSS Compiler
434
+ // ─────────────────────────────────────────────
435
+
436
+ /**
437
+ * Compiles ThyScript-flavored CSS syntax to standard CSS.
438
+ * Supports: keyword shortcuts, variables ($var: value), nesting (&),
439
+ * math expressions (calc expressions), and color functions.
440
+ * @param {string} thyCSS
441
+ * @returns {string}
442
+ */
443
+ compileCSS(thyCSS) {
444
+ if (typeof thyCSS !== 'string') return thyCSS;
445
+
446
+ // Extract and apply variables: $varName: value;
447
+ const vars = {};
448
+ let css = thyCSS.replace(/\$(\w+)\s*:\s*([^;]+);/g, (_, name, value) => {
449
+ vars[name] = value.trim();
450
+ return '';
451
+ });
452
+
453
+ // Replace variable usages: $varName
454
+ css = css.replace(/\$(\w+)/g, (_, name) => vars[name] || `var(--${name})`);
455
+
456
+ // Replace ThyScript keyword shortcuts inside rules
457
+ for (const [keyword, expansion] of Object.entries(this._thyCSSSyntax)) {
458
+ const regex = new RegExp(`\\b${keyword}\\b\\s*;`, 'g');
459
+ css = css.replace(regex, expansion);
460
+ }
461
+
462
+ // Expand nested selectors (simple single-level nesting with &)
463
+ css = this._expandNestedCSS(css);
464
+
465
+ return css.trim();
466
+ }
467
+
468
+ _expandNestedCSS(css) {
469
+ const output = [];
470
+ let i = 0;
471
+ const len = css.length;
472
+
473
+ while (i < len) {
474
+ // Find the next selector + block
475
+ const braceOpen = css.indexOf('{', i);
476
+ if (braceOpen === -1) { output.push(css.slice(i)); break; }
477
+
478
+ const selector = css.slice(i, braceOpen).trim();
479
+ let depth = 1;
480
+ let j = braceOpen + 1;
481
+ while (j < len && depth > 0) {
482
+ if (css[j] === '{') depth++;
483
+ else if (css[j] === '}') depth--;
484
+ j++;
485
+ }
486
+ const blockContent = css.slice(braceOpen + 1, j - 1);
487
+
488
+ // Check for nested selectors inside blockContent
489
+ const nested = [];
490
+ let plainProps = '';
491
+ let k = 0;
492
+ const bl = blockContent.length;
493
+ while (k < bl) {
494
+ const innerBrace = blockContent.indexOf('{', k);
495
+ if (innerBrace === -1) { plainProps += blockContent.slice(k); break; }
496
+ const nestedSelector = blockContent.slice(k, innerBrace).trim();
497
+ // If nestedSelector contains & or looks like a selector, treat as nested
498
+ if (nestedSelector && (nestedSelector.includes('&') || /^[.:#@>~+\[a-zA-Z*]/.test(nestedSelector.replace(/^\s+/, '')))) {
499
+ plainProps += blockContent.slice(k, blockContent.lastIndexOf(nestedSelector, innerBrace)).trim();
500
+ let nd = 1;
501
+ let l = innerBrace + 1;
502
+ while (l < bl && nd > 0) {
503
+ if (blockContent[l] === '{') nd++;
504
+ else if (blockContent[l] === '}') nd--;
505
+ l++;
506
+ }
507
+ const nestedBlock = blockContent.slice(innerBrace + 1, l - 1);
508
+ const resolvedSel = nestedSelector.includes('&')
509
+ ? nestedSelector.replace(/&/g, selector)
510
+ : `${selector} ${nestedSelector}`;
511
+ nested.push(`${resolvedSel} {\n${nestedBlock}\n}`);
512
+ k = l;
513
+ } else {
514
+ plainProps += blockContent.slice(k, innerBrace + 1);
515
+ k = innerBrace + 1;
516
+ }
517
+ }
518
+
519
+ if (plainProps.trim()) output.push(`${selector} {\n${plainProps.trim()}\n}`);
520
+ output.push(...nested);
521
+ i = j;
522
+ }
523
+
524
+ return output.join('\n');
525
+ }
526
+
527
+ // ─────────────────────────────────────────────
528
+ // Complete Responsive Web Page Generator
529
+ // ─────────────────────────────────────────────
530
+
531
+ /**
532
+ * Generates a complete responsive web page with all bells and whistles.
533
+ * @param {object} options
534
+ * @param {string} [options.title]
535
+ * @param {string} [options.primaryColor]
536
+ * @param {string} [options.fontFamily]
537
+ * @param {string} [options.theme] - 'light' | 'dark' | 'auto'
538
+ * @param {string} [options.heroTitle]
539
+ * @param {string} [options.heroSubtitle]
540
+ * @param {object[]} [options.sections] - [{ id, title, content }]
541
+ * @param {object[]} [options.navLinks] - [{ href, label }]
542
+ * @param {string} [options.footer]
543
+ * @param {boolean} [options.animations]
544
+ * @returns {string}
545
+ */
546
+ generateWebPage(options = {}) {
547
+ const {
548
+ title = 'ThyScript App',
549
+ primaryColor = '#6366f1',
550
+ fontFamily = "'Segoe UI', system-ui, -apple-system, sans-serif",
551
+ theme = 'auto',
552
+ heroTitle = 'Welcome',
553
+ heroSubtitle = 'Built with ThyScript',
554
+ sections = [],
555
+ navLinks = [],
556
+ footer = `© ${new Date().getFullYear()} ThyScript`,
557
+ animations = true,
558
+ } = options;
559
+
560
+ const navHTML = navLinks.length
561
+ ? `<nav class="ts-nav">
562
+ <div class="ts-nav-brand">${this._escapeHTML(title)}</div>
563
+ <ul class="ts-nav-links">
564
+ ${navLinks.map((l) => `<li><a href="${l.href}">${this._escapeHTML(l.label)}</a></li>`).join('')}
565
+ </ul>
566
+ </nav>`
567
+ : '';
568
+
569
+ const sectionsHTML = sections.map((s) => `
570
+ <section id="${s.id || ''}" class="ts-section">
571
+ <h2>${this._escapeHTML(s.title || '')}</h2>
572
+ <div class="ts-section-content">${s.content || ''}</div>
573
+ </section>`).join('');
574
+
575
+ const animCSS = animations ? this._generateAllAnimations() : '';
576
+ const gridCSS = this._generateGrid(12);
577
+ const themeCSS = this._generateThemeCSS(theme);
578
+
579
+ const inlineCSS = `
580
+ * { box-sizing: border-box; margin: 0; padding: 0; }
581
+ body { font-family: ${fontFamily}; line-height: 1.6; }
582
+ .ts-nav { display: flex; align-items: center; justify-content: space-between; padding: 1rem 2rem; background: ${primaryColor}; color: #fff; position: sticky; top: 0; z-index: 100; }
583
+ .ts-nav-brand { font-size: 1.25rem; font-weight: 700; }
584
+ .ts-nav-links { list-style: none; display: flex; gap: 1.5rem; }
585
+ .ts-nav-links a { color: #fff; text-decoration: none; opacity: 0.85; transition: opacity 0.2s; }
586
+ .ts-nav-links a:hover { opacity: 1; }
587
+ .ts-hero { min-height: 60vh; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 4rem 2rem; background: linear-gradient(135deg, ${primaryColor}22, ${primaryColor}44); }
588
+ .ts-hero h1 { font-size: clamp(2rem, 5vw, 4rem); margin-bottom: 1rem; color: ${primaryColor}; ${animations ? 'animation: ts-fade-in 0.8s ease;' : ''} }
589
+ .ts-hero p { font-size: 1.2rem; opacity: 0.75; max-width: 600px; ${animations ? 'animation: ts-slide-up 0.8s ease 0.2s both;' : ''} }
590
+ .ts-section { padding: 4rem 2rem; max-width: 1200px; margin: 0 auto; }
591
+ .ts-section h2 { font-size: 2rem; margin-bottom: 1.5rem; color: ${primaryColor}; }
592
+ .ts-section-content { opacity: 0.8; }
593
+ .ts-footer { padding: 2rem; text-align: center; background: #f8f8f8; border-top: 1px solid #eee; opacity: 0.7; margin-top: 4rem; }
594
+ @media (max-width: 768px) {
595
+ .ts-nav { flex-direction: column; gap: 0.75rem; }
596
+ .ts-nav-links { flex-wrap: wrap; justify-content: center; }
597
+ }
598
+ ${animCSS}
599
+ ${gridCSS}
600
+ ${themeCSS}
601
+ `;
602
+
603
+ const body = `
604
+ ${navHTML}
605
+ <main>
606
+ <div class="ts-hero">
607
+ <h1>${this._escapeHTML(heroTitle)}</h1>
608
+ <p>${this._escapeHTML(heroSubtitle)}</p>
609
+ </div>
610
+ ${sectionsHTML}
611
+ </main>
612
+ <footer class="ts-footer">${footer}</footer>
613
+ `;
614
+
615
+ return this.generatePage({ title, inlineCSS, body, responsive: true });
616
+ }
617
+
618
+ // ─────────────────────────────────────────────
619
+ // Reactive Rendering
620
+ // ─────────────────────────────────────────────
621
+
622
+ /**
623
+ * Renders a component with reactive state.
624
+ * On state changes, re-renders and returns new HTML.
625
+ * @param {{ name: string, template: string, onUpdate?: function }} component
626
+ * @param {object} state
627
+ * @returns {{ html: string, update: function }}
628
+ */
629
+ renderReactive(component, state) {
630
+ const { name, template, onUpdate } = component;
631
+ this._components.set(name, component);
632
+ this._componentStates.set(name, { ...state });
633
+
634
+ const render = (currentState) => {
635
+ return this.render(template, currentState);
636
+ };
637
+
638
+ const html = render(state);
639
+
640
+ const update = (newState) => {
641
+ const currentState = this._componentStates.get(name) || {};
642
+ const merged = { ...currentState, ...newState };
643
+ this._componentStates.set(name, merged);
644
+ const newHtml = render(merged);
645
+ if (typeof onUpdate === 'function') onUpdate(newHtml, merged);
646
+ return newHtml;
647
+ };
648
+
649
+ return { html, update, getState: () => ({ ...this._componentStates.get(name) }) };
650
+ }
651
+
652
+ // ─────────────────────────────────────────────
653
+ // Server-Side Rendering
654
+ // ─────────────────────────────────────────────
655
+
656
+ /**
657
+ * Server-side renders a component with given props.
658
+ * @param {{ template: string, css?: object, wrapper?: string }} component
659
+ * @param {object} props
660
+ * @returns {{ html: string, css: string, fullPage: string }}
661
+ */
662
+ serverSideRender(component, props = {}) {
663
+ const { template, css = {}, wrapper = 'div', name = 'ssr-component' } = component;
664
+
665
+ const html = this.render(template, props);
666
+ const cssStr = css ? this.generateCSS(css) : '';
667
+ const prefixedCSS = cssStr ? this.autoPrefix(cssStr) : '';
668
+ const wrappedHTML = `<${wrapper} class="ts-ssr-root" data-component="${name}">${html}</${wrapper}>`;
669
+
670
+ const fullPage = this.generatePage({
671
+ title: props.title || name,
672
+ inlineCSS: prefixedCSS,
673
+ body: wrappedHTML,
674
+ meta: { 'generator': 'ThyScript SSR' },
675
+ });
676
+
677
+ return { html: wrappedHTML, css: prefixedCSS, fullPage };
678
+ }
679
+
680
+ // ─────────────────────────────────────────────
681
+ // Animation Generator
682
+ // ─────────────────────────────────────────────
683
+
684
+ /**
685
+ * Generates CSS for a specific animation.
686
+ * @param {'fade'|'slide'|'bounce'|'pulse'|'rotate'|'shake'|'flip'|'zoom'} type
687
+ * @param {object} [opts] - { duration, easing, delay, iterationCount }
688
+ * @returns {string} CSS @keyframes + class definition.
689
+ */
690
+ generateAnimation(type, opts = {}) {
691
+ const {
692
+ duration = '0.5s',
693
+ easing = 'ease',
694
+ delay = '0s',
695
+ iterationCount = '1',
696
+ } = opts;
697
+
698
+ const animations = {
699
+ fade: `
700
+ @keyframes ts-fade-in { from { opacity: 0; } to { opacity: 1; } }
701
+ .ts-fade-in { animation: ts-fade-in ${duration} ${easing} ${delay} ${iterationCount} both; }
702
+ @keyframes ts-fade-out { from { opacity: 1; } to { opacity: 0; } }
703
+ .ts-fade-out { animation: ts-fade-out ${duration} ${easing} ${delay} ${iterationCount} both; }`,
704
+
705
+ slide: `
706
+ @keyframes ts-slide-up { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
707
+ .ts-slide-up { animation: ts-slide-up ${duration} ${easing} ${delay} ${iterationCount} both; }
708
+ @keyframes ts-slide-down { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
709
+ .ts-slide-down { animation: ts-slide-down ${duration} ${easing} ${delay} ${iterationCount} both; }
710
+ @keyframes ts-slide-left { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
711
+ .ts-slide-left { animation: ts-slide-left ${duration} ${easing} ${delay} ${iterationCount} both; }
712
+ @keyframes ts-slide-right { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
713
+ .ts-slide-right { animation: ts-slide-right ${duration} ${easing} ${delay} ${iterationCount} both; }`,
714
+
715
+ bounce: `
716
+ @keyframes ts-bounce { 0%, 100% { transform: translateY(0); animation-timing-function: cubic-bezier(0.8,0,1,1); } 50% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0,0,0.2,1); } }
717
+ .ts-bounce { animation: ts-bounce ${duration} ${easing} ${delay} ${iterationCount}; }`,
718
+
719
+ pulse: `
720
+ @keyframes ts-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
721
+ .ts-pulse { animation: ts-pulse ${duration} ${easing} ${delay} ${iterationCount}; }
722
+ @keyframes ts-pulse-scale { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
723
+ .ts-pulse-scale { animation: ts-pulse-scale ${duration} ${easing} ${delay} ${iterationCount}; }`,
724
+
725
+ rotate: `
726
+ @keyframes ts-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
727
+ .ts-rotate { animation: ts-rotate ${duration} linear ${delay} ${iterationCount}; }
728
+ @keyframes ts-rotate-back { from { transform: rotate(360deg); } to { transform: rotate(0deg); } }
729
+ .ts-rotate-back { animation: ts-rotate-back ${duration} linear ${delay} ${iterationCount}; }`,
730
+
731
+ shake: `
732
+ @keyframes ts-shake { 0%, 100% { transform: translateX(0); } 10%, 50%, 90% { transform: translateX(-8px); } 30%, 70% { transform: translateX(8px); } }
733
+ .ts-shake { animation: ts-shake ${duration} ${easing} ${delay} ${iterationCount}; }`,
734
+
735
+ flip: `
736
+ @keyframes ts-flip { from { transform: perspective(400px) rotateY(0); } to { transform: perspective(400px) rotateY(180deg); } }
737
+ .ts-flip { animation: ts-flip ${duration} ${easing} ${delay} ${iterationCount}; }`,
738
+
739
+ zoom: `
740
+ @keyframes ts-zoom-in { from { transform: scale(0.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
741
+ .ts-zoom-in { animation: ts-zoom-in ${duration} ${easing} ${delay} ${iterationCount} both; }
742
+ @keyframes ts-zoom-out { from { transform: scale(1); opacity: 1; } to { transform: scale(0.8); opacity: 0; } }
743
+ .ts-zoom-out { animation: ts-zoom-out ${duration} ${easing} ${delay} ${iterationCount} both; }`,
744
+ };
745
+
746
+ return animations[type] || '';
747
+ }
748
+
749
+ _generateAllAnimations() {
750
+ return ['fade', 'slide', 'bounce', 'pulse', 'rotate', 'shake', 'flip', 'zoom']
751
+ .map((t) => this.generateAnimation(t))
752
+ .join('\n');
753
+ }
754
+
755
+ // ─────────────────────────────────────────────
756
+ // Grid Generator
757
+ // ─────────────────────────────────────────────
758
+
759
+ /**
760
+ * Generates a responsive CSS Grid system.
761
+ * @param {number} [columns=12] - Number of grid columns.
762
+ * @param {string} [gap='1rem'] - Grid gap.
763
+ * @param {object} [breakpoints] - { sm: '640px', md: '768px', lg: '1024px', xl: '1280px' }
764
+ * @returns {string}
765
+ */
766
+ generateGrid(columns = 12, gap = '1rem', breakpoints = {}) {
767
+ return this._generateGrid(columns, gap, breakpoints);
768
+ }
769
+
770
+ _generateGrid(columns = 12, gap = '1rem', breakpoints = {}) {
771
+ const bp = Object.assign({ sm: '640px', md: '768px', lg: '1024px', xl: '1280px' }, breakpoints);
772
+ const colClasses = [];
773
+
774
+ for (let i = 1; i <= columns; i++) {
775
+ colClasses.push(`.ts-col-${i} { grid-column: span ${i} / span ${i}; }`);
776
+ }
777
+
778
+ const smCols = Array.from({ length: columns }, (_, i) => `.ts-col-sm-${i + 1} { grid-column: span ${i + 1} / span ${i + 1}; }`);
779
+ const mdCols = Array.from({ length: columns }, (_, i) => `.ts-col-md-${i + 1} { grid-column: span ${i + 1} / span ${i + 1}; }`);
780
+ const lgCols = Array.from({ length: columns }, (_, i) => `.ts-col-lg-${i + 1} { grid-column: span ${i + 1} / span ${i + 1}; }`);
781
+
782
+ return `
783
+ .ts-grid { display: grid; grid-template-columns: repeat(${columns}, minmax(0, 1fr)); gap: ${gap}; }
784
+ .ts-grid-auto { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: ${gap}; }
785
+ ${colClasses.join('\n')}
786
+ @media (min-width: ${bp.sm}) { ${smCols.join(' ')} }
787
+ @media (min-width: ${bp.md}) { ${mdCols.join(' ')} }
788
+ @media (min-width: ${bp.lg}) { ${lgCols.join(' ')} }
789
+ `;
790
+ }
791
+
792
+ // ─────────────────────────────────────────────
793
+ // Theme Generator
794
+ // ─────────────────────────────────────────────
795
+
796
+ /**
797
+ * Generates dark/light mode CSS custom properties.
798
+ * @param {'light'|'dark'|'auto'} [mode='auto']
799
+ * @param {object} [customVars] - Additional CSS variable overrides.
800
+ * @returns {string}
801
+ */
802
+ generateTheme(mode = 'auto', customVars = {}) {
803
+ return this._generateThemeCSS(mode, customVars);
804
+ }
805
+
806
+ _generateThemeCSS(mode = 'auto', customVars = {}) {
807
+ const lightVars = Object.assign({
808
+ '--ts-bg': '#ffffff',
809
+ '--ts-bg-secondary': '#f8f9fa',
810
+ '--ts-text': '#1a1a2e',
811
+ '--ts-text-secondary': '#6c757d',
812
+ '--ts-border': '#dee2e6',
813
+ '--ts-primary': '#6366f1',
814
+ '--ts-primary-hover': '#4f46e5',
815
+ '--ts-success': '#10b981',
816
+ '--ts-warning': '#f59e0b',
817
+ '--ts-error': '#ef4444',
818
+ '--ts-info': '#3b82f6',
819
+ '--ts-shadow': 'rgba(0,0,0,0.1)',
820
+ '--ts-card-bg': '#ffffff',
821
+ }, customVars.light || {});
822
+
823
+ const darkVars = Object.assign({
824
+ '--ts-bg': '#0f0f23',
825
+ '--ts-bg-secondary': '#1a1a2e',
826
+ '--ts-text': '#e2e8f0',
827
+ '--ts-text-secondary': '#94a3b8',
828
+ '--ts-border': '#2d2d44',
829
+ '--ts-primary': '#818cf8',
830
+ '--ts-primary-hover': '#6366f1',
831
+ '--ts-success': '#34d399',
832
+ '--ts-warning': '#fbbf24',
833
+ '--ts-error': '#f87171',
834
+ '--ts-info': '#60a5fa',
835
+ '--ts-shadow': 'rgba(0,0,0,0.4)',
836
+ '--ts-card-bg': '#1e1e3a',
837
+ }, customVars.dark || {});
838
+
839
+ const renderVars = (vars) => Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join('\n');
840
+
841
+ if (mode === 'light') {
842
+ return `:root {\n${renderVars(lightVars)}\n}`;
843
+ }
844
+ if (mode === 'dark') {
845
+ return `:root {\n${renderVars(darkVars)}\n}`;
846
+ }
847
+ // Auto (system preference)
848
+ return `
849
+ :root {
850
+ ${renderVars(lightVars)}
851
+ }
852
+ @media (prefers-color-scheme: dark) {
853
+ :root {
854
+ ${renderVars(darkVars)}
855
+ }
856
+ }
857
+ body {
858
+ background-color: var(--ts-bg);
859
+ color: var(--ts-text);
860
+ transition: background-color 0.3s ease, color 0.3s ease;
861
+ }
862
+ `;
863
+ }
864
+ }
865
+
866
+ module.exports = HTMLCSSEngine;