@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
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for convert-tailwind.js CLI
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it } from 'node:test';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
import { execSync } from 'node:child_process';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
|
|
11
|
-
const SCRIPT_PATH = path.join(process.cwd(), 'scripts', 'convert-tailwind.js');
|
|
12
|
-
|
|
13
|
-
describe('convert-tailwind CLI', () => {
|
|
14
|
-
it('should show help message', () => {
|
|
15
|
-
const output = execSync(`node "${SCRIPT_PATH}" --help`).toString();
|
|
16
|
-
assert.match(output, /Usage:/);
|
|
17
|
-
assert.match(output, /Options:/);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should convert string input', () => {
|
|
21
|
-
const input = '"<div class=\'p-4\'></div>"';
|
|
22
|
-
const output = execSync(`node "${SCRIPT_PATH}" --string ${input}`).toString();
|
|
23
|
-
// Check output contains expected attributes (ignoring order/formatting)
|
|
24
|
-
assert.match(output, /space="p:medium"/);
|
|
25
|
-
assert.doesNotMatch(output, /class=/);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should support exact mode', () => {
|
|
29
|
-
const input = '"<div class=\'p-4\'></div>"';
|
|
30
|
-
const output = execSync(`node "${SCRIPT_PATH}" --string ${input} --exact`).toString();
|
|
31
|
-
assert.match(output, /space="p:tw-4"/);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should fail without input file', () => {
|
|
35
|
-
try {
|
|
36
|
-
// Provide an argument so it doesn't just show help
|
|
37
|
-
execSync(`node "${SCRIPT_PATH}" --exact`);
|
|
38
|
-
assert.fail('Should have failed');
|
|
39
|
-
} catch (error) {
|
|
40
|
-
assert.strictEqual(error.status, 1);
|
|
41
|
-
assert.match(error.stderr.toString(), /Error: Input file required/);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should fail with missing string argument', () => {
|
|
46
|
-
try {
|
|
47
|
-
execSync(`node "${SCRIPT_PATH}" --string`);
|
|
48
|
-
assert.fail('Should have failed');
|
|
49
|
-
} catch (error) {
|
|
50
|
-
assert.strictEqual(error.status, 1);
|
|
51
|
-
assert.match(error.stderr.toString(), /Error: --string requires an HTML string argument/);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should handle file input/output', () => {
|
|
56
|
-
const inputFile = path.join(process.cwd(), 'tests', 'fixtures', 'test-input.html');
|
|
57
|
-
const outputFile = path.join(process.cwd(), 'tests', 'fixtures', 'test-output.html');
|
|
58
|
-
|
|
59
|
-
// Ensure fixture dir exists
|
|
60
|
-
fs.mkdirSync(path.dirname(inputFile), { recursive: true });
|
|
61
|
-
|
|
62
|
-
const content = '<div class="p-4 flex"></div>';
|
|
63
|
-
fs.writeFileSync(inputFile, content);
|
|
64
|
-
|
|
65
|
-
// Run CLI
|
|
66
|
-
execSync(`node "${SCRIPT_PATH}" "${inputFile}" -o "${outputFile}"`);
|
|
67
|
-
|
|
68
|
-
// Check output file
|
|
69
|
-
const result = fs.readFileSync(outputFile, 'utf-8');
|
|
70
|
-
assert.match(result, /layout="flex"/);
|
|
71
|
-
assert.match(result, /space="p:medium"/);
|
|
72
|
-
|
|
73
|
-
// Cleanup
|
|
74
|
-
fs.unlinkSync(inputFile);
|
|
75
|
-
fs.unlinkSync(outputFile);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should fail on invalid file paths', () => {
|
|
79
|
-
try {
|
|
80
|
-
// Trying to access something outside allowed dir (if validator is active)
|
|
81
|
-
// or just a non-existent file
|
|
82
|
-
execSync(`node "${SCRIPT_PATH}" non-existent.html`);
|
|
83
|
-
assert.fail('Should have failed');
|
|
84
|
-
} catch (error) {
|
|
85
|
-
assert.strictEqual(error.status, 1);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should handle unrecognized classes', () => {
|
|
90
|
-
const input = '"<div class=\'p-4 custom-class\'></div>"';
|
|
91
|
-
const output = execSync(`node "${SCRIPT_PATH}" --string ${input}`).toString();
|
|
92
|
-
assert.match(output, /space="p:medium"/);
|
|
93
|
-
assert.match(output, /class="custom-class"/);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SenangStart CSS - Security Unit Tests
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, it } from 'node:test';
|
|
6
|
-
import assert from 'node:assert';
|
|
7
|
-
import { parseSource } from '../../src/compiler/parser.js';
|
|
8
|
-
import { tokenize, tokenizeAll } from '../../src/compiler/tokenizer.js';
|
|
9
|
-
import { mergeConfig, deepMerge } from '../../src/config/defaults.js';
|
|
10
|
-
|
|
11
|
-
describe('Security Tests', () => {
|
|
12
|
-
|
|
13
|
-
describe('Parser - ReDoS Prevention', () => {
|
|
14
|
-
|
|
15
|
-
it('handles maliciously long attribute values without freezing', () => {
|
|
16
|
-
const longValue = 'a'.repeat(10000);
|
|
17
|
-
const maliciousHtml = `<div layout="${longValue}"></div>`;
|
|
18
|
-
|
|
19
|
-
const startTime = Date.now();
|
|
20
|
-
const result = parseSource(maliciousHtml);
|
|
21
|
-
const elapsed = Date.now() - startTime;
|
|
22
|
-
|
|
23
|
-
assert.ok(elapsed < 1000, `Parser took too long: ${elapsed}ms`);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('truncates excessively long attribute values', () => {
|
|
27
|
-
const longValue = 'a'.repeat(10000);
|
|
28
|
-
const html = `<div layout="${longValue}"></div>`;
|
|
29
|
-
|
|
30
|
-
const result = parseSource(html);
|
|
31
|
-
|
|
32
|
-
assert.ok(result.layout.size <= 1);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('handles deeply nested quotes without catastrophic backtracking', () => {
|
|
36
|
-
const maliciousHtml = `<div layout="${'"'.repeat(1000)}"></div>`;
|
|
37
|
-
|
|
38
|
-
const startTime = Date.now();
|
|
39
|
-
const result = parseSource(maliciousHtml);
|
|
40
|
-
const elapsed = Date.now() - startTime;
|
|
41
|
-
|
|
42
|
-
assert.ok(elapsed < 500, `Parser took too long: ${elapsed}ms`);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('Tokenizer - Input Validation', () => {
|
|
48
|
-
|
|
49
|
-
it('rejects empty token strings', () => {
|
|
50
|
-
const result = tokenize('', 'layout');
|
|
51
|
-
assert.strictEqual(result.error, 'Invalid token format');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('rejects excessively long token strings', () => {
|
|
55
|
-
const longToken = 'a'.repeat(201);
|
|
56
|
-
const result = tokenize(longToken, 'layout');
|
|
57
|
-
assert.strictEqual(result.error, 'Invalid token format');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('validates token property length', () => {
|
|
61
|
-
const result = tokenize('propertynameexceedsonehundredcharactersdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd:medium', 'layout');
|
|
62
|
-
assert.strictEqual(result.error, 'Invalid token structure');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('rejects invalid breakpoint prefixes', () => {
|
|
66
|
-
const result = tokenize('invalid:breakpoint:p:medium', 'layout');
|
|
67
|
-
assert.strictEqual(result.breakpoint, null);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('rejects invalid state prefixes', () => {
|
|
71
|
-
const result = tokenize('invalid:state:p:medium', 'layout');
|
|
72
|
-
assert.strictEqual(result.state, null);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('sanitizes semicolons in values to prevent CSS injection', () => {
|
|
76
|
-
const result = tokenize('p:[color:red;]', 'space');
|
|
77
|
-
assert.strictEqual(result.value, 'color:red_');
|
|
78
|
-
assert.strictEqual(result.isArbitrary, true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('allows braces for legitimate use cases like content icons', () => {
|
|
82
|
-
const result = tokenize('content:["{icon}"]', 'visual');
|
|
83
|
-
assert.strictEqual(result.value, '"{icon}"');
|
|
84
|
-
assert.strictEqual(result.isArbitrary, true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('sanitizes semicolons in arbitrary values', () => {
|
|
88
|
-
const result = tokenize('content:[test;]', 'visual');
|
|
89
|
-
assert.strictEqual(result.value, 'test_');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe('Config - Deep Merge', () => {
|
|
95
|
-
|
|
96
|
-
it('deep merges nested theme objects', () => {
|
|
97
|
-
const userConfig = {
|
|
98
|
-
theme: {
|
|
99
|
-
customSection: {
|
|
100
|
-
nested: {
|
|
101
|
-
deep: 'value'
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const result = mergeConfig(userConfig);
|
|
108
|
-
assert.strictEqual(result.theme.customSection.nested.deep, 'value');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('preserves defaults when user adds nested properties', () => {
|
|
112
|
-
const userConfig = {
|
|
113
|
-
theme: {
|
|
114
|
-
colors: {
|
|
115
|
-
'custom': '#123456'
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const result = mergeConfig(userConfig);
|
|
121
|
-
assert.strictEqual(result.theme.colors.custom, '#123456');
|
|
122
|
-
assert.strictEqual(result.theme.colors.primary, '#2563EB');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('handles circular references gracefully', () => {
|
|
126
|
-
const obj = { test: 'value' };
|
|
127
|
-
obj.self = obj;
|
|
128
|
-
|
|
129
|
-
const userConfig = {
|
|
130
|
-
theme: {
|
|
131
|
-
colors: obj
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
// Should not throw and should handle the circular reference
|
|
136
|
-
assert.doesNotThrow(() => mergeConfig(userConfig));
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('Deep Merge Utility', () => {
|
|
142
|
-
|
|
143
|
-
it('merges nested objects recursively', () => {
|
|
144
|
-
const target = {
|
|
145
|
-
a: { b: { c: 1 } },
|
|
146
|
-
d: 2
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const source = {
|
|
150
|
-
a: { b: { e: 3 } },
|
|
151
|
-
f: 4
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const result = deepMerge(target, source);
|
|
155
|
-
|
|
156
|
-
assert.deepStrictEqual(result, {
|
|
157
|
-
a: { b: { c: 1, e: 3 } },
|
|
158
|
-
d: 2,
|
|
159
|
-
f: 4
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('overrides primitive values', () => {
|
|
164
|
-
const result = deepMerge(
|
|
165
|
-
{ value: 'original' },
|
|
166
|
-
{ value: 'updated' }
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
assert.strictEqual(result.value, 'updated');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('handles undefined source values', () => {
|
|
173
|
-
const result = deepMerge(
|
|
174
|
-
{ a: 1 },
|
|
175
|
-
{ b: undefined }
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
assert.strictEqual(result.a, 1);
|
|
179
|
-
assert.strictEqual(result.b, undefined);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe('TokenizeAll - Security', () => {
|
|
185
|
-
|
|
186
|
-
it('filters out tokens with errors', () => {
|
|
187
|
-
const parsed = {
|
|
188
|
-
layout: new Set(['']),
|
|
189
|
-
space: new Set(['p:medium']),
|
|
190
|
-
visual: new Set([])
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const tokens = tokenizeAll(parsed);
|
|
194
|
-
|
|
195
|
-
const errorToken = tokens.find(t => t.error);
|
|
196
|
-
assert.ok(errorToken, 'Should have an error token');
|
|
197
|
-
|
|
198
|
-
const validTokens = tokens.filter(t => !t.error);
|
|
199
|
-
assert.ok(validTokens.length >= 1);
|
|
200
|
-
assert.strictEqual(validTokens[0].property, 'p');
|
|
201
|
-
assert.strictEqual(validTokens[0].value, 'medium');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
});
|
/package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js}
RENAMED
|
File without changes
|