@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.
- package/dist/senangstart-css.js +2751 -1952
- package/dist/senangstart-css.min.js +266 -225
- package/dist/senangstart-tw.js +440 -77
- package/dist/senangstart-tw.min.js +1 -1
- package/docs/SYNTAX-REFERENCE.md +1731 -1590
- package/docs/guide/configuration.md +2 -2
- package/docs/guide/preflight.md +20 -1
- package/docs/guide/states.md +60 -0
- package/docs/ms/guide/configuration.md +2 -2
- package/docs/ms/guide/preflight.md +19 -0
- package/docs/ms/guide/states.md +60 -0
- package/docs/ms/reference/breakpoints.md +14 -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/ms/reference/visual/border-radius.md +50 -10
- package/docs/ms/reference/visual/contain.md +57 -0
- package/docs/ms/reference/visual/content-visibility.md +53 -0
- package/docs/ms/reference/visual/placeholder-color.md +92 -0
- package/docs/ms/reference/visual/writing-mode.md +53 -0
- package/docs/ms/reference/visual.md +6 -0
- package/docs/public/assets/senangstart-css.min.js +266 -225
- package/docs/public/llms.txt +63 -2
- package/docs/reference/breakpoints.md +14 -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/docs/reference/visual/border-radius.md +50 -10
- package/docs/reference/visual/contain.md +57 -0
- package/docs/reference/visual/content-visibility.md +53 -0
- package/docs/reference/visual/placeholder-color.md +92 -0
- package/docs/reference/visual/writing-mode.md +53 -0
- package/docs/reference/visual.md +7 -0
- package/docs/syntax-reference.json +2185 -2009
- package/package.json +1 -1
- package/public/senangstart.css +1 -1
- package/scripts/convert-tailwind.js +486 -89
- package/scripts/generate-docs.js +403 -403
- package/scripts/generate-llms-txt.js +28 -0
- package/src/cdn/senangstart-engine.js +37 -1927
- package/src/cdn/tw-conversion-engine.js +504 -78
- package/src/cli/commands/build.js +10 -0
- package/src/compiler/generators/css.js +400 -67
- package/src/compiler/generators/preflight.js +26 -13
- package/src/compiler/generators/typescript.js +3 -1
- package/src/compiler/index.js +27 -3
- package/src/compiler/parser.js +24 -7
- package/src/config/defaults.js +4 -1
- package/src/core/constants.js +5 -3
- package/src/definitions/index.js +7 -3
- package/src/definitions/layout.js +2 -2
- package/src/definitions/space.js +45 -19
- package/src/definitions/visual-performance.js +126 -0
- package/src/definitions/visual.js +25 -9
- package/src/index.js +47 -0
- package/src/utils/common.js +17 -5
- 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 +1520 -6
- 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 +61 -21
- 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: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:
|
|
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' });
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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'], '
|
|
248
|
-
assert.strictEqual(spacingScale['8'], '
|
|
249
|
-
assert.strictEqual(spacingScale['12'], '
|
|
250
|
-
assert.strictEqual(spacingScale['24'], '
|
|
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
|
});
|