tina4-nodejs 3.13.95 → 3.13.97
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/CLAUDE.md +3 -4
- package/package.json +2 -1
- package/packages/cli/dist/bin.js +708 -1012
- package/packages/core/dist/index.js +588 -893
- package/packages/core/public/css/tina4.min.css +1 -1
- package/packages/core/src/index.ts +1 -3
- package/packages/core/src/messenger.ts +288 -96
- package/packages/core/src/queueBackends/kafkaBackend.ts +23 -2
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +29 -17
- package/packages/core/src/request.ts +28 -7
- package/packages/core/src/server.ts +135 -7
- package/packages/core/src/session.ts +8 -1
- package/packages/orm/dist/index.js +639 -944
- package/packages/orm/src/autoCrud.ts +12 -10
- package/packages/orm/src/database.ts +62 -58
- package/packages/orm/src/databaseResult.ts +44 -73
- package/packages/orm/src/index.ts +0 -3
- package/packages/orm/src/migration.ts +26 -8
- package/packages/orm/src/model.ts +4 -0
- package/packages/orm/src/queryBuilder.ts +12 -5
- package/packages/orm/src/types.ts +7 -74
- package/packages/swagger/dist/index.js +78 -20
- package/packages/swagger/src/generator.ts +172 -29
- package/types/core/src/index.d.ts +1 -3
- package/types/core/src/messenger.d.ts +45 -4
- package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -0
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +2 -1
- package/types/core/src/server.d.ts +0 -4
- package/types/core/src/session.d.ts +7 -0
- package/types/orm/src/database.d.ts +34 -30
- package/types/orm/src/databaseResult.d.ts +26 -36
- package/types/orm/src/index.d.ts +1 -2
- package/types/orm/src/migration.d.ts +4 -3
- package/types/orm/src/types.d.ts +7 -34
- package/packages/core/src/scss.ts +0 -623
- package/types/core/src/scss.d.ts +0 -19
|
@@ -1,623 +0,0 @@
|
|
|
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, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
5
|
-
import { join, resolve, dirname, basename } 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
|
-
/** Compile all .scss files in a directory into a single CSS output file. */
|
|
51
|
-
compileScss(scssDir: string = "src/scss", output: string = "src/public/css/default.css", minify: boolean = false): string {
|
|
52
|
-
const absDir = resolve(scssDir);
|
|
53
|
-
if (!existsSync(absDir)) return "";
|
|
54
|
-
|
|
55
|
-
// Collect non-partial .scss files, sorted
|
|
56
|
-
const files = readdirSync(absDir)
|
|
57
|
-
.filter((f) => f.endsWith(".scss") && !f.startsWith("_"))
|
|
58
|
-
.sort()
|
|
59
|
-
.map((f) => join(absDir, f));
|
|
60
|
-
|
|
61
|
-
if (files.length === 0) return "";
|
|
62
|
-
|
|
63
|
-
// Merge all files, resolving imports
|
|
64
|
-
const paths = [absDir, ...this._importPaths];
|
|
65
|
-
const imported = new Set<string>();
|
|
66
|
-
let merged = "";
|
|
67
|
-
for (const file of files) {
|
|
68
|
-
const content = readFileSync(file, "utf-8");
|
|
69
|
-
imported.add(file);
|
|
70
|
-
merged += resolveImports(content, paths, imported) + "\n";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
let css = compileString(merged, paths, { ...this._variables });
|
|
74
|
-
|
|
75
|
-
if (minify) {
|
|
76
|
-
css = css.replace(/\/\*.*?\*\//gs, "");
|
|
77
|
-
css = css.replace(/\s+/g, " ");
|
|
78
|
-
css = css.replace(/\s*([{}:;,])\s*/g, "$1");
|
|
79
|
-
css = css.replace(/;}/g, "}");
|
|
80
|
-
css = css.trim();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Write output only if content changed (avoids triggering DevReload loops)
|
|
84
|
-
const absOutput = resolve(output);
|
|
85
|
-
const outDir = dirname(absOutput);
|
|
86
|
-
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
87
|
-
let existing: string | null = null;
|
|
88
|
-
try {
|
|
89
|
-
existing = existsSync(absOutput) ? readFileSync(absOutput, "utf-8") : null;
|
|
90
|
-
} catch {
|
|
91
|
-
existing = null;
|
|
92
|
-
}
|
|
93
|
-
if (existing !== css) {
|
|
94
|
-
writeFileSync(absOutput, css, "utf-8");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return css;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ── Internal Compilation Pipeline ────────────────────────────────
|
|
102
|
-
|
|
103
|
-
function compileString(
|
|
104
|
-
scss: string,
|
|
105
|
-
importPaths: string[],
|
|
106
|
-
variables: Record<string, string>
|
|
107
|
-
): string {
|
|
108
|
-
// 1. Resolve @import statements
|
|
109
|
-
const imported = new Set<string>();
|
|
110
|
-
scss = resolveImports(scss, importPaths, imported);
|
|
111
|
-
|
|
112
|
-
// 2. Strip single-line comments (preserve /* */ block comments)
|
|
113
|
-
scss = scss.replace(/(?<![:"'])\/\/[^\n]*/g, "");
|
|
114
|
-
|
|
115
|
-
// 3. Extract and store variables
|
|
116
|
-
scss = extractVariables(scss, variables);
|
|
117
|
-
|
|
118
|
-
// 4. Extract mixins
|
|
119
|
-
const mixins: Record<string, { params: string[]; body: string }> = {};
|
|
120
|
-
scss = extractMixins(scss, mixins);
|
|
121
|
-
|
|
122
|
-
// 5. Resolve @include
|
|
123
|
-
scss = resolveIncludes(scss, mixins);
|
|
124
|
-
|
|
125
|
-
// 5.5. Resolve #{ ... } interpolation (before $var substitution + nesting).
|
|
126
|
-
scss = resolveInterpolation(scss, variables);
|
|
127
|
-
|
|
128
|
-
// 6. Substitute variables
|
|
129
|
-
scss = substituteVariables(scss, variables);
|
|
130
|
-
|
|
131
|
-
// 7. Evaluate basic math in property values
|
|
132
|
-
scss = evalMath(scss);
|
|
133
|
-
|
|
134
|
-
// 7.5. Resolve color functions (lighten/darken/rgba/rgb/mix) — after variable
|
|
135
|
-
// substitution so a $colour arg is already a hex literal (issue #124).
|
|
136
|
-
scss = resolveColorFunctions(scss);
|
|
137
|
-
|
|
138
|
-
// 8. Flatten nested rules
|
|
139
|
-
const css = flattenNesting(scss);
|
|
140
|
-
|
|
141
|
-
// 9. Cleanup
|
|
142
|
-
return cleanup(css);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── Import Resolution ────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
function resolveImports(content: string, paths: string[], imported: Set<string>): string {
|
|
148
|
-
return content.replace(/@import\s+["']?([^"';\n]+)["']?\s*;/g, (_match, name: string) => {
|
|
149
|
-
name = name.trim();
|
|
150
|
-
const candidates: string[] = [];
|
|
151
|
-
for (const base of paths) {
|
|
152
|
-
candidates.push(
|
|
153
|
-
join(base, `${name}.scss`),
|
|
154
|
-
join(base, `_${name}.scss`),
|
|
155
|
-
join(base, name),
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
for (const candidate of candidates) {
|
|
159
|
-
if (existsSync(candidate) && !imported.has(candidate)) {
|
|
160
|
-
imported.add(candidate);
|
|
161
|
-
const fileContent = readFileSync(candidate, "utf-8");
|
|
162
|
-
return resolveImports(fileContent, [dirname(candidate), ...paths], imported);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return `/* IMPORT NOT FOUND: ${name} */`;
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ── Variables ────────────────────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Flags that may trail a variable declaration's value. `!default` means "assign
|
|
173
|
-
* only if this variable is not already set" — the flag that makes a variable
|
|
174
|
-
* themeable. `!global` is the scope flag. Both are compiler directives: they are
|
|
175
|
-
* consumed at the declaration and must never reach the CSS, because
|
|
176
|
-
* `padding: 1.5rem !default` is invalid CSS and browsers drop the whole
|
|
177
|
-
* declaration. Sass flag names are case-SENSITIVE (`!DEFAULT` is an error in
|
|
178
|
-
* Dart Sass), so the match is deliberately case-sensitive.
|
|
179
|
-
*/
|
|
180
|
-
const VARIABLE_FLAG = /\s*!(default|global)\s*$/;
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Split trailing `!default` / `!global` flags off a variable declaration value.
|
|
184
|
-
* Returns `[valueWithoutFlags, declaresDefault]`.
|
|
185
|
-
*
|
|
186
|
-
* Only ever called on the value of a `$name: value;` declaration, so a literal
|
|
187
|
-
* `!default` anywhere else — inside a quoted string (`content: "x !default y"`)
|
|
188
|
-
* or a function argument — is left untouched, exactly as Dart Sass leaves it. A
|
|
189
|
-
* blanket strip would corrupt real string content, and would silently turn
|
|
190
|
-
* `rgba(#000 !default, 0.1)` (a syntax error in Dart Sass) into valid-looking
|
|
191
|
-
* CSS that Sass would never emit.
|
|
192
|
-
*/
|
|
193
|
-
function stripVariableFlags(value: string): [string, boolean] {
|
|
194
|
-
let declaresDefault = false;
|
|
195
|
-
for (;;) {
|
|
196
|
-
const match = VARIABLE_FLAG.exec(value);
|
|
197
|
-
if (match === null) return [value.trim(), declaresDefault];
|
|
198
|
-
if (match[1] === "default") declaresDefault = true;
|
|
199
|
-
value = value.slice(0, match.index);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Extract `$variable: value;` declarations, honouring the `!default` flag.
|
|
205
|
-
*
|
|
206
|
-
* `$x: value !default;` assigns only when `$x` is not already set. That is what
|
|
207
|
-
* makes a variable themeable — a user who writes `$primary: red;` BEFORE
|
|
208
|
-
* importing a partial that declares `$primary: blue !default;` keeps red.
|
|
209
|
-
* Declarations are visited in source order, so "already set" means "set by an
|
|
210
|
-
* earlier declaration or by a preset variable". A value of `null` counts as
|
|
211
|
-
* unset, as in Sass.
|
|
212
|
-
*/
|
|
213
|
-
function extractVariables(scss: string, variables: Record<string, string>): string {
|
|
214
|
-
return scss.replace(/\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/g, (_m, name: string, value: string) => {
|
|
215
|
-
const [stripped, declaresDefault] = stripVariableFlags(value.trim());
|
|
216
|
-
// !default must not overwrite a value that is already set.
|
|
217
|
-
if (declaresDefault && (variables[name] ?? "null") !== "null") {
|
|
218
|
-
return "";
|
|
219
|
-
}
|
|
220
|
-
let resolved = stripped;
|
|
221
|
-
// Resolve variable references within the value
|
|
222
|
-
for (const [vName, vVal] of Object.entries(variables)) {
|
|
223
|
-
resolved = resolved.replaceAll(`$${vName}`, vVal);
|
|
224
|
-
}
|
|
225
|
-
variables[name] = resolved;
|
|
226
|
-
return "";
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function substituteVariables(scss: string, variables: Record<string, string>): string {
|
|
231
|
-
// Sort by longest name first to avoid partial matches
|
|
232
|
-
const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
|
|
233
|
-
for (const name of sorted) {
|
|
234
|
-
scss = scss.replaceAll(`$${name}`, variables[name]);
|
|
235
|
-
}
|
|
236
|
-
return scss;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Resolve SCSS `#{ ... }` interpolation. Each `#{ expr }` is replaced by its
|
|
241
|
-
* resolved inner text: a `$variable` inside the braces resolves to its value,
|
|
242
|
-
* anything else is inlined verbatim (trimmed). This lets a value carry a
|
|
243
|
-
* variable inside a string context plain `$var` substitution can't reach —
|
|
244
|
-
* e.g. `calc(100% - #{$gap})` → `calc(100% - 20px)` — and lets a variable
|
|
245
|
-
* appear in a selector (`.icon-#{$name}` → `.icon-home`). Run BEFORE nested
|
|
246
|
-
* rule flattening so the literal braces never confuse the block matcher.
|
|
247
|
-
*/
|
|
248
|
-
function resolveInterpolation(scss: string, variables: Record<string, string>): string {
|
|
249
|
-
const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
|
|
250
|
-
return scss.replace(/#\{([^{}]*)\}/g, (_m, inner: string) => {
|
|
251
|
-
let resolved = inner.trim();
|
|
252
|
-
for (const name of sorted) {
|
|
253
|
-
resolved = resolved.replaceAll(`$${name}`, variables[name]);
|
|
254
|
-
}
|
|
255
|
-
return resolved;
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ── Mixins ───────────────────────────────────────────────────────
|
|
260
|
-
|
|
261
|
-
function extractMixins(
|
|
262
|
-
scss: string,
|
|
263
|
-
mixins: Record<string, { params: string[]; body: string }>
|
|
264
|
-
): string {
|
|
265
|
-
const pattern = /@mixin\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*\{/g;
|
|
266
|
-
let match: RegExpExecArray | null;
|
|
267
|
-
const locations: { start: number; end: number; name: string }[] = [];
|
|
268
|
-
|
|
269
|
-
while ((match = pattern.exec(scss)) !== null) {
|
|
270
|
-
const name = match[1];
|
|
271
|
-
const paramsStr = match[2] ?? "";
|
|
272
|
-
const params = paramsStr
|
|
273
|
-
.split(",")
|
|
274
|
-
.map((p) => p.trim().replace(/^\$/, ""))
|
|
275
|
-
.filter(Boolean);
|
|
276
|
-
|
|
277
|
-
const bodyStart = match.index + match[0].length;
|
|
278
|
-
const body = findBlock(scss, bodyStart);
|
|
279
|
-
if (body !== null) {
|
|
280
|
-
mixins[name] = { params, body };
|
|
281
|
-
locations.push({
|
|
282
|
-
start: match.index,
|
|
283
|
-
end: bodyStart + body.length + 1,
|
|
284
|
-
name,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Remove mixin definitions from source (reverse order to preserve indices)
|
|
290
|
-
let result = scss;
|
|
291
|
-
for (const loc of locations.reverse()) {
|
|
292
|
-
result = result.slice(0, loc.start) + result.slice(loc.end);
|
|
293
|
-
}
|
|
294
|
-
return result;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function resolveIncludes(
|
|
298
|
-
scss: string,
|
|
299
|
-
mixins: Record<string, { params: string[]; body: string }>
|
|
300
|
-
): string {
|
|
301
|
-
return scss.replace(
|
|
302
|
-
/@include\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*;/g,
|
|
303
|
-
(_m, name: string, argsStr: string | undefined) => {
|
|
304
|
-
if (!(name in mixins)) {
|
|
305
|
-
return `/* MIXIN NOT FOUND: ${name} */`;
|
|
306
|
-
}
|
|
307
|
-
const mixin = mixins[name];
|
|
308
|
-
const args = argsStr
|
|
309
|
-
? argsStr.split(",").map((a) => a.trim()).filter(Boolean)
|
|
310
|
-
: [];
|
|
311
|
-
let body = mixin.body;
|
|
312
|
-
for (let i = 0; i < mixin.params.length; i++) {
|
|
313
|
-
const paramName = mixin.params[i].split(":")[0].trim();
|
|
314
|
-
const defaultVal = mixin.params[i].includes(":")
|
|
315
|
-
? mixin.params[i].split(":").slice(1).join(":").trim()
|
|
316
|
-
: "";
|
|
317
|
-
const value = i < args.length ? args[i] : defaultVal;
|
|
318
|
-
body = body.replaceAll(`$${paramName}`, value);
|
|
319
|
-
}
|
|
320
|
-
return body;
|
|
321
|
-
}
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ── Math Evaluation ──────────────────────────────────────────────
|
|
326
|
-
|
|
327
|
-
function evalMath(scss: string): string {
|
|
328
|
-
// Mixed-unit arithmetic is left verbatim — that is exactly what CSS calc()
|
|
329
|
-
// is for, and folding it silently produces invalid output (see tina4-nodejs#1).
|
|
330
|
-
// Math inside calc(...) is preserved untouched on the same principle: the
|
|
331
|
-
// author asked the browser to compute it.
|
|
332
|
-
//
|
|
333
|
-
// Rules for folding:
|
|
334
|
-
// * Both operands unitless → fold
|
|
335
|
-
// * Same unit on both operands → fold, keep unit
|
|
336
|
-
// * One operand unitless for * or / → fold, keep the other unit
|
|
337
|
-
// * Anything else (mixed units on +/-, etc) → leave verbatim
|
|
338
|
-
|
|
339
|
-
// Step 1 — mask calc(...) ranges so the math regex cannot eat into them.
|
|
340
|
-
const placeholders: string[] = [];
|
|
341
|
-
const masked = scss.replace(/calc\([^()]*\)/g, (m) => {
|
|
342
|
-
placeholders.push(m);
|
|
343
|
-
return `\x00CALC${placeholders.length - 1}\x00`;
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
// Step 2 — run the math fold on what remains.
|
|
347
|
-
const folded = masked.replace(
|
|
348
|
-
/([\d.]+)([a-z%]*)\s*([+\-*/])\s*([\d.]+)([a-z%]*)/g,
|
|
349
|
-
(full, n1: string, u1: string, op: string, n2: string, u2: string) => {
|
|
350
|
-
const num1 = parseFloat(n1);
|
|
351
|
-
const num2 = parseFloat(n2);
|
|
352
|
-
if (Number.isNaN(num1) || Number.isNaN(num2)) return full;
|
|
353
|
-
|
|
354
|
-
const unit1 = u1 || "";
|
|
355
|
-
const unit2 = u2 || "";
|
|
356
|
-
|
|
357
|
-
// Decide result unit; bail if units are incompatible.
|
|
358
|
-
let unit: string;
|
|
359
|
-
if (unit1 === unit2) {
|
|
360
|
-
unit = unit1;
|
|
361
|
-
} else if ((op === "*" || op === "/") && unit1 === "") {
|
|
362
|
-
unit = unit2;
|
|
363
|
-
} else if ((op === "*" || op === "/") && unit2 === "") {
|
|
364
|
-
unit = unit1;
|
|
365
|
-
} else {
|
|
366
|
-
return full;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
let result: number;
|
|
370
|
-
switch (op) {
|
|
371
|
-
case "+": result = num1 + num2; break;
|
|
372
|
-
case "-": result = num1 - num2; break;
|
|
373
|
-
case "*": result = num1 * num2; break;
|
|
374
|
-
case "/":
|
|
375
|
-
if (num2 === 0) return full;
|
|
376
|
-
result = num1 / num2;
|
|
377
|
-
break;
|
|
378
|
-
default: return full;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (result === Math.floor(result)) {
|
|
382
|
-
return `${Math.floor(result)}${unit}`;
|
|
383
|
-
}
|
|
384
|
-
return `${result.toFixed(2)}${unit}`;
|
|
385
|
-
}
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
// Step 3 — restore the calc() ranges verbatim.
|
|
389
|
-
return folded.replace(/\x00CALC(\d+)\x00/g, (_m, idx: string) => {
|
|
390
|
-
return placeholders[parseInt(idx, 10)];
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ── Color Functions ──────────────────────────────────────────────
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Resolve lighten(), darken(), rgba()/rgb(), and mix() color functions.
|
|
398
|
-
*
|
|
399
|
-
* rgba(<hex>, <alpha>) is the damaging case (issue #124): the functional rgba()
|
|
400
|
-
* notation cannot take a hex, so rgba(#0f3460, 0.12) is invalid CSS and browsers
|
|
401
|
-
* drop the whole declaration. Convert the hex to its r,g,b components. Runs after
|
|
402
|
-
* variable substitution so a $colour arg is already a hex literal.
|
|
403
|
-
*/
|
|
404
|
-
function resolveColorFunctions(scss: string): string {
|
|
405
|
-
scss = scss.replace(/lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g, (_m, color: string, amt: string) =>
|
|
406
|
-
adjustLightness(color.trim(), parseFloat(amt.trim().replace(/%$/, "")) / 100)
|
|
407
|
-
);
|
|
408
|
-
scss = scss.replace(/darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g, (_m, color: string, amt: string) =>
|
|
409
|
-
adjustLightness(color.trim(), -(parseFloat(amt.trim().replace(/%$/, "")) / 100))
|
|
410
|
-
);
|
|
411
|
-
// rgba(<hex>, <alpha>) — only the two-arg hex form; leave rgba(r,g,b,a) alone.
|
|
412
|
-
scss = scss.replace(/rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)/g, (whole, hex: string, alpha: string) => {
|
|
413
|
-
const rgb = hexToRgb(hex);
|
|
414
|
-
return rgb === null ? whole : `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha.trim()})`;
|
|
415
|
-
});
|
|
416
|
-
scss = scss.replace(/rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)/g, (whole, hex: string) => {
|
|
417
|
-
const rgb = hexToRgb(hex);
|
|
418
|
-
return rgb === null ? whole : `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
|
|
419
|
-
});
|
|
420
|
-
// mix(<c1>, <c2>[, <weight>]) — Sass weight is c1's proportion (default 50%).
|
|
421
|
-
scss = scss.replace(
|
|
422
|
-
/mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)/g,
|
|
423
|
-
(whole, h1: string, h2: string, weight?: string) => {
|
|
424
|
-
const c1 = hexToRgb(h1);
|
|
425
|
-
const c2 = hexToRgb(h2);
|
|
426
|
-
if (c1 === null || c2 === null) return whole;
|
|
427
|
-
const w = weight ? parseFloat(weight.replace(/%$/, "")) / 100 : 0.5;
|
|
428
|
-
const mixed = [0, 1, 2].map((i) => Math.round(c1[i] * w + c2[i] * (1 - w)));
|
|
429
|
-
return `#${mixed.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
430
|
-
}
|
|
431
|
-
);
|
|
432
|
-
return scss;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** Parse a #rgb / #rrggbb hex string into an [r, g, b] tuple, or null. */
|
|
436
|
-
function hexToRgb(color: string): [number, number, number] | null {
|
|
437
|
-
let c = color.trim().replace(/^#/, "");
|
|
438
|
-
if (c.length === 3) {
|
|
439
|
-
c = c.split("").map((ch) => ch + ch).join("");
|
|
440
|
-
}
|
|
441
|
-
if (!/^[0-9a-fA-F]{6}$/.test(c)) return null;
|
|
442
|
-
return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** Adjust the HSL lightness of a hex color by `amount` (-1..1), return hex.
|
|
446
|
-
* Truncates (Math.trunc) to match the Python master's int(x*255) byte-for-byte. */
|
|
447
|
-
function adjustLightness(color: string, amount: number): string {
|
|
448
|
-
const rgb = hexToRgb(color);
|
|
449
|
-
if (rgb === null) return color;
|
|
450
|
-
let [r, g, b] = rgb.map((v) => v / 255) as [number, number, number];
|
|
451
|
-
const max = Math.max(r, g, b);
|
|
452
|
-
const min = Math.min(r, g, b);
|
|
453
|
-
let l = (max + min) / 2;
|
|
454
|
-
const d = max - min;
|
|
455
|
-
let h = 0;
|
|
456
|
-
let s = 0;
|
|
457
|
-
if (d !== 0) {
|
|
458
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
459
|
-
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
|
460
|
-
else if (max === g) h = (b - r) / d + 2;
|
|
461
|
-
else h = (r - g) / d + 4;
|
|
462
|
-
h /= 6;
|
|
463
|
-
}
|
|
464
|
-
l = Math.max(0, Math.min(1, l + amount));
|
|
465
|
-
if (s === 0) {
|
|
466
|
-
r = g = b = l;
|
|
467
|
-
} else {
|
|
468
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
469
|
-
const p = 2 * l - q;
|
|
470
|
-
r = hueToRgb(p, q, h + 1 / 3);
|
|
471
|
-
g = hueToRgb(p, q, h);
|
|
472
|
-
b = hueToRgb(p, q, h - 1 / 3);
|
|
473
|
-
}
|
|
474
|
-
const hex = (v: number): string => Math.trunc(v * 255).toString(16).padStart(2, "0");
|
|
475
|
-
return `#${hex(r)}${hex(g)}${hex(b)}`;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function hueToRgb(p: number, q: number, t: number): number {
|
|
479
|
-
if (t < 0) t += 1;
|
|
480
|
-
if (t > 1) t -= 1;
|
|
481
|
-
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
482
|
-
if (t < 1 / 2) return q;
|
|
483
|
-
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
484
|
-
return p;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// ── Nesting Flattener ────────────────────────────────────────────
|
|
488
|
-
|
|
489
|
-
function flattenNesting(scss: string): string {
|
|
490
|
-
const output: string[] = [];
|
|
491
|
-
flattenBlock(scss, [], output);
|
|
492
|
-
return output.join("\n");
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function flattenBlock(content: string, parentSelectors: string[], output: string[]): void {
|
|
496
|
-
let pos = 0;
|
|
497
|
-
const properties: string[] = [];
|
|
498
|
-
|
|
499
|
-
while (pos < content.length) {
|
|
500
|
-
// Skip whitespace
|
|
501
|
-
while (pos < content.length && /[\s]/.test(content[pos])) {
|
|
502
|
-
pos++;
|
|
503
|
-
}
|
|
504
|
-
if (pos >= content.length) break;
|
|
505
|
-
|
|
506
|
-
// Block comment — preserve
|
|
507
|
-
if (content[pos] === "/" && content[pos + 1] === "*") {
|
|
508
|
-
const end = content.indexOf("*/", pos + 2);
|
|
509
|
-
if (end === -1) break;
|
|
510
|
-
output.push(content.slice(pos, end + 2));
|
|
511
|
-
pos = end + 2;
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// @media query — special handling
|
|
516
|
-
if (content.slice(pos, pos + 6) === "@media") {
|
|
517
|
-
const brace = content.indexOf("{", pos);
|
|
518
|
-
if (brace === -1) break;
|
|
519
|
-
const mediaQuery = content.slice(pos, brace).trim();
|
|
520
|
-
const body = findBlock(content, brace + 1);
|
|
521
|
-
if (body === null) break;
|
|
522
|
-
pos = brace + 1 + body.length + 1;
|
|
523
|
-
|
|
524
|
-
const innerOutput: string[] = [];
|
|
525
|
-
flattenBlock(body, parentSelectors, innerOutput);
|
|
526
|
-
if (innerOutput.length > 0) {
|
|
527
|
-
output.push(`${mediaQuery} {`);
|
|
528
|
-
for (const line of innerOutput) {
|
|
529
|
-
output.push(` ${line}`);
|
|
530
|
-
}
|
|
531
|
-
output.push("}");
|
|
532
|
-
}
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Find next { or ;
|
|
537
|
-
const bracePos = content.indexOf("{", pos);
|
|
538
|
-
const semiPos = content.indexOf(";", pos);
|
|
539
|
-
|
|
540
|
-
// Property (has ; before { or no { at all)
|
|
541
|
-
if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
|
|
542
|
-
const prop = content.slice(pos, semiPos).trim();
|
|
543
|
-
if (prop && !prop.startsWith("@")) {
|
|
544
|
-
properties.push(prop);
|
|
545
|
-
}
|
|
546
|
-
pos = semiPos + 1;
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Nested block
|
|
551
|
-
if (bracePos !== -1) {
|
|
552
|
-
const selectorText = content.slice(pos, bracePos).trim();
|
|
553
|
-
const body = findBlock(content, bracePos + 1);
|
|
554
|
-
if (body === null) break;
|
|
555
|
-
pos = bracePos + 1 + body.length + 1;
|
|
556
|
-
|
|
557
|
-
if (!selectorText) continue;
|
|
558
|
-
|
|
559
|
-
// Expand selectors with parent reference (&)
|
|
560
|
-
const selectors = selectorText.split(",").map((s) => s.trim());
|
|
561
|
-
const newSelectors: string[] = [];
|
|
562
|
-
for (const sel of selectors) {
|
|
563
|
-
if (parentSelectors.length > 0) {
|
|
564
|
-
for (const parent of parentSelectors) {
|
|
565
|
-
if (sel.includes("&")) {
|
|
566
|
-
newSelectors.push(sel.replace(/&/g, parent));
|
|
567
|
-
} else {
|
|
568
|
-
newSelectors.push(`${parent} ${sel}`);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
} else {
|
|
572
|
-
newSelectors.push(sel);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
flattenBlock(body, newSelectors, output);
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Remaining text — treat as property
|
|
581
|
-
const remaining = content.slice(pos).trim();
|
|
582
|
-
if (remaining) {
|
|
583
|
-
properties.push(remaining);
|
|
584
|
-
}
|
|
585
|
-
break;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Emit properties for current selector
|
|
589
|
-
if (properties.length > 0 && parentSelectors.length > 0) {
|
|
590
|
-
const selectorStr = parentSelectors.join(", ");
|
|
591
|
-
output.push(`${selectorStr} {`);
|
|
592
|
-
for (const prop of properties) {
|
|
593
|
-
output.push(` ${prop};`);
|
|
594
|
-
}
|
|
595
|
-
output.push("}");
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// ── Utilities ────────────────────────────────────────────────────
|
|
600
|
-
|
|
601
|
-
function findBlock(content: string, start: number): string | null {
|
|
602
|
-
let depth = 1;
|
|
603
|
-
let pos = start;
|
|
604
|
-
while (pos < content.length && depth > 0) {
|
|
605
|
-
if (content[pos] === "{") depth++;
|
|
606
|
-
else if (content[pos] === "}") depth--;
|
|
607
|
-
if (depth > 0) pos++;
|
|
608
|
-
}
|
|
609
|
-
return depth === 0 ? content.slice(start, pos) : null;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function cleanup(css: string): string {
|
|
613
|
-
// Remove empty rulesets
|
|
614
|
-
css = css.replace(/[^{}]+\{\s*\}/g, "");
|
|
615
|
-
// Remove multiple blank lines
|
|
616
|
-
css = css.replace(/\n{3,}/g, "\n\n");
|
|
617
|
-
// Remove trailing whitespace per line
|
|
618
|
-
css = css
|
|
619
|
-
.split("\n")
|
|
620
|
-
.map((line) => line.trimEnd())
|
|
621
|
-
.join("\n");
|
|
622
|
-
return css.trim() + "\n";
|
|
623
|
-
}
|
package/types/core/src/scss.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export interface ScssConfig {
|
|
2
|
-
importPaths?: string[];
|
|
3
|
-
variables?: Record<string, string>;
|
|
4
|
-
}
|
|
5
|
-
export declare class ScssCompiler {
|
|
6
|
-
private _importPaths;
|
|
7
|
-
private _variables;
|
|
8
|
-
constructor(config?: ScssConfig);
|
|
9
|
-
/** Compile an SCSS string to CSS. */
|
|
10
|
-
compile(source: string): string;
|
|
11
|
-
/** Compile an SCSS file to CSS. */
|
|
12
|
-
compileFile(filePath: string): string;
|
|
13
|
-
/** Add a directory to the import resolution path. */
|
|
14
|
-
addImportPath(path: string): void;
|
|
15
|
-
/** Set or override an SCSS variable. */
|
|
16
|
-
setVariable(name: string, value: string): void;
|
|
17
|
-
/** Compile all .scss files in a directory into a single CSS output file. */
|
|
18
|
-
compileScss(scssDir?: string, output?: string, minify?: boolean): string;
|
|
19
|
-
}
|