@bookklik/senangstart-css 0.2.10 → 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.
Files changed (39) hide show
  1. package/.agent/skills/add-utility/SKILL.md +65 -0
  2. package/.agent/workflows/add-utility.md +2 -0
  3. package/.agent/workflows/build.md +2 -0
  4. package/.agent/workflows/dev.md +2 -0
  5. package/AGENTS.md +30 -0
  6. package/dist/senangstart-css.js +362 -151
  7. package/dist/senangstart-css.min.js +175 -174
  8. package/dist/senangstart-tw.js +4 -4
  9. package/dist/senangstart-tw.min.js +1 -1
  10. package/docs/ms/reference/visual/ring-color.md +2 -2
  11. package/docs/ms/reference/visual/ring-offset.md +3 -3
  12. package/docs/ms/reference/visual/ring.md +5 -5
  13. package/docs/public/assets/senangstart-css.min.js +175 -174
  14. package/docs/public/llms.txt +10 -10
  15. package/docs/reference/visual/ring-color.md +2 -2
  16. package/docs/reference/visual/ring-offset.md +3 -3
  17. package/docs/reference/visual/ring.md +5 -5
  18. package/package.json +1 -1
  19. package/src/cdn/tw-conversion-engine.js +4 -4
  20. package/src/cli/commands/build.js +42 -14
  21. package/src/cli/commands/dev.js +157 -93
  22. package/src/compiler/generators/css.js +371 -199
  23. package/src/compiler/tokenizer.js +25 -23
  24. package/src/core/tokenizer-core.js +46 -19
  25. package/src/definitions/visual-borders.js +10 -10
  26. package/src/utils/common.js +456 -39
  27. package/src/utils/node-io.js +82 -0
  28. package/tests/integration/dev-recovery.test.js +231 -0
  29. package/tests/unit/cli/memory-limits.test.js +169 -0
  30. package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
  31. package/tests/unit/compiler/generators/css-errors.test.js +102 -0
  32. package/tests/unit/convert-tailwind.test.js +518 -442
  33. package/tests/unit/utils/common.test.js +376 -26
  34. package/tests/unit/utils/file-timeout.test.js +154 -0
  35. package/tests/unit/utils/theme-validation.test.js +181 -0
  36. package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
  37. package/tests/unit/convert-tailwind.cli.test.js +0 -95
  38. package/tests/unit/security.test.js +0 -206
  39. /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
@@ -1,23 +1,25 @@
1
- /**
2
- * SenangStart CSS - Tokenizer
3
- * Re-exports core tokenizer functions with backward compatibility
4
- */
5
-
6
- // Re-export everything from core tokenizer
7
- export {
8
- tokenize,
9
- tokenizeAll,
10
- sanitizeValue,
11
- isValidToken
12
- } from '../core/tokenizer-core.js';
13
-
14
- // Re-export constants for backward compatibility
15
- export {
16
- BREAKPOINTS,
17
- STATES,
18
- LAYOUT_KEYWORDS
19
- } from '../core/constants.js';
20
-
21
- // Default export for backward compatibility
22
- import { tokenize, tokenizeAll } from '../core/tokenizer-core.js';
23
- export default { tokenize, tokenizeAll };
1
+ /**
2
+ * SenangStart CSS - Tokenizer
3
+ * Re-exports core tokenizer functions with backward compatibility
4
+ */
5
+
6
+ // Re-export everything from core tokenizer
7
+ export {
8
+ tokenize,
9
+ tokenizeAll,
10
+ tokenizeAllWithBatching,
11
+ sanitizeValue,
12
+ isValidToken
13
+ } from '../core/tokenizer-core.js';
14
+
15
+ // Re-export constants for backward compatibility
16
+ export {
17
+ BREAKPOINTS,
18
+ STATES,
19
+ LAYOUT_KEYWORDS
20
+ } from '../core/constants.js';
21
+
22
+ // Default export for backward compatibility
23
+ import { tokenize, tokenizeAll, tokenizeAllWithBatching } from '../core/tokenizer-core.js';
24
+ export default { tokenize, tokenizeAll, tokenizeAllWithBatching };
25
+
@@ -1,10 +1,10 @@
1
- /**
2
- * SenangStart CSS - Core Tokenizer
3
- * Pure tokenizer functions shared by JIT runtime and build-time compiler
4
- */
5
-
6
- import { BREAKPOINTS, STATES, LAYOUT_KEYWORDS } from './constants.js';
7
- import { sanitizeValue } from '../utils/common.js';
1
+ /**
2
+ * SenangStart CSS - Core Tokenizer
3
+ * Pure tokenizer functions shared by JIT runtime and build-time compiler
4
+ */
5
+
6
+ import { BREAKPOINTS, STATES, LAYOUT_KEYWORDS } from './constants.js';
7
+ import { sanitizeValue, batchProcessTokens } from '../utils/common.js';
8
8
 
9
9
  /**
10
10
  * Sanitize token value to prevent CSS injection
@@ -164,15 +164,42 @@ export function tokenize(raw, attrType) {
164
164
  * @returns {Array} - Array of token objects
165
165
  */
166
166
  export function tokenizeAll(parsed) {
167
- const tokens = [];
168
-
169
- for (const [attrType, values] of Object.entries(parsed)) {
170
- for (const raw of values) {
171
- tokens.push(tokenize(raw, attrType));
172
- }
173
- }
174
-
175
- return tokens;
176
- }
177
-
178
- export default { tokenize, tokenizeAll, sanitizeValue, isValidToken };
167
+ const tokens = [];
168
+
169
+ for (const [attrType, values] of Object.entries(parsed)) {
170
+ for (const raw of values) {
171
+ tokens.push(tokenize(raw, attrType));
172
+ }
173
+ }
174
+
175
+ return tokens;
176
+ }
177
+
178
+ /**
179
+ * Tokenize all values with memory-protected batch processing
180
+ * @param {Object} parsed - Parsed tokens from parser { layout: Set, space: Set, visual: Set }
181
+ * @param {number} batchSize - Number of tokens per batch (default: 1000)
182
+ * @returns {Promise<Array>} - Array of token objects
183
+ */
184
+ export async function tokenizeAllWithBatching(parsed, batchSize = 1000) {
185
+ const rawTokens = [];
186
+
187
+ // Collect all raw tokens first
188
+ for (const [attrType, values] of Object.entries(parsed)) {
189
+ for (const raw of values) {
190
+ rawTokens.push({ raw, attrType });
191
+ }
192
+ }
193
+
194
+ // Process tokens in batches with memory protection
195
+ const tokens = await batchProcessTokens(
196
+ rawTokens,
197
+ ({ raw, attrType }) => tokenize(raw, attrType),
198
+ batchSize
199
+ );
200
+
201
+ return tokens;
202
+ }
203
+
204
+ export default { tokenize, tokenizeAll, tokenizeAllWithBatching, sanitizeValue, isValidToken };
205
+
@@ -181,11 +181,11 @@ export const ring = {
181
181
  supportsArbitrary: true,
182
182
  values: [
183
183
  { value: 'none', css: 'box-shadow: 0 0 0 0 transparent;', description: 'No ring', descriptionMs: 'Tiada cincin' },
184
- { value: 'thin', css: 'box-shadow: 0 0 0 1px var(--ring-color);', description: 'Thin ring (1px)', descriptionMs: 'Cincin nipis (1px)' },
185
- { value: 'regular', css: 'box-shadow: 0 0 0 2px var(--ring-color);', description: 'Regular ring (2px)', descriptionMs: 'Cincin biasa (2px)' },
186
- { value: 'small', css: 'box-shadow: 0 0 0 4px var(--ring-color);', description: 'Small ring (4px)', descriptionMs: 'Cincin kecil (4px)' },
187
- { value: 'medium', css: 'box-shadow: 0 0 0 6px var(--ring-color);', description: 'Medium ring (6px)', descriptionMs: 'Cincin sederhana (6px)' },
188
- { value: 'big', css: 'box-shadow: 0 0 0 8px var(--ring-color);', description: 'Big ring (8px)', descriptionMs: 'Cincin besar (8px)' }
184
+ { value: 'thin', css: 'box-shadow: var(--ring-inset) 0 0 0 1px var(--ss-ring-color);', description: 'Thin ring (1px)', descriptionMs: 'Cincin nipis (1px)' },
185
+ { value: 'regular', css: 'box-shadow: var(--ring-inset) 0 0 0 2px var(--ss-ring-color);', description: 'Regular ring (2px)', descriptionMs: 'Cincin biasa (2px)' },
186
+ { value: 'small', css: 'box-shadow: var(--ring-inset) 0 0 0 4px var(--ss-ring-color);', description: 'Small ring (4px)', descriptionMs: 'Cincin kecil (4px)' },
187
+ { value: 'medium', css: 'box-shadow: var(--ring-inset) 0 0 0 6px var(--ss-ring-color);', description: 'Medium ring (6px)', descriptionMs: 'Cincin sederhana (6px)' },
188
+ { value: 'big', css: 'box-shadow: var(--ring-inset) 0 0 0 8px var(--ss-ring-color);', description: 'Big ring (8px)', descriptionMs: 'Cincin besar (8px)' }
189
189
  ],
190
190
  examples: [
191
191
  { code: '<button visual="focus-visible:ring:small ring-color:primary">Focus me</button>', description: 'Focus ring on keyboard focus' },
@@ -216,8 +216,8 @@ export const ringColor = {
216
216
  usesScale: 'colors',
217
217
  supportsArbitrary: true,
218
218
  values: [
219
- { value: 'primary', css: '--ring-color: var(--c-primary);', description: 'Primary ring color', descriptionMs: 'Warna cincin utama' },
220
- { value: 'blue-500', css: '--ring-color: var(--c-blue-500);', description: 'Blue ring color', descriptionMs: 'Warna cincin biru' }
219
+ { value: 'primary', css: '--ss-ring-color: var(--c-primary);', description: 'Primary ring color', descriptionMs: 'Warna cincin utama' },
220
+ { value: 'blue-500', css: '--ss-ring-color: var(--c-blue-500);', description: 'Blue ring color', descriptionMs: 'Warna cincin biru' }
221
221
  ],
222
222
  examples: [
223
223
  { code: '<button visual="ring:small ring-color:primary">Colored ring</button>', description: 'Ring with custom color' }
@@ -233,9 +233,9 @@ export const ringOffset = {
233
233
  category: 'visual',
234
234
  supportsArbitrary: true,
235
235
  values: [
236
- { value: '0', css: '--ring-offset: 0px;', description: 'No offset', descriptionMs: 'Tiada ruang' },
237
- { value: '2', css: '--ring-offset: 2px;', description: '2px offset', descriptionMs: 'Ruang 2px' },
238
- { value: '4', css: '--ring-offset: 4px;', description: '4px offset', descriptionMs: 'Ruang 4px' }
236
+ { value: '0', css: '--ss-ring-offset-width: 0px;', description: 'No offset', descriptionMs: 'Tiada ruang' },
237
+ { value: '2', css: '--ss-ring-offset-width: 2px;', description: '2px offset', descriptionMs: 'Ruang 2px' },
238
+ { value: '4', css: '--ss-ring-offset-width: 4px;', description: '4px offset', descriptionMs: 'Ruang 4px' }
239
239
  ],
240
240
  examples: [
241
241
  { code: '<button visual="ring:small ring-offset:2 ring-color:primary">With offset</button>', description: 'Ring with offset' }
@@ -1,39 +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
- * Removes potentially dangerous characters/sequences
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
- let sanitized = value;
18
-
19
- // Remove potentially dangerous characters that could break CSS syntax
20
- // Note: We don't filter {} because some tests expect them (e.g. content icons like content: "→")
21
- const dangerousChars = /[;]/g;
22
- sanitized = sanitized.replace(dangerousChars, '_');
23
-
24
- // Filter CSS injection attempts via at-rules
25
- const atRules = /@import|@charset|@namespace|@supports|@keyframes/gi;
26
- sanitized = sanitized.replace(atRules, '');
27
-
28
- // Filter expression() (IE vulnerability)
29
- const expression = /expression\s*\(/gi;
30
- sanitized = sanitized.replace(expression, '');
31
-
32
- // Filter javascript: and data: URLs in url() that could execute scripts
33
- const dangerousUrls = /(url\s*\(\s*['"]?)(javascript:|data:)([^)]*\))/gi;
34
- sanitized = sanitized.replace(dangerousUrls, '$1about:blank$3');
35
-
36
- return sanitized;
37
- }
38
-
39
- export default { sanitizeValue };
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
+ };