@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.
- package/dist/senangstart-css.js +2510 -1927
- package/dist/senangstart-css.min.js +211 -208
- package/dist/senangstart-tw.js +170 -73
- package/dist/senangstart-tw.min.js +1 -1
- package/docs/guide/configuration.md +2 -2
- package/docs/guide/states.md +60 -0
- package/docs/ms/guide/configuration.md +2 -2
- package/docs/ms/guide/states.md +60 -0
- package/docs/ms/reference/colors.md +2 -2
- package/docs/ms/reference/space/height.md +10 -10
- package/docs/ms/reference/space/width.md +12 -12
- package/docs/public/assets/senangstart-css.min.js +211 -208
- package/docs/public/llms.txt +28 -0
- package/docs/reference/colors.md +2 -2
- package/docs/reference/space/height.md +10 -10
- package/docs/reference/space/width.md +12 -12
- package/package.json +1 -1
- package/public/senangstart.css +1 -1
- package/scripts/convert-tailwind.js +191 -68
- package/scripts/generate-llms-txt.js +28 -0
- package/src/cdn/senangstart-engine.js +37 -1927
- package/src/cdn/tw-conversion-engine.js +203 -74
- package/src/compiler/generators/css.js +300 -54
- package/src/compiler/parser.js +14 -4
- package/src/config/defaults.js +1 -1
- package/src/core/constants.js +5 -3
- package/src/definitions/index.js +3 -2
- package/src/definitions/layout.js +2 -2
- package/src/definitions/space.js +45 -19
- package/src/index.js +47 -0
- package/templates/senangstart.config.js +1 -1
- package/tests/helpers/test-utils.js +1 -1
- package/tests/integration/compiler.test.js +12 -1
- package/tests/unit/compiler/generators/css.coverage.test.js +833 -0
- package/tests/unit/compiler/generators/css.test.js +1418 -1
- package/tests/unit/compiler/generators/preflight.test.js +31 -0
- package/tests/unit/compiler/parser.test.js +26 -0
- package/tests/unit/config/defaults.test.js +2 -2
- package/tests/unit/convert-tailwind.cli.test.js +95 -0
- package/tests/unit/convert-tailwind.coverage.test.js +225 -0
- package/tests/unit/convert-tailwind.test.js +49 -20
- package/tests/unit/core/tokenizer-core.test.js +102 -0
- package/tests/unit/definitions/index.test.js +108 -0
- package/tests/unit/definitions/layout_definitions.test.js +40 -0
- package/tests/unit/utils/common.test.js +26 -0
- 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': '#
|
|
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, '#
|
|
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:
|
|
53
|
-
assert.deepStrictEqual(convertClass('p-8'), { category: 'space', value: 'p:
|
|
54
|
-
assert.deepStrictEqual(convertClass('px-4'), { category: 'space', value: 'p-x:
|
|
55
|
-
assert.deepStrictEqual(convertClass('py-2'), { category: 'space', value: 'p-y:
|
|
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:
|
|
60
|
-
assert.deepStrictEqual(convertClass('mt-8'), { category: 'space', value: 'm-t:
|
|
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:
|
|
66
|
-
assert.deepStrictEqual(convertClass('gap-x-2'), { category: 'space', value: 'g-x:
|
|
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:
|
|
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:
|
|
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:-
|
|
78
|
-
assert.deepStrictEqual(convertClass('-mt-8'), { category: 'space', value: 'm-t:-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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'], '
|
|
248
|
-
assert.strictEqual(spacingScale['8'], '
|
|
249
|
-
assert.strictEqual(spacingScale['12'], '
|
|
250
|
-
assert.strictEqual(spacingScale['24'], '
|
|
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
|
+
});
|