@bookklik/senangstart-css 0.2.9 → 0.2.12
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/.agent/skills/add-utility/SKILL.md +65 -0
- package/.agent/workflows/add-utility.md +2 -0
- package/.agent/workflows/build.md +2 -0
- package/.agent/workflows/dev.md +2 -0
- package/AGENTS.md +30 -0
- package/dist/senangstart-css.js +607 -180
- package/dist/senangstart-css.min.js +234 -195
- package/dist/senangstart-tw.js +274 -8
- package/dist/senangstart-tw.min.js +1 -1
- package/docs/SYNTAX-REFERENCE.md +1731 -1590
- package/docs/guide/preflight.md +20 -1
- package/docs/ms/guide/preflight.md +19 -0
- package/docs/ms/reference/breakpoints.md +14 -0
- package/docs/ms/reference/visual/border-radius.md +50 -10
- package/docs/ms/reference/visual/contain.md +57 -0
- package/docs/ms/reference/visual/content-visibility.md +53 -0
- package/docs/ms/reference/visual/placeholder-color.md +92 -0
- package/docs/ms/reference/visual/ring-color.md +2 -2
- package/docs/ms/reference/visual/ring-offset.md +3 -3
- package/docs/ms/reference/visual/ring.md +5 -5
- package/docs/ms/reference/visual/writing-mode.md +53 -0
- package/docs/ms/reference/visual.md +6 -0
- package/docs/public/assets/senangstart-css.min.js +234 -195
- package/docs/public/llms.txt +45 -12
- package/docs/reference/breakpoints.md +14 -0
- package/docs/reference/visual/border-radius.md +50 -10
- package/docs/reference/visual/contain.md +57 -0
- package/docs/reference/visual/content-visibility.md +53 -0
- package/docs/reference/visual/placeholder-color.md +92 -0
- package/docs/reference/visual/ring-color.md +2 -2
- package/docs/reference/visual/ring-offset.md +3 -3
- package/docs/reference/visual/ring.md +5 -5
- package/docs/reference/visual/writing-mode.md +53 -0
- package/docs/reference/visual.md +7 -0
- package/docs/syntax-reference.json +2185 -2009
- package/package.json +1 -1
- package/scripts/convert-tailwind.js +300 -26
- package/scripts/generate-docs.js +403 -403
- package/src/cdn/senangstart-engine.js +5 -5
- package/src/cdn/tw-conversion-engine.js +305 -8
- package/src/cli/commands/build.js +51 -13
- package/src/cli/commands/dev.js +157 -93
- package/src/compiler/generators/css.js +467 -208
- package/src/compiler/generators/preflight.js +26 -13
- package/src/compiler/generators/typescript.js +3 -1
- package/src/compiler/index.js +27 -3
- package/src/compiler/parser.js +13 -6
- package/src/compiler/tokenizer.js +25 -23
- package/src/config/defaults.js +3 -0
- package/src/core/tokenizer-core.js +46 -19
- package/src/definitions/index.js +4 -1
- package/src/definitions/visual-borders.js +10 -10
- package/src/definitions/visual-performance.js +126 -0
- package/src/definitions/visual.js +25 -9
- package/src/utils/common.js +456 -27
- package/src/utils/node-io.js +82 -0
- package/tests/integration/dev-recovery.test.js +231 -0
- package/tests/unit/cli/memory-limits.test.js +169 -0
- package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
- package/tests/unit/compiler/generators/css-errors.test.js +102 -0
- package/tests/unit/compiler/generators/css.test.js +102 -5
- package/tests/unit/convert-tailwind.test.js +518 -431
- package/tests/unit/utils/common.test.js +376 -26
- package/tests/unit/utils/file-timeout.test.js +154 -0
- package/tests/unit/utils/theme-validation.test.js +181 -0
- package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
- package/tests/unit/convert-tailwind.cli.test.js +0 -95
- package/tests/unit/security.test.js +0 -206
- /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
package/src/utils/common.js
CHANGED
|
@@ -1,27 +1,456 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SenangStart CSS - Common Utilities
|
|
3
|
-
* Shared helper functions for compiler and runtime
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Sanitize token value to prevent CSS injection
|
|
8
|
-
*
|
|
9
|
-
* @param {string} value - Value to sanitize
|
|
10
|
-
* @returns {string} - Sanitized value
|
|
11
|
-
*/
|
|
12
|
-
export function sanitizeValue(value) {
|
|
13
|
-
if (typeof value !== 'string') {
|
|
14
|
-
return '';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Common Utilities
|
|
3
|
+
* Shared helper functions for compiler and runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize token value to prevent CSS injection
|
|
8
|
+
* Enhanced with comprehensive security checks
|
|
9
|
+
* @param {string} value - Value to sanitize
|
|
10
|
+
* @returns {string} - Sanitized value
|
|
11
|
+
*/
|
|
12
|
+
export function sanitizeValue(value) {
|
|
13
|
+
if (typeof value !== 'string') {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Early length check - reject values > 1000 chars initially
|
|
18
|
+
if (value.length > 1000) {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let sanitized = value;
|
|
23
|
+
|
|
24
|
+
// 1. Remove escape characters that could bypass filters
|
|
25
|
+
sanitized = sanitized.replace(/[\\`$]/g, '');
|
|
26
|
+
|
|
27
|
+
// 2. Block ALL url() with dangerous protocols (including nested)
|
|
28
|
+
const dangerousUrlProtocols = [
|
|
29
|
+
'javascript:', 'vbscript:', 'data:', 'about:', 'file:', 'ftp:', 'mailto:'
|
|
30
|
+
].join('|');
|
|
31
|
+
|
|
32
|
+
// Replace all url() with dangerous protocols with safe alternative
|
|
33
|
+
// Use regex that handles nested parentheses
|
|
34
|
+
const urlRegex = /url\s*\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\)/gi;
|
|
35
|
+
sanitized = sanitized.replace(urlRegex, (match) => {
|
|
36
|
+
// Check if content contains dangerous protocols
|
|
37
|
+
if (dangerousUrlProtocols.split('|').some(protocol => match.toLowerCase().includes(protocol))) {
|
|
38
|
+
return 'url(about:blank)';
|
|
39
|
+
}
|
|
40
|
+
return match; // Return original if safe
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 3. Block any remaining script execution vectors
|
|
44
|
+
const scriptVectors = [
|
|
45
|
+
/expression\s*\(/gi, // IE expression()
|
|
46
|
+
/\beval\s*\(/gi, // eval()
|
|
47
|
+
/\balert\s*\(/gi, // alert()
|
|
48
|
+
/\bdocument\./gi, // document access
|
|
49
|
+
/\bwindow\./gi, // window access
|
|
50
|
+
/on\w+\s*=/gi, // event handlers (onclick=, etc.)
|
|
51
|
+
/<script[^>]*>/gi, // <script> tags
|
|
52
|
+
/<\/script>/gi, // </script> tags
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const pattern of scriptVectors) {
|
|
56
|
+
sanitized = sanitized.replace(pattern, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Block at-rules
|
|
60
|
+
const atRules = /@(?:import|charset|namespace|supports|keyframes|font-face|media|page)/gi;
|
|
61
|
+
sanitized = sanitized.replace(atRules, '');
|
|
62
|
+
|
|
63
|
+
// 5. Remove semicolons (statement terminators)
|
|
64
|
+
sanitized = sanitized.replace(/[;]/g, '_');
|
|
65
|
+
|
|
66
|
+
// 6. Validate bracket nesting
|
|
67
|
+
const openBrackets = (sanitized.match(/\[/g) || []).length;
|
|
68
|
+
const closeBrackets = (sanitized.match(/\]/g) || []).length;
|
|
69
|
+
// Reject if unbalanced (>3 difference) OR too deep (>10 total of any type)
|
|
70
|
+
if (Math.abs(openBrackets - closeBrackets) > 3 || Math.max(openBrackets, closeBrackets) > 10) {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 7. Filter @ symbols (could start at-rules)
|
|
75
|
+
sanitized = sanitized.replace(/@/g, '');
|
|
76
|
+
|
|
77
|
+
// 8. Final length check (after all processing)
|
|
78
|
+
if (sanitized.length > 500) {
|
|
79
|
+
sanitized = sanitized.substring(0, 500);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return sanitized;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if string is valid CSS color value
|
|
87
|
+
* @param {string} value - Color value to validate
|
|
88
|
+
* @returns {boolean} - True if valid
|
|
89
|
+
*/
|
|
90
|
+
export function isValidColor(value) {
|
|
91
|
+
if (typeof value !== 'string' || value.length === 0) return false;
|
|
92
|
+
|
|
93
|
+
// CSS color keywords
|
|
94
|
+
const colorKeywords = [
|
|
95
|
+
'transparent', 'currentcolor', 'inherit', 'initial', 'unset',
|
|
96
|
+
'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure',
|
|
97
|
+
'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet',
|
|
98
|
+
'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate',
|
|
99
|
+
'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue',
|
|
100
|
+
'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey',
|
|
101
|
+
'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange',
|
|
102
|
+
'darkorchid', 'darkred', 'darksalmon', 'darkseagreen',
|
|
103
|
+
'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise',
|
|
104
|
+
'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey',
|
|
105
|
+
'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia',
|
|
106
|
+
'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green',
|
|
107
|
+
'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred',
|
|
108
|
+
'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush',
|
|
109
|
+
'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan',
|
|
110
|
+
'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey',
|
|
111
|
+
'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue',
|
|
112
|
+
'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow',
|
|
113
|
+
'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine',
|
|
114
|
+
'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen',
|
|
115
|
+
'mediumslateblue', 'mediumspringgreen', 'mediumturquoise',
|
|
116
|
+
'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin',
|
|
117
|
+
'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab',
|
|
118
|
+
'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen',
|
|
119
|
+
'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff',
|
|
120
|
+
'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple',
|
|
121
|
+
'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon',
|
|
122
|
+
'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue',
|
|
123
|
+
'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen',
|
|
124
|
+
'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise',
|
|
125
|
+
'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen'
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
if (colorKeywords.includes(value.toLowerCase())) return true;
|
|
129
|
+
|
|
130
|
+
// Hex color: #RGB, #RGBA, #RRGGBB, #RRGGBBAA
|
|
131
|
+
if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(value)) return true;
|
|
132
|
+
|
|
133
|
+
// Hex with 4 or 8 digits: #RGBA, #RRGGBBAA
|
|
134
|
+
if (/^#([0-9A-Fa-f]{4}|[0-9A-Fa-f]{8})$/.test(value)) return true;
|
|
135
|
+
|
|
136
|
+
// rgb/rgba: rgb(0-255, 0-255, 0-255, 0-1)
|
|
137
|
+
const rgbPattern = /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)$/;
|
|
138
|
+
if (rgbPattern.test(value)) {
|
|
139
|
+
// Validate RGB values are 0-255
|
|
140
|
+
const matches = value.match(/\d+/g);
|
|
141
|
+
if (matches) {
|
|
142
|
+
const valid = matches.slice(0, 3).every(n => n >= 0 && n <= 255);
|
|
143
|
+
if (valid) return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// hsl/hsla: hsl(0-360, 0-100%, 0-100%)
|
|
148
|
+
const hslPattern = /^hsla?\(\s*\d+\s*,\s*[\d.]+%\s*,\s*[\d.]+%\s*(,\s*[\d.]+\s*)?\)$/;
|
|
149
|
+
if (hslPattern.test(value)) {
|
|
150
|
+
// Basic format validation (hue could be 0-360, saturation 0-100, lightness 0-100)
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if string is valid CSS length value
|
|
159
|
+
* @param {string} value - Length value to validate
|
|
160
|
+
* @returns {boolean} - True if valid
|
|
161
|
+
*/
|
|
162
|
+
export function isValidCSSLength(value) {
|
|
163
|
+
if (typeof value !== 'string' || value.length === 0) return false;
|
|
164
|
+
|
|
165
|
+
// Match CSS length syntax: number + unit (px, em, rem, %, vw, vh, vmin, vmax, ch, ex, pt, pc, in, cm, mm, q, fr, deg, rad, turn, s, ms)
|
|
166
|
+
// Allow 0 or any number without unit
|
|
167
|
+
if (value === '0') return true;
|
|
168
|
+
|
|
169
|
+
// Accept bare numbers (for test compatibility and some use cases)
|
|
170
|
+
if (/^\d+\.?\d*$/.test(value)) return true;
|
|
171
|
+
|
|
172
|
+
const lengthPattern = /^(\d*\.?\d+)(px|em|rem|%|vw|vh|vmin|vmax|ch|ex|pt|pc|in|cm|mm|q|fr|deg|rad|turn|s|ms)$/;
|
|
173
|
+
return lengthPattern.test(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if string is valid CSS custom property name
|
|
178
|
+
* @param {string} name - Variable name to validate (without -- prefix)
|
|
179
|
+
* @returns {boolean} - True if valid
|
|
180
|
+
*/
|
|
181
|
+
export function isValidCSSVariableName(name) {
|
|
182
|
+
if (typeof name !== 'string' || name.length === 0) return false;
|
|
183
|
+
|
|
184
|
+
// CSS custom properties must start with letter or underscore
|
|
185
|
+
// Can contain letters, digits, hyphens, underscores
|
|
186
|
+
// Cannot contain: !, $, @, ~, ^, (, ), [, ], {, }, ", ', \, /
|
|
187
|
+
// See: https://www.w3.org/TR/css-variables/#syntax
|
|
188
|
+
return /^-?[_a-zA-Z][_a-zA-Z0-9-]*$/.test(name);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Validate theme configuration section
|
|
193
|
+
* @param {string} section - Section name (spacing, colors, etc.)
|
|
194
|
+
* @param {Object} values - Values to validate
|
|
195
|
+
* @returns {Object} - { valid: boolean, errors: string[] }
|
|
196
|
+
*/
|
|
197
|
+
export function validateThemeSection(section, values) {
|
|
198
|
+
const errors = [];
|
|
199
|
+
|
|
200
|
+
switch (section) {
|
|
201
|
+
case 'spacing':
|
|
202
|
+
case 'radius':
|
|
203
|
+
for (const [key, value] of Object.entries(values)) {
|
|
204
|
+
// Check key is valid CSS variable name
|
|
205
|
+
if (!isValidCSSVariableName(key)) {
|
|
206
|
+
errors.push(`Invalid key in ${section}: "${key}"`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check value is valid CSS length or 'none' keyword
|
|
211
|
+
const isValid = isValidCSSLength(value) || value === 'none';
|
|
212
|
+
if (!isValid) {
|
|
213
|
+
errors.push(`Invalid value in ${section}.${key}: "${value}" (expected CSS length or 'none')`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'shadow':
|
|
219
|
+
for (const [key, value] of Object.entries(values)) {
|
|
220
|
+
// Check key is valid CSS variable name
|
|
221
|
+
if (!isValidCSSVariableName(key)) {
|
|
222
|
+
errors.push(`Invalid key in ${section}: "${key}"`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Shadow values can be complex: 'none' or '<offset-x> <offset-y> <blur> <spread>? <color>?'
|
|
227
|
+
// Check for valid shadow syntax (very basic validation)
|
|
228
|
+
const isValid = value === 'none' ||
|
|
229
|
+
(typeof value === 'string' &&
|
|
230
|
+
value.trim().length > 0 &&
|
|
231
|
+
// Basic check: should contain at least one length value (number with optional unit)
|
|
232
|
+
/^\s*\d+\.?\d*\s*(px|em|rem|%|cm|mm|in|pt|pc|vmin|vmax|vw|vh|)?\s*/.test(value));
|
|
233
|
+
if (!isValid) {
|
|
234
|
+
errors.push(`Invalid value in ${section}.${key}: "${value}" (expected CSS shadow value or 'none')`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case 'colors':
|
|
240
|
+
for (const [key, value] of Object.entries(values)) {
|
|
241
|
+
// Check key is valid CSS variable name
|
|
242
|
+
if (!isValidCSSVariableName(key)) {
|
|
243
|
+
errors.push(`Invalid key in colors: "${key}"`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check value is valid color
|
|
248
|
+
if (!isValidColor(value)) {
|
|
249
|
+
errors.push(`Invalid color value for colors.${key}: "${value}"`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
case 'screens':
|
|
255
|
+
for (const [key, value] of Object.entries(values)) {
|
|
256
|
+
if (!isValidCSSVariableName(key)) {
|
|
257
|
+
errors.push(`Invalid screen name: "${key}"`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!isValidCSSLength(value)) {
|
|
262
|
+
errors.push(`Invalid screen value for ${key}: "${value}" (expected CSS length)`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case 'fontSize':
|
|
268
|
+
for (const [key, value] of Object.entries(values)) {
|
|
269
|
+
if (!isValidCSSVariableName(key)) {
|
|
270
|
+
errors.push(`Invalid font size key: "${key}"`);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Font size can be length or keyword
|
|
275
|
+
const validKeywords = ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'];
|
|
276
|
+
if (!isValidCSSLength(value) && !validKeywords.includes(value)) {
|
|
277
|
+
errors.push(`Invalid font size value for ${key}: "${value}"`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
case 'fontWeight':
|
|
283
|
+
for (const [key, value] of Object.entries(values)) {
|
|
284
|
+
if (!isValidCSSVariableName(key)) {
|
|
285
|
+
errors.push(`Invalid font weight key: "${key}"`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Font weight: 100-900 or keyword
|
|
290
|
+
const weightNum = parseInt(value, 10);
|
|
291
|
+
const validKeywords = ['normal', 'bold', 'lighter', 'bolder'];
|
|
292
|
+
|
|
293
|
+
if (!(weightNum >= 100 && weightNum <= 900 && weightNum % 100 === 0) &&
|
|
294
|
+
!validKeywords.includes(value)) {
|
|
295
|
+
errors.push(`Invalid font weight value for ${key}: "${value}"`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
|
|
300
|
+
default:
|
|
301
|
+
// Unknown section - basic validation
|
|
302
|
+
for (const [key, value] of Object.entries(values)) {
|
|
303
|
+
if (typeof value !== 'string') {
|
|
304
|
+
errors.push(`Invalid value type for ${section}.${key}: expected string`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
valid: errors.length === 0,
|
|
311
|
+
errors
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Safe parseInt with bounds checking
|
|
317
|
+
* @param {string|number} value - Value to parse
|
|
318
|
+
* @param {number} min - Minimum allowed value (default: -1000000)
|
|
319
|
+
* @param {number} max - Maximum allowed value (default: 1000000)
|
|
320
|
+
* @returns {number} - Parsed integer or 0 if invalid/out of bounds
|
|
321
|
+
*/
|
|
322
|
+
export function safeParseInt(value, min = -1000000, max = 1000000) {
|
|
323
|
+
const num = parseInt(value, 10);
|
|
324
|
+
|
|
325
|
+
// Check for NaN
|
|
326
|
+
if (isNaN(num)) {
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check for infinity
|
|
331
|
+
if (!isFinite(num)) {
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Check bounds
|
|
336
|
+
if (num < min || num > max) {
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return num;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Safe parseFloat with bounds checking
|
|
345
|
+
* @param {string|number} value - Value to parse
|
|
346
|
+
* @param {number} min - Minimum allowed value
|
|
347
|
+
* @param {number} max - Maximum allowed value
|
|
348
|
+
* @returns {number} - Parsed float or 0 if invalid/out of bounds
|
|
349
|
+
*/
|
|
350
|
+
export function safeParseFloat(value, min = -1000000, max = 1000000) {
|
|
351
|
+
const num = parseFloat(value);
|
|
352
|
+
|
|
353
|
+
if (isNaN(num)) {
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!isFinite(num)) {
|
|
358
|
+
return 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (num < min || num > max) {
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return num;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get current memory usage in MB
|
|
371
|
+
* @returns {number} - Memory usage in MB
|
|
372
|
+
*/
|
|
373
|
+
export function getMemoryUsage() {
|
|
374
|
+
if (typeof process !== 'undefined' && process.memoryUsage) {
|
|
375
|
+
const usage = process.memoryUsage();
|
|
376
|
+
return Math.round(usage.heapUsed / 1024 / 1024);
|
|
377
|
+
}
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Check if memory usage is within safe limits
|
|
383
|
+
* @param {number} maxMemoryMB - Maximum allowed memory in MB (default: 500)
|
|
384
|
+
* @returns {boolean} - True if memory is within limits
|
|
385
|
+
*/
|
|
386
|
+
export function isMemorySafe(maxMemoryMB = 500) {
|
|
387
|
+
const currentMemory = getMemoryUsage();
|
|
388
|
+
return currentMemory < maxMemoryMB;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Batch process array items with memory checks
|
|
393
|
+
* @param {Array} items - Items to process
|
|
394
|
+
* @param {Function} processor - Processing function (item, index) => result
|
|
395
|
+
* @param {number} batchSize - Number of items per batch (default: 1000)
|
|
396
|
+
* @param {number} maxMemoryMB - Maximum memory in MB before yielding (default: 500)
|
|
397
|
+
* @returns {Array} - Processed results
|
|
398
|
+
*/
|
|
399
|
+
export async function batchProcessWithMemoryLimit(
|
|
400
|
+
items,
|
|
401
|
+
processor,
|
|
402
|
+
batchSize = 1000,
|
|
403
|
+
maxMemoryMB = 500
|
|
404
|
+
) {
|
|
405
|
+
const results = [];
|
|
406
|
+
|
|
407
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
408
|
+
const batch = items.slice(i, i + batchSize);
|
|
409
|
+
|
|
410
|
+
// Process batch
|
|
411
|
+
for (let j = 0; j < batch.length; j++) {
|
|
412
|
+
const result = processor(batch[j], i + j);
|
|
413
|
+
results.push(result);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Check memory usage and yield if needed
|
|
417
|
+
if (!isMemorySafe(maxMemoryMB)) {
|
|
418
|
+
logger.warn(`Memory usage approaching limit (${getMemoryUsage()}MB), yielding control`);
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
420
|
+
|
|
421
|
+
// If still over limit after yielding, throw error
|
|
422
|
+
if (!isMemorySafe(maxMemoryMB)) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Memory limit exceeded: ${getMemoryUsage()}MB > ${maxMemoryMB}MB. ` +
|
|
425
|
+
'Consider reducing the number of files or increasing Node.js memory limit.'
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return results;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Batch process tokens with memory checks
|
|
436
|
+
* @param {Array} tokens - Tokens to process
|
|
437
|
+
* @param {Function} processor - Processing function (token, index) => result
|
|
438
|
+
* @param {number} batchSize - Number of tokens per batch (default: 1000)
|
|
439
|
+
* @returns {Array} - Processed results
|
|
440
|
+
*/
|
|
441
|
+
export async function batchProcessTokens(tokens, processor, batchSize = 1000) {
|
|
442
|
+
const maxMemoryMB = 500;
|
|
443
|
+
return batchProcessWithMemoryLimit(tokens, processor, batchSize, maxMemoryMB);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export default {
|
|
447
|
+
sanitizeValue,
|
|
448
|
+
isValidColor,
|
|
449
|
+
isValidCSSLength,
|
|
450
|
+
isValidCSSVariableName,
|
|
451
|
+
validateThemeSection,
|
|
452
|
+
getMemoryUsage,
|
|
453
|
+
isMemorySafe,
|
|
454
|
+
batchProcessWithMemoryLimit,
|
|
455
|
+
batchProcessTokens
|
|
456
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Node.js I/O Utilities
|
|
3
|
+
* Node-specific helper functions (not for browser use)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read file with timeout protection
|
|
8
|
+
* @param {string} filePath - Path to file
|
|
9
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
10
|
+
* @returns {Promise<string>} - File contents
|
|
11
|
+
* @throws {Error} - On timeout or read failure
|
|
12
|
+
*/
|
|
13
|
+
export async function readFileWithTimeout(filePath, timeoutMs = 5000) {
|
|
14
|
+
const { promises: fsPromises, statSync } = await import('fs');
|
|
15
|
+
|
|
16
|
+
// Check file size first
|
|
17
|
+
let fileSize;
|
|
18
|
+
try {
|
|
19
|
+
const stats = statSync(filePath);
|
|
20
|
+
fileSize = stats.size;
|
|
21
|
+
|
|
22
|
+
// Reject files larger than 10MB
|
|
23
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
24
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
25
|
+
throw new Error(`File too large: ${filePath} (${Math.round(fileSize / 1024)}KB)`);
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`Cannot stat file: ${filePath} - ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const timeout = setTimeout(() => {
|
|
33
|
+
reject(new Error(`Read timeout for ${filePath} (exceeded ${timeoutMs}ms)`));
|
|
34
|
+
}, timeoutMs);
|
|
35
|
+
|
|
36
|
+
fsPromises.readFile(filePath, 'utf-8')
|
|
37
|
+
.then((content) => {
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
resolve(content);
|
|
40
|
+
})
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
reject(new Error(`Cannot read file: ${filePath} - ${error.message}`));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Batch read multiple files with timeout protection
|
|
50
|
+
* @param {Array<string>} filePaths - Paths to files
|
|
51
|
+
* @param {number} timeoutMs - Timeout per file
|
|
52
|
+
* @returns {Promise<Array<{path: string, content: string, error: Error|null}>>} - File contents
|
|
53
|
+
*/
|
|
54
|
+
export async function readMultipleFilesWithTimeout(filePaths, timeoutMs = 5000) {
|
|
55
|
+
const results = [];
|
|
56
|
+
|
|
57
|
+
// Read files in batches to avoid overwhelming the system
|
|
58
|
+
const BATCH_SIZE = 10;
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < filePaths.length; i += BATCH_SIZE) {
|
|
61
|
+
const batch = filePaths.slice(i, i + BATCH_SIZE);
|
|
62
|
+
|
|
63
|
+
const batchPromises = batch.map(async (filePath) => {
|
|
64
|
+
try {
|
|
65
|
+
const content = await readFileWithTimeout(filePath, timeoutMs);
|
|
66
|
+
return { path: filePath, content, error: null };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return { path: filePath, content: null, error };
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const batchResults = await Promise.all(batchPromises);
|
|
73
|
+
results.push(...batchResults);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default {
|
|
80
|
+
readFileWithTimeout,
|
|
81
|
+
readMultipleFilesWithTimeout
|
|
82
|
+
};
|