tina4-nodejs 3.0.0-rc.2
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/BENCHMARK_REPORT.md +96 -0
- package/CARBONAH.md +140 -0
- package/CLAUDE.md +599 -0
- package/COMPARISON.md +194 -0
- package/README.md +595 -0
- package/package.json +59 -0
- package/packages/cli/src/bin.ts +110 -0
- package/packages/cli/src/commands/init.ts +194 -0
- package/packages/cli/src/commands/migrate.ts +96 -0
- package/packages/cli/src/commands/migrateCreate.ts +59 -0
- package/packages/cli/src/commands/routes.ts +61 -0
- package/packages/cli/src/commands/serve.ts +58 -0
- package/packages/cli/src/commands/test.ts +83 -0
- package/packages/core/gallery/auth/meta.json +1 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
- package/packages/core/gallery/database/meta.json +1 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
- package/packages/core/gallery/error-overlay/meta.json +1 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
- package/packages/core/gallery/orm/meta.json +1 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
- package/packages/core/gallery/queue/meta.json +1 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
- package/packages/core/gallery/rest-api/meta.json +1 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
- package/packages/core/gallery/templates/meta.json +1 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
- package/packages/core/public/css/tina4.css +2463 -0
- package/packages/core/public/css/tina4.min.css +1 -0
- package/packages/core/public/favicon.ico +0 -0
- package/packages/core/public/images/logo.svg +5 -0
- package/packages/core/public/images/tina4-logo-icon.webp +0 -0
- package/packages/core/public/js/frond.min.js +420 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
- package/packages/core/public/js/tina4.min.js +93 -0
- package/packages/core/public/swagger/index.html +90 -0
- package/packages/core/public/swagger/oauth2-redirect.html +63 -0
- package/packages/core/src/ai.ts +359 -0
- package/packages/core/src/api.ts +248 -0
- package/packages/core/src/auth.ts +287 -0
- package/packages/core/src/cache.ts +121 -0
- package/packages/core/src/constants.ts +48 -0
- package/packages/core/src/container.ts +90 -0
- package/packages/core/src/devAdmin.ts +2024 -0
- package/packages/core/src/devMailbox.ts +316 -0
- package/packages/core/src/dotenv.ts +172 -0
- package/packages/core/src/errorOverlay.test.ts +122 -0
- package/packages/core/src/errorOverlay.ts +278 -0
- package/packages/core/src/events.ts +112 -0
- package/packages/core/src/fakeData.ts +309 -0
- package/packages/core/src/graphql.ts +812 -0
- package/packages/core/src/health.ts +31 -0
- package/packages/core/src/htmlElement.ts +172 -0
- package/packages/core/src/i18n.ts +136 -0
- package/packages/core/src/index.ts +88 -0
- package/packages/core/src/logger.ts +226 -0
- package/packages/core/src/messenger.ts +822 -0
- package/packages/core/src/middleware.ts +138 -0
- package/packages/core/src/queue.ts +481 -0
- package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
- package/packages/core/src/rateLimiter.ts +107 -0
- package/packages/core/src/request.ts +189 -0
- package/packages/core/src/response.ts +146 -0
- package/packages/core/src/routeDiscovery.ts +87 -0
- package/packages/core/src/router.ts +398 -0
- package/packages/core/src/scss.ts +366 -0
- package/packages/core/src/server.ts +610 -0
- package/packages/core/src/service.ts +380 -0
- package/packages/core/src/session.ts +480 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
- package/packages/core/src/static.ts +58 -0
- package/packages/core/src/testing.ts +233 -0
- package/packages/core/src/types.ts +98 -0
- package/packages/core/src/watcher.ts +37 -0
- package/packages/core/src/websocket.ts +408 -0
- package/packages/core/src/wsdl.ts +546 -0
- package/packages/core/templates/errors/302.twig +14 -0
- package/packages/core/templates/errors/401.twig +9 -0
- package/packages/core/templates/errors/403.twig +29 -0
- package/packages/core/templates/errors/404.twig +29 -0
- package/packages/core/templates/errors/500.twig +38 -0
- package/packages/core/templates/errors/502.twig +9 -0
- package/packages/core/templates/errors/503.twig +12 -0
- package/packages/core/templates/errors/base.twig +37 -0
- package/packages/frond/src/engine.ts +1475 -0
- package/packages/frond/src/index.ts +2 -0
- package/packages/orm/src/adapters/firebird.ts +455 -0
- package/packages/orm/src/adapters/mssql.ts +440 -0
- package/packages/orm/src/adapters/mysql.ts +355 -0
- package/packages/orm/src/adapters/postgres.ts +362 -0
- package/packages/orm/src/adapters/sqlite.ts +270 -0
- package/packages/orm/src/autoCrud.ts +231 -0
- package/packages/orm/src/baseModel.ts +536 -0
- package/packages/orm/src/database.ts +321 -0
- package/packages/orm/src/fakeData.ts +118 -0
- package/packages/orm/src/index.ts +49 -0
- package/packages/orm/src/migration.ts +392 -0
- package/packages/orm/src/model.ts +56 -0
- package/packages/orm/src/query.ts +113 -0
- package/packages/orm/src/seeder.ts +120 -0
- package/packages/orm/src/sqlTranslation.ts +272 -0
- package/packages/orm/src/types.ts +110 -0
- package/packages/orm/src/validation.ts +93 -0
- package/packages/swagger/src/generator.ts +189 -0
- package/packages/swagger/src/index.ts +2 -0
- package/packages/swagger/src/ui.ts +48 -0
- package/skills/tina4-developer.skill +0 -0
- package/skills/tina4-js.skill +0 -0
- package/skills/tina4-maintainer.skill +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// Tina4 SCSS — Zero-dependency SCSS-to-CSS compiler (subset).
|
|
2
|
+
// Supports variables, nesting, & parent selector, @import, @mixin/@include, comments, basic math.
|
|
3
|
+
|
|
4
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { join, resolve, dirname } from "node:path";
|
|
6
|
+
|
|
7
|
+
// ── Types ────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface ScssConfig {
|
|
10
|
+
importPaths?: string[];
|
|
11
|
+
variables?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ── ScssCompiler ─────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export class ScssCompiler {
|
|
17
|
+
private _importPaths: string[];
|
|
18
|
+
private _variables: Record<string, string>;
|
|
19
|
+
|
|
20
|
+
constructor(config?: ScssConfig) {
|
|
21
|
+
this._importPaths = config?.importPaths ? [...config.importPaths] : [];
|
|
22
|
+
this._variables = config?.variables ? { ...config.variables } : {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Compile an SCSS string to CSS. */
|
|
26
|
+
compile(source: string): string {
|
|
27
|
+
return compileString(source, this._importPaths, { ...this._variables });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Compile an SCSS file to CSS. */
|
|
31
|
+
compileFile(filePath: string): string {
|
|
32
|
+
const absPath = resolve(filePath);
|
|
33
|
+
const content = readFileSync(absPath, "utf-8");
|
|
34
|
+
const paths = [dirname(absPath), ...this._importPaths];
|
|
35
|
+
return compileString(content, paths, { ...this._variables });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Add a directory to the import resolution path. */
|
|
39
|
+
addImportPath(path: string): void {
|
|
40
|
+
this._importPaths.push(resolve(path));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Set or override an SCSS variable. */
|
|
44
|
+
setVariable(name: string, value: string): void {
|
|
45
|
+
// Strip leading $ if provided
|
|
46
|
+
const key = name.startsWith("$") ? name.slice(1) : name;
|
|
47
|
+
this._variables[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Internal Compilation Pipeline ────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function compileString(
|
|
54
|
+
scss: string,
|
|
55
|
+
importPaths: string[],
|
|
56
|
+
variables: Record<string, string>
|
|
57
|
+
): string {
|
|
58
|
+
// 1. Resolve @import statements
|
|
59
|
+
const imported = new Set<string>();
|
|
60
|
+
scss = resolveImports(scss, importPaths, imported);
|
|
61
|
+
|
|
62
|
+
// 2. Strip single-line comments (preserve /* */ block comments)
|
|
63
|
+
scss = scss.replace(/(?<![:"'])\/\/[^\n]*/g, "");
|
|
64
|
+
|
|
65
|
+
// 3. Extract and store variables
|
|
66
|
+
scss = extractVariables(scss, variables);
|
|
67
|
+
|
|
68
|
+
// 4. Extract mixins
|
|
69
|
+
const mixins: Record<string, { params: string[]; body: string }> = {};
|
|
70
|
+
scss = extractMixins(scss, mixins);
|
|
71
|
+
|
|
72
|
+
// 5. Resolve @include
|
|
73
|
+
scss = resolveIncludes(scss, mixins);
|
|
74
|
+
|
|
75
|
+
// 6. Substitute variables
|
|
76
|
+
scss = substituteVariables(scss, variables);
|
|
77
|
+
|
|
78
|
+
// 7. Evaluate basic math in property values
|
|
79
|
+
scss = evalMath(scss);
|
|
80
|
+
|
|
81
|
+
// 8. Flatten nested rules
|
|
82
|
+
const css = flattenNesting(scss);
|
|
83
|
+
|
|
84
|
+
// 9. Cleanup
|
|
85
|
+
return cleanup(css);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Import Resolution ────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function resolveImports(content: string, paths: string[], imported: Set<string>): string {
|
|
91
|
+
return content.replace(/@import\s+["']?([^"';\n]+)["']?\s*;/g, (_match, name: string) => {
|
|
92
|
+
name = name.trim();
|
|
93
|
+
const candidates: string[] = [];
|
|
94
|
+
for (const base of paths) {
|
|
95
|
+
candidates.push(
|
|
96
|
+
join(base, `${name}.scss`),
|
|
97
|
+
join(base, `_${name}.scss`),
|
|
98
|
+
join(base, name),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
for (const candidate of candidates) {
|
|
102
|
+
if (existsSync(candidate) && !imported.has(candidate)) {
|
|
103
|
+
imported.add(candidate);
|
|
104
|
+
const fileContent = readFileSync(candidate, "utf-8");
|
|
105
|
+
return resolveImports(fileContent, [dirname(candidate), ...paths], imported);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return `/* IMPORT NOT FOUND: ${name} */`;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Variables ────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function extractVariables(scss: string, variables: Record<string, string>): string {
|
|
115
|
+
return scss.replace(/\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/g, (_m, name: string, value: string) => {
|
|
116
|
+
let resolved = value.trim();
|
|
117
|
+
// Resolve variable references within the value
|
|
118
|
+
for (const [vName, vVal] of Object.entries(variables)) {
|
|
119
|
+
resolved = resolved.replaceAll(`$${vName}`, vVal);
|
|
120
|
+
}
|
|
121
|
+
variables[name] = resolved;
|
|
122
|
+
return "";
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function substituteVariables(scss: string, variables: Record<string, string>): string {
|
|
127
|
+
// Sort by longest name first to avoid partial matches
|
|
128
|
+
const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
|
|
129
|
+
for (const name of sorted) {
|
|
130
|
+
scss = scss.replaceAll(`$${name}`, variables[name]);
|
|
131
|
+
}
|
|
132
|
+
return scss;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Mixins ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function extractMixins(
|
|
138
|
+
scss: string,
|
|
139
|
+
mixins: Record<string, { params: string[]; body: string }>
|
|
140
|
+
): string {
|
|
141
|
+
const pattern = /@mixin\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*\{/g;
|
|
142
|
+
let match: RegExpExecArray | null;
|
|
143
|
+
const locations: { start: number; end: number; name: string }[] = [];
|
|
144
|
+
|
|
145
|
+
while ((match = pattern.exec(scss)) !== null) {
|
|
146
|
+
const name = match[1];
|
|
147
|
+
const paramsStr = match[2] ?? "";
|
|
148
|
+
const params = paramsStr
|
|
149
|
+
.split(",")
|
|
150
|
+
.map((p) => p.trim().replace(/^\$/, ""))
|
|
151
|
+
.filter(Boolean);
|
|
152
|
+
|
|
153
|
+
const bodyStart = match.index + match[0].length;
|
|
154
|
+
const body = findBlock(scss, bodyStart);
|
|
155
|
+
if (body !== null) {
|
|
156
|
+
mixins[name] = { params, body };
|
|
157
|
+
locations.push({
|
|
158
|
+
start: match.index,
|
|
159
|
+
end: bodyStart + body.length + 1,
|
|
160
|
+
name,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Remove mixin definitions from source (reverse order to preserve indices)
|
|
166
|
+
let result = scss;
|
|
167
|
+
for (const loc of locations.reverse()) {
|
|
168
|
+
result = result.slice(0, loc.start) + result.slice(loc.end);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveIncludes(
|
|
174
|
+
scss: string,
|
|
175
|
+
mixins: Record<string, { params: string[]; body: string }>
|
|
176
|
+
): string {
|
|
177
|
+
return scss.replace(
|
|
178
|
+
/@include\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*;/g,
|
|
179
|
+
(_m, name: string, argsStr: string | undefined) => {
|
|
180
|
+
if (!(name in mixins)) {
|
|
181
|
+
return `/* MIXIN NOT FOUND: ${name} */`;
|
|
182
|
+
}
|
|
183
|
+
const mixin = mixins[name];
|
|
184
|
+
const args = argsStr
|
|
185
|
+
? argsStr.split(",").map((a) => a.trim()).filter(Boolean)
|
|
186
|
+
: [];
|
|
187
|
+
let body = mixin.body;
|
|
188
|
+
for (let i = 0; i < mixin.params.length; i++) {
|
|
189
|
+
const paramName = mixin.params[i].split(":")[0].trim();
|
|
190
|
+
const defaultVal = mixin.params[i].includes(":")
|
|
191
|
+
? mixin.params[i].split(":").slice(1).join(":").trim()
|
|
192
|
+
: "";
|
|
193
|
+
const value = i < args.length ? args[i] : defaultVal;
|
|
194
|
+
body = body.replaceAll(`$${paramName}`, value);
|
|
195
|
+
}
|
|
196
|
+
return body;
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Math Evaluation ──────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
function evalMath(scss: string): string {
|
|
204
|
+
return scss.replace(
|
|
205
|
+
/([\d.]+)([a-z%]*)\s*([+\-*/])\s*([\d.]+)([a-z%]*)/g,
|
|
206
|
+
(_m, n1: string, u1: string, op: string, n2: string, _u2: string) => {
|
|
207
|
+
try {
|
|
208
|
+
const num1 = parseFloat(n1);
|
|
209
|
+
const num2 = parseFloat(n2);
|
|
210
|
+
let result: number;
|
|
211
|
+
switch (op) {
|
|
212
|
+
case "+": result = num1 + num2; break;
|
|
213
|
+
case "-": result = num1 - num2; break;
|
|
214
|
+
case "*": result = num1 * num2; break;
|
|
215
|
+
case "/": result = num2 !== 0 ? num1 / num2 : 0; break;
|
|
216
|
+
default: return _m;
|
|
217
|
+
}
|
|
218
|
+
const unit = u1 || "";
|
|
219
|
+
if (result === Math.floor(result)) {
|
|
220
|
+
return `${Math.floor(result)}${unit}`;
|
|
221
|
+
}
|
|
222
|
+
return `${result.toFixed(2)}${unit}`;
|
|
223
|
+
} catch {
|
|
224
|
+
return _m;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Nesting Flattener ────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
function flattenNesting(scss: string): string {
|
|
233
|
+
const output: string[] = [];
|
|
234
|
+
flattenBlock(scss, [], output);
|
|
235
|
+
return output.join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function flattenBlock(content: string, parentSelectors: string[], output: string[]): void {
|
|
239
|
+
let pos = 0;
|
|
240
|
+
const properties: string[] = [];
|
|
241
|
+
|
|
242
|
+
while (pos < content.length) {
|
|
243
|
+
// Skip whitespace
|
|
244
|
+
while (pos < content.length && /[\s]/.test(content[pos])) {
|
|
245
|
+
pos++;
|
|
246
|
+
}
|
|
247
|
+
if (pos >= content.length) break;
|
|
248
|
+
|
|
249
|
+
// Block comment — preserve
|
|
250
|
+
if (content[pos] === "/" && content[pos + 1] === "*") {
|
|
251
|
+
const end = content.indexOf("*/", pos + 2);
|
|
252
|
+
if (end === -1) break;
|
|
253
|
+
output.push(content.slice(pos, end + 2));
|
|
254
|
+
pos = end + 2;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// @media query — special handling
|
|
259
|
+
if (content.slice(pos, pos + 6) === "@media") {
|
|
260
|
+
const brace = content.indexOf("{", pos);
|
|
261
|
+
if (brace === -1) break;
|
|
262
|
+
const mediaQuery = content.slice(pos, brace).trim();
|
|
263
|
+
const body = findBlock(content, brace + 1);
|
|
264
|
+
if (body === null) break;
|
|
265
|
+
pos = brace + 1 + body.length + 1;
|
|
266
|
+
|
|
267
|
+
const innerOutput: string[] = [];
|
|
268
|
+
flattenBlock(body, parentSelectors, innerOutput);
|
|
269
|
+
if (innerOutput.length > 0) {
|
|
270
|
+
output.push(`${mediaQuery} {`);
|
|
271
|
+
for (const line of innerOutput) {
|
|
272
|
+
output.push(` ${line}`);
|
|
273
|
+
}
|
|
274
|
+
output.push("}");
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Find next { or ;
|
|
280
|
+
const bracePos = content.indexOf("{", pos);
|
|
281
|
+
const semiPos = content.indexOf(";", pos);
|
|
282
|
+
|
|
283
|
+
// Property (has ; before { or no { at all)
|
|
284
|
+
if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
|
|
285
|
+
const prop = content.slice(pos, semiPos).trim();
|
|
286
|
+
if (prop && !prop.startsWith("@")) {
|
|
287
|
+
properties.push(prop);
|
|
288
|
+
}
|
|
289
|
+
pos = semiPos + 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Nested block
|
|
294
|
+
if (bracePos !== -1) {
|
|
295
|
+
const selectorText = content.slice(pos, bracePos).trim();
|
|
296
|
+
const body = findBlock(content, bracePos + 1);
|
|
297
|
+
if (body === null) break;
|
|
298
|
+
pos = bracePos + 1 + body.length + 1;
|
|
299
|
+
|
|
300
|
+
if (!selectorText) continue;
|
|
301
|
+
|
|
302
|
+
// Expand selectors with parent reference (&)
|
|
303
|
+
const selectors = selectorText.split(",").map((s) => s.trim());
|
|
304
|
+
const newSelectors: string[] = [];
|
|
305
|
+
for (const sel of selectors) {
|
|
306
|
+
if (parentSelectors.length > 0) {
|
|
307
|
+
for (const parent of parentSelectors) {
|
|
308
|
+
if (sel.includes("&")) {
|
|
309
|
+
newSelectors.push(sel.replace(/&/g, parent));
|
|
310
|
+
} else {
|
|
311
|
+
newSelectors.push(`${parent} ${sel}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
newSelectors.push(sel);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
flattenBlock(body, newSelectors, output);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Remaining text — treat as property
|
|
324
|
+
const remaining = content.slice(pos).trim();
|
|
325
|
+
if (remaining) {
|
|
326
|
+
properties.push(remaining);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Emit properties for current selector
|
|
332
|
+
if (properties.length > 0 && parentSelectors.length > 0) {
|
|
333
|
+
const selectorStr = parentSelectors.join(", ");
|
|
334
|
+
output.push(`${selectorStr} {`);
|
|
335
|
+
for (const prop of properties) {
|
|
336
|
+
output.push(` ${prop};`);
|
|
337
|
+
}
|
|
338
|
+
output.push("}");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Utilities ────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
function findBlock(content: string, start: number): string | null {
|
|
345
|
+
let depth = 1;
|
|
346
|
+
let pos = start;
|
|
347
|
+
while (pos < content.length && depth > 0) {
|
|
348
|
+
if (content[pos] === "{") depth++;
|
|
349
|
+
else if (content[pos] === "}") depth--;
|
|
350
|
+
if (depth > 0) pos++;
|
|
351
|
+
}
|
|
352
|
+
return depth === 0 ? content.slice(start, pos) : null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function cleanup(css: string): string {
|
|
356
|
+
// Remove empty rulesets
|
|
357
|
+
css = css.replace(/[^{}]+\{\s*\}/g, "");
|
|
358
|
+
// Remove multiple blank lines
|
|
359
|
+
css = css.replace(/\n{3,}/g, "\n\n");
|
|
360
|
+
// Remove trailing whitespace per line
|
|
361
|
+
css = css
|
|
362
|
+
.split("\n")
|
|
363
|
+
.map((line) => line.trimEnd())
|
|
364
|
+
.join("\n");
|
|
365
|
+
return css.trim() + "\n";
|
|
366
|
+
}
|