@bookklik/senangstart-css 0.2.8 → 0.2.10

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 (72) hide show
  1. package/dist/senangstart-css.js +2751 -1952
  2. package/dist/senangstart-css.min.js +266 -225
  3. package/dist/senangstart-tw.js +440 -77
  4. package/dist/senangstart-tw.min.js +1 -1
  5. package/docs/SYNTAX-REFERENCE.md +1731 -1590
  6. package/docs/guide/configuration.md +2 -2
  7. package/docs/guide/preflight.md +20 -1
  8. package/docs/guide/states.md +60 -0
  9. package/docs/ms/guide/configuration.md +2 -2
  10. package/docs/ms/guide/preflight.md +19 -0
  11. package/docs/ms/guide/states.md +60 -0
  12. package/docs/ms/reference/breakpoints.md +14 -0
  13. package/docs/ms/reference/colors.md +2 -2
  14. package/docs/ms/reference/space/height.md +10 -10
  15. package/docs/ms/reference/space/width.md +12 -12
  16. package/docs/ms/reference/visual/border-radius.md +50 -10
  17. package/docs/ms/reference/visual/contain.md +57 -0
  18. package/docs/ms/reference/visual/content-visibility.md +53 -0
  19. package/docs/ms/reference/visual/placeholder-color.md +92 -0
  20. package/docs/ms/reference/visual/writing-mode.md +53 -0
  21. package/docs/ms/reference/visual.md +6 -0
  22. package/docs/public/assets/senangstart-css.min.js +266 -225
  23. package/docs/public/llms.txt +63 -2
  24. package/docs/reference/breakpoints.md +14 -0
  25. package/docs/reference/colors.md +2 -2
  26. package/docs/reference/space/height.md +10 -10
  27. package/docs/reference/space/width.md +12 -12
  28. package/docs/reference/visual/border-radius.md +50 -10
  29. package/docs/reference/visual/contain.md +57 -0
  30. package/docs/reference/visual/content-visibility.md +53 -0
  31. package/docs/reference/visual/placeholder-color.md +92 -0
  32. package/docs/reference/visual/writing-mode.md +53 -0
  33. package/docs/reference/visual.md +7 -0
  34. package/docs/syntax-reference.json +2185 -2009
  35. package/package.json +1 -1
  36. package/public/senangstart.css +1 -1
  37. package/scripts/convert-tailwind.js +486 -89
  38. package/scripts/generate-docs.js +403 -403
  39. package/scripts/generate-llms-txt.js +28 -0
  40. package/src/cdn/senangstart-engine.js +37 -1927
  41. package/src/cdn/tw-conversion-engine.js +504 -78
  42. package/src/cli/commands/build.js +10 -0
  43. package/src/compiler/generators/css.js +400 -67
  44. package/src/compiler/generators/preflight.js +26 -13
  45. package/src/compiler/generators/typescript.js +3 -1
  46. package/src/compiler/index.js +27 -3
  47. package/src/compiler/parser.js +24 -7
  48. package/src/config/defaults.js +4 -1
  49. package/src/core/constants.js +5 -3
  50. package/src/definitions/index.js +7 -3
  51. package/src/definitions/layout.js +2 -2
  52. package/src/definitions/space.js +45 -19
  53. package/src/definitions/visual-performance.js +126 -0
  54. package/src/definitions/visual.js +25 -9
  55. package/src/index.js +47 -0
  56. package/src/utils/common.js +17 -5
  57. package/templates/senangstart.config.js +1 -1
  58. package/tests/helpers/test-utils.js +1 -1
  59. package/tests/integration/compiler.test.js +12 -1
  60. package/tests/unit/compiler/generators/css.coverage.test.js +833 -0
  61. package/tests/unit/compiler/generators/css.test.js +1520 -6
  62. package/tests/unit/compiler/generators/preflight.test.js +31 -0
  63. package/tests/unit/compiler/parser.test.js +26 -0
  64. package/tests/unit/config/defaults.test.js +2 -2
  65. package/tests/unit/convert-tailwind.cli.test.js +95 -0
  66. package/tests/unit/convert-tailwind.coverage.test.js +225 -0
  67. package/tests/unit/convert-tailwind.test.js +61 -21
  68. package/tests/unit/core/tokenizer-core.test.js +102 -0
  69. package/tests/unit/definitions/index.test.js +108 -0
  70. package/tests/unit/definitions/layout_definitions.test.js +40 -0
  71. package/tests/unit/utils/common.test.js +26 -0
  72. 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:0' });
46
+ assert.deepStrictEqual(convertClass('left-0'), { category: 'layout', value: 'left:0' });
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:0' });
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' });
@@ -105,7 +127,7 @@ describe('convertClass', () => {
105
127
  it('should convert text size classes', () => {
106
128
  assert.deepStrictEqual(convertClass('text-sm'), { category: 'visual', value: 'text-size:small' });
107
129
  assert.deepStrictEqual(convertClass('text-xl'), { category: 'visual', value: 'text-size:big' });
108
- assert.deepStrictEqual(convertClass('text-2xl'), { category: 'visual', value: 'text-size:giant' });
130
+ assert.deepStrictEqual(convertClass('text-2xl'), { category: 'visual', value: 'text-size:huge' });
109
131
  });
110
132
 
111
133
  it('should convert border radius classes', () => {
@@ -114,6 +136,17 @@ describe('convertClass', () => {
114
136
  assert.deepStrictEqual(convertClass('rounded-full'), { category: 'visual', value: 'rounded:round' });
115
137
  });
116
138
 
139
+ it('should convert directional border radius classes', () => {
140
+ assert.deepStrictEqual(convertClass('rounded-t-lg'), { category: 'visual', value: 'rounded-t:medium' });
141
+ assert.deepStrictEqual(convertClass('rounded-b-lg'), { category: 'visual', value: 'rounded-b:medium' });
142
+ assert.deepStrictEqual(convertClass('rounded-l-lg'), { category: 'visual', value: 'rounded-l:medium' });
143
+ assert.deepStrictEqual(convertClass('rounded-r-lg'), { category: 'visual', value: 'rounded-r:medium' });
144
+ assert.deepStrictEqual(convertClass('rounded-tl-3xl'), { category: 'visual', value: 'rounded-tl:big' });
145
+ assert.deepStrictEqual(convertClass('rounded-tr-3xl'), { category: 'visual', value: 'rounded-tr:big' });
146
+ assert.deepStrictEqual(convertClass('rounded-bl-full'), { category: 'visual', value: 'rounded-bl:round' });
147
+ assert.deepStrictEqual(convertClass('rounded-br-full'), { category: 'visual', value: 'rounded-br:round' });
148
+ });
149
+
117
150
  it('should convert shadow classes', () => {
118
151
  assert.deepStrictEqual(convertClass('shadow'), { category: 'visual', value: 'shadow:small' });
119
152
  assert.deepStrictEqual(convertClass('shadow-md'), { category: 'visual', value: 'shadow:medium' });
@@ -149,12 +182,19 @@ describe('convertClass', () => {
149
182
  assert.deepStrictEqual(convertClass('via-purple-500'), { category: 'visual', value: 'via:purple-500' });
150
183
  assert.deepStrictEqual(convertClass('to-pink-500'), { category: 'visual', value: 'to:pink-500' });
151
184
  });
185
+
186
+ it('should convert translate utilities with fractions', () => {
187
+ assert.deepStrictEqual(convertClass('translate-x-1/2'), { category: 'visual', value: 'translate-x:half' });
188
+ assert.deepStrictEqual(convertClass('translate-y-full'), { category: 'visual', value: 'translate-y:full' });
189
+ assert.deepStrictEqual(convertClass('-translate-x-1/2'), { category: 'visual', value: 'translate-x:-half' });
190
+ assert.deepStrictEqual(convertClass('-translate-y-full'), { category: 'visual', value: 'translate-y:-full' });
191
+ });
152
192
  });
153
193
 
154
194
  describe('Prefixed classes', () => {
155
195
  it('should handle responsive prefixes', () => {
156
196
  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' });
197
+ assert.deepStrictEqual(convertClass('lg:p-8'), { category: 'space', value: 'tw-lg:p:large' });
158
198
  });
159
199
 
160
200
  it('should handle dark mode prefix', () => {
@@ -189,7 +229,7 @@ describe('convertClasses', () => {
189
229
  const result = convertClasses('flex items-center p-4 bg-blue-500 text-white');
190
230
 
191
231
  assert.deepStrictEqual(result.layout, ['flex', 'items:center']);
192
- assert.deepStrictEqual(result.space, ['p:small']);
232
+ assert.deepStrictEqual(result.space, ['p:medium']);
193
233
  assert.deepStrictEqual(result.visual, ['bg:blue-500', 'text:white']);
194
234
  assert.deepStrictEqual(result.unrecognized, []);
195
235
  });
@@ -207,7 +247,7 @@ describe('convertHTML', () => {
207
247
  const result = convertHTML(input);
208
248
 
209
249
  assert.ok(result.includes('layout="flex items:center"'));
210
- assert.ok(result.includes('space="p:small"'));
250
+ assert.ok(result.includes('space="p:medium"'));
211
251
  assert.ok(result.includes('visual="bg:blue-500"'));
212
252
  assert.ok(!result.includes('class='));
213
253
  });
@@ -237,17 +277,17 @@ describe('convertHTML', () => {
237
277
  const result = convertHTML(input);
238
278
 
239
279
  assert.ok(result.includes('layout="flex"'));
240
- assert.ok(result.includes('space="p:small"'));
280
+ assert.ok(result.includes('space="p:medium"'));
241
281
  });
242
282
  });
243
283
 
244
284
  describe('spacingScale', () => {
245
285
  it('should have expected scale mappings', () => {
246
286
  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');
287
+ assert.strictEqual(spacingScale['4'], 'medium');
288
+ assert.strictEqual(spacingScale['8'], 'large');
289
+ assert.strictEqual(spacingScale['12'], 'big');
290
+ assert.strictEqual(spacingScale['24'], 'giant');
251
291
  assert.strictEqual(spacingScale['auto'], 'auto');
252
292
  });
253
293
  });