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.
- package/bin/thy-node.js +332 -0
- package/bin/thy.js +828 -0
- package/bin/thys.js +25 -0
- package/examples/calculator.thy +274 -0
- package/examples/calculator.thy.js +628 -0
- package/examples/hello.thy +34 -0
- package/examples/login.thy +382 -0
- package/examples/login.thy.js +710 -0
- package/package.json +54 -0
- package/scripts/_class.js +18 -0
- package/scripts/_class2.js +11 -0
- package/scripts/_class3.js +21 -0
- package/scripts/_dstruct.js +20 -0
- package/scripts/_dstruct2.js +24 -0
- package/scripts/_scaffold.js +16 -0
- package/scripts/_scaffold2.js +16 -0
- package/scripts/_scaffold3.js +26 -0
- package/scripts/_tpl_compile.js +13 -0
- package/scripts/_tpl_test.js +9 -0
- package/scripts/_typeof.js +26 -0
- package/scripts/_typeof2.js +25 -0
- package/scripts/patch_genmethod.js +44 -0
- package/scripts/smoke_test.js +167 -0
- package/scripts/smoke_test2.js +151 -0
- package/scripts/test_control_panel.js +58 -0
- package/src/compiler/CodeGenerator.js +1449 -0
- package/src/compiler/JITCompiler.js +325 -0
- package/src/database/DatabaseEngine.js +931 -0
- package/src/errors/ErrorDetector.js +305 -0
- package/src/errors/ErrorMessages.js +497 -0
- package/src/index.js +169 -0
- package/src/lexer/Lexer.js +342 -0
- package/src/lexer/TokenTypes.js +1234 -0
- package/src/memory/MemoryManager.js +283 -0
- package/src/parser/AST.js +606 -0
- package/src/parser/Parser.js +1942 -0
- package/src/runtime/ThyRuntime.js +265 -0
- package/src/runtime/stdlib/ArrayLib.js +207 -0
- package/src/runtime/stdlib/FileLib.js +168 -0
- package/src/runtime/stdlib/MathLib.js +411 -0
- package/src/runtime/stdlib/StringLib.js +387 -0
- package/src/server/ControlPanel.js +1188 -0
- package/src/server/HTMLCSSEngine.js +866 -0
- package/src/server/SecurityEngine.js +714 -0
- package/src/server/ServerEngine.js +738 -0
- package/tests/test1_math.thy +82 -0
- package/tests/test2_oop.thy +133 -0
- package/tests/test3_server.thy +143 -0
- 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, '<')
|
|
124
|
+
.replace(/>/g, '>')
|
|
125
|
+
.replace(/"/g, '"')
|
|
126
|
+
.replace(/'/g, ''');
|
|
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;
|