@bookklik/senangstart-css 0.2.8 → 0.2.9

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 (46) hide show
  1. package/dist/senangstart-css.js +2510 -1927
  2. package/dist/senangstart-css.min.js +211 -208
  3. package/dist/senangstart-tw.js +170 -73
  4. package/dist/senangstart-tw.min.js +1 -1
  5. package/docs/guide/configuration.md +2 -2
  6. package/docs/guide/states.md +60 -0
  7. package/docs/ms/guide/configuration.md +2 -2
  8. package/docs/ms/guide/states.md +60 -0
  9. package/docs/ms/reference/colors.md +2 -2
  10. package/docs/ms/reference/space/height.md +10 -10
  11. package/docs/ms/reference/space/width.md +12 -12
  12. package/docs/public/assets/senangstart-css.min.js +211 -208
  13. package/docs/public/llms.txt +28 -0
  14. package/docs/reference/colors.md +2 -2
  15. package/docs/reference/space/height.md +10 -10
  16. package/docs/reference/space/width.md +12 -12
  17. package/package.json +1 -1
  18. package/public/senangstart.css +1 -1
  19. package/scripts/convert-tailwind.js +191 -68
  20. package/scripts/generate-llms-txt.js +28 -0
  21. package/src/cdn/senangstart-engine.js +37 -1927
  22. package/src/cdn/tw-conversion-engine.js +203 -74
  23. package/src/compiler/generators/css.js +300 -54
  24. package/src/compiler/parser.js +14 -4
  25. package/src/config/defaults.js +1 -1
  26. package/src/core/constants.js +5 -3
  27. package/src/definitions/index.js +3 -2
  28. package/src/definitions/layout.js +2 -2
  29. package/src/definitions/space.js +45 -19
  30. package/src/index.js +47 -0
  31. package/templates/senangstart.config.js +1 -1
  32. package/tests/helpers/test-utils.js +1 -1
  33. package/tests/integration/compiler.test.js +12 -1
  34. package/tests/unit/compiler/generators/css.coverage.test.js +833 -0
  35. package/tests/unit/compiler/generators/css.test.js +1418 -1
  36. package/tests/unit/compiler/generators/preflight.test.js +31 -0
  37. package/tests/unit/compiler/parser.test.js +26 -0
  38. package/tests/unit/config/defaults.test.js +2 -2
  39. package/tests/unit/convert-tailwind.cli.test.js +95 -0
  40. package/tests/unit/convert-tailwind.coverage.test.js +225 -0
  41. package/tests/unit/convert-tailwind.test.js +49 -20
  42. package/tests/unit/core/tokenizer-core.test.js +102 -0
  43. package/tests/unit/definitions/index.test.js +108 -0
  44. package/tests/unit/definitions/layout_definitions.test.js +40 -0
  45. package/tests/unit/utils/common.test.js +26 -0
  46. package/scripts/bundle-jit.js +0 -45
@@ -0,0 +1,31 @@
1
+
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert';
4
+ import { generatePreflight } from '../../../../src/compiler/generators/preflight.js';
5
+
6
+ test('generatePreflight', async (t) => {
7
+ await t.test('should generate preflight css', () => {
8
+ const css = generatePreflight({});
9
+
10
+ // Basic assertions to ensure content is generated
11
+ assert.ok(css.includes('SenangStart Preflight'), 'Should include header');
12
+ assert.ok(css.includes('box-sizing: border-box'), 'Should include box-sizing reset');
13
+ assert.ok(css.includes('html'), 'Should include html selector');
14
+ assert.ok(css.includes('body'), 'Should include body selector');
15
+ });
16
+
17
+ await t.test('should include opinionated defaults', () => {
18
+ const css = generatePreflight({});
19
+
20
+ // Check for specific opinionated keys
21
+ assert.ok(css.includes('line-height: 1.5'), 'Should set default line-height');
22
+ assert.ok(css.includes('tab-size: 4'), 'Should set tab-size');
23
+ assert.ok(css.includes('font-family: ui-sans-serif'), 'Should set default font stack');
24
+ });
25
+
26
+ await t.test('should handle modern-normalize styles', () => {
27
+ const css = generatePreflight({});
28
+ assert.ok(css.includes('abbr:where([title])'), 'Should include abbr styles');
29
+ assert.ok(css.includes('::-webkit-search-decoration'), 'Should include search decoration reset');
30
+ });
31
+ });
@@ -188,6 +188,32 @@ describe('Parser', () => {
188
188
  assert.ok(result.visual.has('hover:text:secondary'));
189
189
  });
190
190
 
191
+ it('extracts interact and listens attributes', () => {
192
+ const html = '<div interact="click:toggle" listens="auth:update">Test</div>';
193
+ const result = parseSource(html);
194
+
195
+ assert.ok(result.interact.has('click:toggle'));
196
+ assert.ok(result.listens.has('auth:update'));
197
+ });
198
+
199
+ it('handles extremely long attribute values by skipping them', () => {
200
+ const longValue = 'a'.repeat(10001);
201
+ const html = `<div layout="${longValue}">Test</div>`;
202
+ const result = parseSource(html);
203
+
204
+ assert.strictEqual(result.layout.size, 0);
205
+ });
206
+
207
+ it('handles long tokens by skipping them', () => {
208
+ const longToken = 'a'.repeat(501);
209
+ const html = `<div layout="${longToken} valid-token">Test</div>`;
210
+ const result = parseSource(html);
211
+
212
+ assert.strictEqual(result.layout.size, 1);
213
+ assert.ok(result.layout.has('valid-token'));
214
+ assert.ok(!result.layout.has(longToken));
215
+ });
216
+
191
217
  });
192
218
 
193
219
  describe('parseMultipleSources', () => {
@@ -67,7 +67,7 @@ describe('Config', () => {
67
67
  const userConfig = {
68
68
  theme: {
69
69
  colors: {
70
- 'brand': '#8B5CF6'
70
+ 'brand': '#38BDF8'
71
71
  }
72
72
  }
73
73
  };
@@ -75,7 +75,7 @@ describe('Config', () => {
75
75
  const result = mergeConfig(userConfig);
76
76
 
77
77
  // Custom color should be added
78
- assert.strictEqual(result.theme.colors.brand, '#8B5CF6');
78
+ assert.strictEqual(result.theme.colors.brand, '#38BDF8');
79
79
  // Default colors should still exist
80
80
  assert.strictEqual(result.theme.colors.primary, defaultConfig.theme.colors.primary);
81
81
  });
@@ -0,0 +1,95 @@
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
+ });
@@ -0,0 +1,225 @@
1
+
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert';
4
+ import { execSync } from 'node:child_process';
5
+ import path from 'node:path';
6
+ import { convertClass, main, convertHTML, convertClasses, isValidFilePath } from '../../scripts/convert-tailwind.js';
7
+ import fs from 'node:fs';
8
+ import { spawnSync } from 'node:child_process';
9
+
10
+ test('convert-tailwind coverage', async (t) => {
11
+
12
+ await t.test('mask and divide utilities', () => {
13
+ assert.deepStrictEqual(convertClass('mask-none'), { category: 'visual', value: 'mask:none' });
14
+ assert.deepStrictEqual(convertClass('mask-image-none'), { category: 'visual', value: 'mask-image:none' });
15
+ assert.deepStrictEqual(convertClass('mask-mode-match'), { category: 'visual', value: 'mask-mode:match' });
16
+ assert.deepStrictEqual(convertClass('mask-origin-center'), { category: 'visual', value: 'mask-origin:center' });
17
+ assert.deepStrictEqual(convertClass('mask-position-center'), { category: 'visual', value: 'mask-position:center' });
18
+ assert.deepStrictEqual(convertClass('mask-repeat-repeat'), { category: 'visual', value: 'mask-repeat:repeat' });
19
+ assert.deepStrictEqual(convertClass('mask-size-auto'), { category: 'visual', value: 'mask-size:auto' });
20
+ assert.deepStrictEqual(convertClass('mask-type-luminance'), { category: 'visual', value: 'mask-type:luminance' });
21
+
22
+ assert.deepStrictEqual(convertClass('divide-x-reverse'), { category: 'visual', value: 'divide-x:reverse' });
23
+ assert.deepStrictEqual(convertClass('divide-y-reverse'), { category: 'visual', value: 'divide-y:reverse' });
24
+
25
+ assert.deepStrictEqual(convertClass('divide-x'), { category: 'visual', value: 'divide-x-w:thin' });
26
+ assert.deepStrictEqual(convertClass('divide-y'), { category: 'visual', value: 'divide-y-w:thin' });
27
+
28
+ assert.deepStrictEqual(convertClass('divide-x-2'), { category: 'visual', value: 'divide-x-w:regular' });
29
+ assert.deepStrictEqual(convertClass('divide-y-4'), { category: 'visual', value: 'divide-y-w:medium' });
30
+
31
+ assert.deepStrictEqual(convertClass('divide-dotted'), { category: 'visual', value: 'divide-style:dotted' });
32
+
33
+ assert.deepStrictEqual(convertClass('divide-4'), { category: 'visual', value: 'divide-w:medium' });
34
+ assert.deepStrictEqual(convertClass('divide-red-500'), { category: 'visual', value: 'divide:red-500' });
35
+ });
36
+
37
+ await t.test('space utilities', () => {
38
+ assert.deepStrictEqual(convertClass('m-[10px]'), { category: 'space', value: 'm:[10px]' });
39
+ assert.deepStrictEqual(convertClass('-m-[10px]'), { category: 'space', value: 'm:[-10px]' });
40
+ assert.deepStrictEqual(convertClass('p-[10px]'), { category: 'space', value: 'p:[10px]' });
41
+
42
+ assert.deepStrictEqual(convertClass('w-min'), { category: 'space', value: 'w:min' });
43
+ assert.deepStrictEqual(convertClass('w-max'), { category: 'space', value: 'w:max' });
44
+ assert.deepStrictEqual(convertClass('w-fit'), { category: 'space', value: 'w:fit' });
45
+ assert.deepStrictEqual(convertClass('h-screen'), { category: 'space', value: 'h:[100vh]' });
46
+
47
+ // Percentage adjectives
48
+ assert.deepStrictEqual(convertClass('w-1/2'), { category: 'space', value: 'w:half' });
49
+ assert.deepStrictEqual(convertClass('h-1/3'), { category: 'space', value: 'h:third' });
50
+
51
+ // Fractional values in getSpacingScale (for padding/gap)
52
+ assert.deepStrictEqual(convertClass('p-1/2'), { category: 'space', value: 'p:half' });
53
+ assert.deepStrictEqual(convertClass('p-1/2', { exact: true }), { category: 'space', value: 'p:half' });
54
+ assert.deepStrictEqual(convertClass('gap-x-1/2'), { category: 'space', value: 'g-x:half' });
55
+ });
56
+
57
+
58
+
59
+ await t.test('transform utilities', () => {
60
+ // 3D Transforms
61
+ assert.deepStrictEqual(convertClass('perspective-500'), { category: 'visual', value: 'perspective:500' });
62
+ assert.deepStrictEqual(convertClass('perspective-origin-top'), { category: 'visual', value: 'perspective-origin:top' });
63
+ assert.deepStrictEqual(convertClass('transform-style-preserve-3d'), { category: 'visual', value: 'transform-style:preserve-3d' });
64
+ assert.deepStrictEqual(convertClass('backface-visible'), { category: 'visual', value: 'backface:visible' });
65
+
66
+ // Rotate
67
+ assert.deepStrictEqual(convertClass('rotate-45'), { category: 'visual', value: 'rotate:45' });
68
+ assert.deepStrictEqual(convertClass('rotate-x-45'), { category: 'visual', value: 'rotate-x:45' });
69
+ assert.deepStrictEqual(convertClass('rotate-y-45'), { category: 'visual', value: 'rotate-y:45' });
70
+ assert.deepStrictEqual(convertClass('rotate-z-45'), { category: 'visual', value: 'rotate-z:45' });
71
+
72
+ // Scale
73
+ assert.deepStrictEqual(convertClass('scale-50'), { category: 'visual', value: 'scale:50' });
74
+ assert.deepStrictEqual(convertClass('scale-x-50'), { category: 'visual', value: 'scale-x:50' });
75
+ assert.deepStrictEqual(convertClass('scale-y-50'), { category: 'visual', value: 'scale-y:50' });
76
+
77
+ // Skew
78
+ assert.deepStrictEqual(convertClass('skew-x-12'), { category: 'visual', value: 'skew-x:12' });
79
+ assert.deepStrictEqual(convertClass('skew-y-12'), { category: 'visual', value: 'skew-y:12' });
80
+
81
+ // Translate
82
+ assert.deepStrictEqual(convertClass('translate-x-4'), { category: 'visual', value: 'translate-x:medium' });
83
+ assert.deepStrictEqual(convertClass('translate-y-4'), { category: 'visual', value: 'translate-y:medium' });
84
+ assert.deepStrictEqual(convertClass('translate-z-4'), { category: 'visual', value: 'translate-z:medium' });
85
+
86
+ // Origin
87
+ assert.deepStrictEqual(convertClass('origin-top'), { category: 'visual', value: 'origin:top' });
88
+
89
+ // Translate edge cases
90
+ assert.deepStrictEqual(convertClass('translate-x-0'), { category: 'visual', value: 'translate-x:0' });
91
+ assert.deepStrictEqual(convertClass('-translate-x-[10px]'), { category: 'visual', value: 'translate-x:[-10px]' });
92
+ });
93
+
94
+ // Mask utilities already covered in first block
95
+
96
+ await t.test('exact mode and edge cases', () => {
97
+ // Spacing scale exact mode
98
+ assert.deepStrictEqual(convertClass('p-4', { exact: true }), { category: 'space', value: 'p:tw-4' });
99
+ assert.deepStrictEqual(convertClass('m-auto', { exact: true }), { category: 'space', value: 'm:auto' });
100
+ assert.deepStrictEqual(convertClass('w-full', { exact: true }), { category: 'space', value: 'w:full' });
101
+ assert.deepStrictEqual(convertClass('p-[20px]', { exact: true }), { category: 'space', value: 'p:[20px]' });
102
+
103
+ // Unknown spacing scale
104
+ assert.deepStrictEqual(convertClass('p-unknown'), { category: 'space', value: 'p:[unknown]' });
105
+
106
+ // Border width exact mode
107
+ assert.deepStrictEqual(convertClass('border-2', { exact: true }), { category: 'visual', value: 'border-w:tw-2' });
108
+ assert.deepStrictEqual(convertClass('border-t-2', { exact: true }), { category: 'visual', value: 'border-t-w:tw-2' });
109
+
110
+ // Other utilities
111
+ assert.deepStrictEqual(convertClass('border-red-500'), { category: 'visual', value: 'border:red-500' });
112
+ assert.deepStrictEqual(convertClass('order-1'), { category: 'layout', value: 'order:1' });
113
+ assert.deepStrictEqual(convertClass('grid-cols-3'), { category: 'layout', value: 'grid-cols:3' });
114
+ assert.deepStrictEqual(convertClass('col-span-2'), { category: 'layout', value: 'col-span:2' });
115
+ assert.deepStrictEqual(convertClass('grid-rows-2'), { category: 'layout', value: 'grid-rows:2' });
116
+ assert.deepStrictEqual(convertClass('row-span-3'), { category: 'layout', value: 'row-span:3' });
117
+ assert.deepStrictEqual(convertClass('opacity-50'), { category: 'visual', value: 'opacity:50' });
118
+
119
+ // Font weight
120
+ assert.deepStrictEqual(convertClass('font-bold'), { category: 'visual', value: 'font:tw-bold' });
121
+
122
+ // Positional arbitrary values
123
+ assert.deepStrictEqual(convertClass('left-[10px]'), { category: 'layout', value: 'left:[10px]' });
124
+
125
+ // Unrecognized classes
126
+ const result = convertClasses('flex unknown-class');
127
+ assert.deepStrictEqual(result.unrecognized, ['unknown-class']);
128
+ });
129
+
130
+ await t.test('internal tests', async (t) => {
131
+ // Mock process.exit and console.error
132
+ const originalExit = process.exit;
133
+ const originalConsoleError = console.error;
134
+ const originalConsoleLog = console.log;
135
+ let lastExitCode = null;
136
+
137
+ process.exit = (code) => {
138
+ lastExitCode = code;
139
+ throw new Error(`ProcessExit:${code}`);
140
+ };
141
+ console.error = (...args) => { /* originalConsoleError('MOCK ERROR:', ...args); */ };
142
+ console.log = (...args) => { /* originalConsoleLog('MOCK LOG:', ...args); */ };
143
+
144
+ try {
145
+ // Help - should return normally
146
+ main(['--help']);
147
+
148
+ // String mode - success
149
+ main(['--string', '<div class="flex"></div>']);
150
+
151
+ // Missing string argument - should exit 1
152
+ assert.throws(() => main(['--string']), /ProcessExit:1/);
153
+
154
+ // Invalid file path - should exit 1
155
+ assert.throws(() => main(['/etc/passwd']), /ProcessExit:1/);
156
+
157
+ // Valid file but nonexistent (ENOENT) - should exit 1
158
+ assert.throws(() => main(['non-existent-local.html']), /ProcessExit:1/);
159
+
160
+ // Create a dummy input file first
161
+ const dummyFile = 'dummy.html';
162
+ fs.writeFileSync(dummyFile, '<div></div>');
163
+ try {
164
+ // Invalid output path - should exit 1
165
+ assert.throws(() => main([dummyFile, '-o', '/etc/passwd']), /ProcessExit:1/);
166
+
167
+ // Console output when no output file
168
+ main([dummyFile]);
169
+
170
+ // main with multiple args to test skip logic
171
+ main(['--string', '<div class="flex"></div>', '-o', 'out.html']);
172
+
173
+ // Test isValidFilePath non-Windows branch if on Windows
174
+ if (process.platform === 'win32') {
175
+ const originalPlatform = process.platform;
176
+ // Node's process.platform is read-only in some versions, try to overwrite
177
+ try {
178
+ Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
179
+ main([dummyFile]);
180
+ } catch (e) {
181
+ // If platform is not configurable, we might not be able to hit this line on Windows
182
+ } finally {
183
+ Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
184
+ }
185
+ }
186
+
187
+ // Successful file output (line 1146-1147)
188
+ const dummyOutput = 'dummy_out.html';
189
+ main([dummyFile, '-o', dummyOutput]);
190
+ assert.ok(fs.existsSync(dummyOutput));
191
+ if (fs.existsSync(dummyOutput)) fs.unlinkSync(dummyOutput);
192
+ } finally {
193
+ if (fs.existsSync(dummyFile)) fs.unlinkSync(dummyFile);
194
+ }
195
+
196
+ // convertHTML direct call
197
+ const res = convertHTML('<div class="p-4 text-red-500 unknown-class"></div>');
198
+ assert.match(res, /space="p:medium"/);
199
+ assert.match(res, /visual="text:red-500"/);
200
+ assert.match(res, /class="unknown-class"/);
201
+
202
+ // main with missing input file should exit 1
203
+ assert.throws(() => main(['-o', 'out.html']), /ProcessExit:1/);
204
+
205
+ // Trigger catch block in main (though it's hard to trigger without real FS errors)
206
+ // We pass a number to trigger TypeError: The "path" argument must be of type string.
207
+ assert.throws(() => main(['dummy.html', '-o', 123]), /ProcessExit:1/);
208
+
209
+ // Directly test isValidFilePath catch block
210
+ assert.strictEqual(isValidFilePath(null), false);
211
+ assert.strictEqual(isValidFilePath(undefined), false);
212
+ assert.strictEqual(isValidFilePath(123), false);
213
+
214
+ // CLI entry point coverage (line 1160-1161)
215
+ const scriptPath = path.resolve('scripts/convert-tailwind.js');
216
+ spawnSync('node', [scriptPath, '--help'], { encoding: 'utf8' });
217
+
218
+ } finally {
219
+ process.exit = originalExit;
220
+ console.error = originalConsoleError;
221
+ console.log = originalConsoleLog;
222
+ }
223
+ });
224
+
225
+ });
@@ -41,6 +41,15 @@ describe('convertClass', () => {
41
41
  assert.deepStrictEqual(convertClass('fixed'), { category: 'layout', value: 'fixed' });
42
42
  });
43
43
 
44
+ it('should convert positional offsets (top, left, right, bottom)', () => {
45
+ assert.deepStrictEqual(convertClass('top-0'), { category: 'layout', value: 'top:none' });
46
+ assert.deepStrictEqual(convertClass('left-0'), { category: 'layout', value: 'left:none' });
47
+ assert.deepStrictEqual(convertClass('bottom-full'), { category: 'layout', value: 'bottom:full' });
48
+ assert.deepStrictEqual(convertClass('left-1/2'), { category: 'layout', value: 'left:half' });
49
+ assert.deepStrictEqual(convertClass('top-1/3'), { category: 'layout', value: 'top:third' });
50
+ assert.deepStrictEqual(convertClass('inset-0'), { category: 'layout', value: 'inset:none' });
51
+ });
52
+
44
53
  it('should convert grid classes', () => {
45
54
  assert.deepStrictEqual(convertClass('grid-cols-3'), { category: 'layout', value: 'grid-cols:3' });
46
55
  assert.deepStrictEqual(convertClass('col-span-2'), { category: 'layout', value: 'col-span:2' });
@@ -49,33 +58,38 @@ describe('convertClass', () => {
49
58
 
50
59
  describe('Spacing classes', () => {
51
60
  it('should convert padding classes', () => {
52
- assert.deepStrictEqual(convertClass('p-4'), { category: 'space', value: 'p:small' });
53
- assert.deepStrictEqual(convertClass('p-8'), { category: 'space', value: 'p:big' });
54
- assert.deepStrictEqual(convertClass('px-4'), { category: 'space', value: 'p-x:small' });
55
- assert.deepStrictEqual(convertClass('py-2'), { category: 'space', value: 'p-y:tiny' });
61
+ assert.deepStrictEqual(convertClass('p-4'), { category: 'space', value: 'p:medium' });
62
+ assert.deepStrictEqual(convertClass('p-8'), { category: 'space', value: 'p:large' });
63
+ assert.deepStrictEqual(convertClass('px-4'), { category: 'space', value: 'p-x:medium' });
64
+ assert.deepStrictEqual(convertClass('py-2'), { category: 'space', value: 'p-y:small' });
56
65
  });
57
66
 
58
67
  it('should convert margin classes', () => {
59
- assert.deepStrictEqual(convertClass('m-4'), { category: 'space', value: 'm:small' });
60
- assert.deepStrictEqual(convertClass('mt-8'), { category: 'space', value: 'm-t:big' });
68
+ assert.deepStrictEqual(convertClass('m-4'), { category: 'space', value: 'm:medium' });
69
+ assert.deepStrictEqual(convertClass('mt-8'), { category: 'space', value: 'm-t:large' });
61
70
  assert.deepStrictEqual(convertClass('mx-auto'), { category: 'space', value: 'm-x:auto' });
62
71
  });
63
72
 
64
73
  it('should convert gap classes', () => {
65
- assert.deepStrictEqual(convertClass('gap-4'), { category: 'space', value: 'g:small' });
66
- assert.deepStrictEqual(convertClass('gap-x-2'), { category: 'space', value: 'g-x:tiny' });
74
+ assert.deepStrictEqual(convertClass('gap-4'), { category: 'space', value: 'g:medium' });
75
+ assert.deepStrictEqual(convertClass('gap-x-2'), { category: 'space', value: 'g-x:small' });
67
76
  });
68
77
 
69
78
  it('should convert width/height classes', () => {
70
- assert.deepStrictEqual(convertClass('w-full'), { category: 'space', value: 'w:[100%]' });
79
+ assert.deepStrictEqual(convertClass('w-full'), { category: 'space', value: 'w:full' });
80
+ assert.deepStrictEqual(convertClass('w-1/2'), { category: 'space', value: 'w:half' });
81
+ assert.deepStrictEqual(convertClass('w-1/3'), { category: 'space', value: 'w:third' });
82
+ assert.deepStrictEqual(convertClass('w-2/3'), { category: 'space', value: 'w:third-2x' });
83
+ assert.deepStrictEqual(convertClass('w-1/4'), { category: 'space', value: 'w:quarter' });
84
+ assert.deepStrictEqual(convertClass('w-3/4'), { category: 'space', value: 'w:quarter-3x' });
71
85
  assert.deepStrictEqual(convertClass('h-screen'), { category: 'space', value: 'h:[100vh]' });
72
- assert.deepStrictEqual(convertClass('max-w-4'), { category: 'space', value: 'max-w:small' });
86
+ assert.deepStrictEqual(convertClass('max-w-4'), { category: 'space', value: 'max-w:medium' });
73
87
  });
74
88
 
75
89
  it('should convert negative margin classes', () => {
76
90
  // Standard exact=false
77
- assert.deepStrictEqual(convertClass('-m-4'), { category: 'space', value: 'm:-small' });
78
- assert.deepStrictEqual(convertClass('-mt-8'), { category: 'space', value: 'm-t:-big' });
91
+ assert.deepStrictEqual(convertClass('-m-4'), { category: 'space', value: 'm:-medium' });
92
+ assert.deepStrictEqual(convertClass('-mt-8'), { category: 'space', value: 'm-t:-large' });
79
93
 
80
94
  // Exact=true
81
95
  assert.deepStrictEqual(convertClass('-m-4', { exact: true }), { category: 'space', value: 'm:-tw-4' });
@@ -92,6 +106,14 @@ describe('convertClass', () => {
92
106
  assert.deepStrictEqual(convertClass('bg-transparent'), { category: 'visual', value: 'bg:transparent' });
93
107
  });
94
108
 
109
+ it('should convert border color classes', () => {
110
+ assert.deepStrictEqual(convertClass('border-gray-900'), { category: 'visual', value: 'border:gray-900' });
111
+ assert.deepStrictEqual(convertClass('border-t-gray-900'), { category: 'visual', value: 'border-t:gray-900' });
112
+ assert.deepStrictEqual(convertClass('border-b-blue-500'), { category: 'visual', value: 'border-b:blue-500' });
113
+ assert.deepStrictEqual(convertClass('border-l-red-300'), { category: 'visual', value: 'border-l:red-300' });
114
+ assert.deepStrictEqual(convertClass('border-r-transparent'), { category: 'visual', value: 'border-r:transparent' });
115
+ });
116
+
95
117
  it('should convert text color classes', () => {
96
118
  assert.deepStrictEqual(convertClass('text-white'), { category: 'visual', value: 'text:white' });
97
119
  assert.deepStrictEqual(convertClass('text-gray-700'), { category: 'visual', value: 'text:gray-700' });
@@ -149,12 +171,19 @@ describe('convertClass', () => {
149
171
  assert.deepStrictEqual(convertClass('via-purple-500'), { category: 'visual', value: 'via:purple-500' });
150
172
  assert.deepStrictEqual(convertClass('to-pink-500'), { category: 'visual', value: 'to:pink-500' });
151
173
  });
174
+
175
+ it('should convert translate utilities with fractions', () => {
176
+ assert.deepStrictEqual(convertClass('translate-x-1/2'), { category: 'visual', value: 'translate-x:half' });
177
+ assert.deepStrictEqual(convertClass('translate-y-full'), { category: 'visual', value: 'translate-y:full' });
178
+ assert.deepStrictEqual(convertClass('-translate-x-1/2'), { category: 'visual', value: 'translate-x:-half' });
179
+ assert.deepStrictEqual(convertClass('-translate-y-full'), { category: 'visual', value: 'translate-y:-full' });
180
+ });
152
181
  });
153
182
 
154
183
  describe('Prefixed classes', () => {
155
184
  it('should handle responsive prefixes', () => {
156
185
  assert.deepStrictEqual(convertClass('md:flex'), { category: 'layout', value: 'tw-md:flex' });
157
- assert.deepStrictEqual(convertClass('lg:p-8'), { category: 'space', value: 'tw-lg:p:big' });
186
+ assert.deepStrictEqual(convertClass('lg:p-8'), { category: 'space', value: 'tw-lg:p:large' });
158
187
  });
159
188
 
160
189
  it('should handle dark mode prefix', () => {
@@ -189,7 +218,7 @@ describe('convertClasses', () => {
189
218
  const result = convertClasses('flex items-center p-4 bg-blue-500 text-white');
190
219
 
191
220
  assert.deepStrictEqual(result.layout, ['flex', 'items:center']);
192
- assert.deepStrictEqual(result.space, ['p:small']);
221
+ assert.deepStrictEqual(result.space, ['p:medium']);
193
222
  assert.deepStrictEqual(result.visual, ['bg:blue-500', 'text:white']);
194
223
  assert.deepStrictEqual(result.unrecognized, []);
195
224
  });
@@ -207,7 +236,7 @@ describe('convertHTML', () => {
207
236
  const result = convertHTML(input);
208
237
 
209
238
  assert.ok(result.includes('layout="flex items:center"'));
210
- assert.ok(result.includes('space="p:small"'));
239
+ assert.ok(result.includes('space="p:medium"'));
211
240
  assert.ok(result.includes('visual="bg:blue-500"'));
212
241
  assert.ok(!result.includes('class='));
213
242
  });
@@ -237,17 +266,17 @@ describe('convertHTML', () => {
237
266
  const result = convertHTML(input);
238
267
 
239
268
  assert.ok(result.includes('layout="flex"'));
240
- assert.ok(result.includes('space="p:small"'));
269
+ assert.ok(result.includes('space="p:medium"'));
241
270
  });
242
271
  });
243
272
 
244
273
  describe('spacingScale', () => {
245
274
  it('should have expected scale mappings', () => {
246
275
  assert.strictEqual(spacingScale['0'], 'none');
247
- assert.strictEqual(spacingScale['4'], 'small');
248
- assert.strictEqual(spacingScale['8'], 'big');
249
- assert.strictEqual(spacingScale['12'], 'giant');
250
- assert.strictEqual(spacingScale['24'], 'vast');
276
+ assert.strictEqual(spacingScale['4'], 'medium');
277
+ assert.strictEqual(spacingScale['8'], 'large');
278
+ assert.strictEqual(spacingScale['12'], 'big');
279
+ assert.strictEqual(spacingScale['24'], 'giant');
251
280
  assert.strictEqual(spacingScale['auto'], 'auto');
252
281
  });
253
282
  });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * SenangStart CSS - Tokenizer Core Unit Tests
3
+ */
4
+
5
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert';
7
+ import { tokenize, isValidToken } from '../../../src/core/tokenizer-core.js';
8
+
9
+ describe('Tokenizer Core', () => {
10
+
11
+ describe('isValidToken', () => {
12
+
13
+ it('returns false for missing property', () => {
14
+ assert.strictEqual(isValidToken({ value: 'val' }), false);
15
+ });
16
+
17
+ it('returns false for non-string property', () => {
18
+ assert.strictEqual(isValidToken({ property: 123, value: 'val' }), false);
19
+ });
20
+
21
+ it('returns false for too long property', () => {
22
+ assert.strictEqual(isValidToken({ property: 'a'.repeat(101), value: 'val' }), false);
23
+ });
24
+
25
+ it('returns false for non-string value', () => {
26
+ assert.strictEqual(isValidToken({ property: 'prop', value: 123 }), false);
27
+ });
28
+
29
+ it('returns false for too long value', () => {
30
+ assert.strictEqual(isValidToken({ property: 'prop', value: 'a'.repeat(501) }), false);
31
+ });
32
+
33
+ it('returns false for invalid breakpoint', () => {
34
+ assert.strictEqual(isValidToken({ property: 'prop', value: 'val', breakpoint: 'unknown' }), false);
35
+ });
36
+
37
+ it('returns false for invalid state', () => {
38
+ assert.strictEqual(isValidToken({ property: 'prop', value: 'val', state: 'unknown' }), false);
39
+ });
40
+
41
+ it('returns true for valid minimal token', () => {
42
+ assert.strictEqual(isValidToken({ property: 'prop', value: 'val' }), true);
43
+ });
44
+
45
+ it('returns true for valid full token', () => {
46
+ assert.strictEqual(isValidToken({
47
+ property: 'prop',
48
+ value: 'val',
49
+ breakpoint: 'tab',
50
+ state: 'hover'
51
+ }), true);
52
+ });
53
+
54
+ it('returns true for null value', () => {
55
+ assert.strictEqual(isValidToken({ property: 'prop', value: null }), true);
56
+ });
57
+ });
58
+
59
+ describe('tokenize - Validation Edge Cases', () => {
60
+
61
+ it('returns error for non-string raw input', () => {
62
+ const token = tokenize(123, 'space');
63
+ assert.strictEqual(token.error, 'Invalid token format');
64
+ });
65
+
66
+ it('returns error for empty raw input', () => {
67
+ const token = tokenize('', 'space');
68
+ assert.strictEqual(token.error, 'Invalid token format');
69
+ });
70
+
71
+ it('returns error for too long raw input', () => {
72
+ const token = tokenize('a'.repeat(201), 'space');
73
+ assert.strictEqual(token.error, 'Invalid token format');
74
+ });
75
+
76
+ it('returns error for invalid token structure', () => {
77
+ // unknown:bg:white -> unknown is not a breakpoint, so it becomes property 'unknown'
78
+ // but if we force an invalid state or something
79
+ const token = tokenize('unknown:state:prop:val', 'space');
80
+ // wait, tokenize doesn't throw on unknown prefixes unless validated by isValidToken
81
+ // if parts[0] is not a breakpoint, it's not shifted.
82
+
83
+ const invalidToken = tokenize('prop:val:too:many:parts', 'space');
84
+ // This will still be a valid token structure but might not be what's expected.
85
+
86
+ // Force isValidToken to fail by using a reserved word that isn't a breakpoint or state in a prefix position
87
+ // but since tokenize logic only checks BREAKPOINTS.includes(parts[0]), it defaults to property.
88
+
89
+ // Let's test a case where it results in an invalid token structure
90
+ const longProp = 'a'.repeat(101);
91
+ const tokenLong = tokenize(`${longProp}:val`, 'space');
92
+ assert.strictEqual(tokenLong.error, 'Invalid token structure');
93
+ });
94
+
95
+ it('handles single part tokens for space/visual attributes', () => {
96
+ const token = tokenize('single', 'space');
97
+ assert.strictEqual(token.property, 'single');
98
+ assert.strictEqual(token.value, 'single');
99
+ });
100
+ });
101
+
102
+ });