@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,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
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
getAllDefinitions,
|
|
5
|
+
getDefinitionsByCategory,
|
|
6
|
+
getDefinition,
|
|
7
|
+
validateDefinitions,
|
|
8
|
+
buildAllMaps
|
|
9
|
+
} from '../../../src/definitions/index.js';
|
|
10
|
+
|
|
11
|
+
describe('Utility Definitions Index', () => {
|
|
12
|
+
describe('getAllDefinitions', () => {
|
|
13
|
+
it('returns a flat array of all definitions', () => {
|
|
14
|
+
const allDefs = getAllDefinitions();
|
|
15
|
+
assert.ok(Array.isArray(allDefs));
|
|
16
|
+
assert.ok(allDefs.length > 0);
|
|
17
|
+
// Verify some known definitions are present
|
|
18
|
+
const names = allDefs.map(d => d.name);
|
|
19
|
+
assert.ok(names.includes('display'));
|
|
20
|
+
assert.ok(names.includes('padding'));
|
|
21
|
+
assert.ok(names.includes('text-color'));
|
|
22
|
+
assert.ok(names.includes('background-color'));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('getDefinitionsByCategory', () => {
|
|
27
|
+
it('returns definitions grouped by category', () => {
|
|
28
|
+
const byCategory = getDefinitionsByCategory();
|
|
29
|
+
assert.ok(byCategory.layout);
|
|
30
|
+
assert.ok(byCategory.space);
|
|
31
|
+
assert.ok(byCategory.visual);
|
|
32
|
+
assert.ok(Array.isArray(byCategory.layout));
|
|
33
|
+
assert.ok(Array.isArray(byCategory.space));
|
|
34
|
+
assert.ok(Array.isArray(byCategory.visual));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getDefinition', () => {
|
|
39
|
+
it('returns a specific definition by name', () => {
|
|
40
|
+
const def = getDefinition('display');
|
|
41
|
+
assert.strictEqual(def.name, 'display');
|
|
42
|
+
assert.strictEqual(def.category, 'layout');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns null for unknown definitions', () => {
|
|
46
|
+
const def = getDefinition('nonexistent-definition');
|
|
47
|
+
assert.strictEqual(def, null);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('validateDefinitions', () => {
|
|
52
|
+
it('validates that all definitions are complete', () => {
|
|
53
|
+
const result = validateDefinitions();
|
|
54
|
+
assert.strictEqual(result.valid, true);
|
|
55
|
+
assert.strictEqual(result.errors.length, 0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reports missing fields', () => {
|
|
59
|
+
const brokenDefs = [
|
|
60
|
+
{ name: 'broken' } // Missing property, description, descriptionMs, category
|
|
61
|
+
];
|
|
62
|
+
const result = validateDefinitions(brokenDefs);
|
|
63
|
+
assert.strictEqual(result.valid, false);
|
|
64
|
+
assert.ok(result.errors.some(e => e.includes("Missing 'property'")));
|
|
65
|
+
assert.ok(result.errors.some(e => e.includes("Missing 'description'")));
|
|
66
|
+
assert.ok(result.errors.some(e => e.includes("Missing 'descriptionMs'")));
|
|
67
|
+
assert.ok(result.errors.some(e => e.includes("Missing 'category'")));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reports unknown name for missing fields if name is missing', () => {
|
|
71
|
+
const brokenDefs = [
|
|
72
|
+
{ property: 'test' } // Missing name
|
|
73
|
+
];
|
|
74
|
+
const result = validateDefinitions(brokenDefs);
|
|
75
|
+
assert.ok(result.errors.some(e => e.includes("in definition 'unknown'")));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('reports missing examples', () => {
|
|
79
|
+
const brokenDefs = [
|
|
80
|
+
{
|
|
81
|
+
name: 'no-examples',
|
|
82
|
+
property: 'test',
|
|
83
|
+
description: 'test',
|
|
84
|
+
descriptionMs: 'test',
|
|
85
|
+
category: 'test',
|
|
86
|
+
examples: []
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
const result = validateDefinitions(brokenDefs);
|
|
90
|
+
assert.strictEqual(result.valid, false);
|
|
91
|
+
assert.ok(result.errors.some(e => e.includes("Missing examples in definition 'no-examples'")));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('buildAllMaps', () => {
|
|
96
|
+
it('builds all CSS generator maps', () => {
|
|
97
|
+
const maps = buildAllMaps();
|
|
98
|
+
assert.ok(maps.layoutMap);
|
|
99
|
+
assert.ok(maps.spacePropertyMap);
|
|
100
|
+
assert.ok(maps.typographyKeywords);
|
|
101
|
+
|
|
102
|
+
// Check for some expected values
|
|
103
|
+
assert.strictEqual(maps.layoutMap['flex'], 'display: flex;');
|
|
104
|
+
assert.strictEqual(maps.spacePropertyMap['p'], 'padding: var(--s-{value});');
|
|
105
|
+
assert.ok(maps.typographyKeywords['italic'] !== undefined);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { buildLayoutMap } from '../../../src/definitions/layout.js';
|
|
4
|
+
|
|
5
|
+
describe('Layout Definitions', () => {
|
|
6
|
+
describe('buildLayoutMap', () => {
|
|
7
|
+
it('builds a map of layout values to CSS', () => {
|
|
8
|
+
const map = buildLayoutMap();
|
|
9
|
+
assert.ok(typeof map === 'object');
|
|
10
|
+
// Check for some common layout values
|
|
11
|
+
assert.strictEqual(map['flex'], 'display: flex;');
|
|
12
|
+
assert.strictEqual(map['grid'], 'display: grid;');
|
|
13
|
+
assert.strictEqual(map['hidden'], 'display: none;');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('skips range values like "1-12"', () => {
|
|
17
|
+
const map = buildLayoutMap();
|
|
18
|
+
// Values like '1-12' (used in grid-cols etc) should be skipped by buildLayoutMap
|
|
19
|
+
// as they are handled by dynamic generators or special syntax
|
|
20
|
+
assert.strictEqual(map['1-12'], undefined);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('skips mocked range values to hit coverage', () => {
|
|
24
|
+
const mockDefinitions = {
|
|
25
|
+
test: {
|
|
26
|
+
name: 'test',
|
|
27
|
+
syntax: 'layout="["',
|
|
28
|
+
dynamic: false,
|
|
29
|
+
values: [
|
|
30
|
+
{ value: '1-5', css: 'test: {n};' },
|
|
31
|
+
{ value: 'normal', css: 'test: normal;' }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const map = buildLayoutMap(mockDefinitions);
|
|
36
|
+
assert.strictEqual(map['1-5'], undefined);
|
|
37
|
+
assert.strictEqual(map['normal'], 'test: normal;');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { sanitizeValue } from '../../../src/utils/common.js';
|
|
4
|
+
|
|
5
|
+
describe('Common Utilities', () => {
|
|
6
|
+
describe('sanitizeValue', () => {
|
|
7
|
+
it('returns empty string for non-string input', () => {
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
assert.strictEqual(sanitizeValue(null), '');
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
assert.strictEqual(sanitizeValue(undefined), '');
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
assert.strictEqual(sanitizeValue(123), '');
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
assert.strictEqual(sanitizeValue({}), '');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('sanitizes dangerous characters', () => {
|
|
19
|
+
assert.strictEqual(sanitizeValue('color: red;'), 'color: red_');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns the same string if no dangerous characters are present', () => {
|
|
23
|
+
assert.strictEqual(sanitizeValue('red-500'), 'red-500');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
package/scripts/bundle-jit.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Bundle JIT Runtime with esbuild
|
|
5
|
-
* Bundles src/cdn/senangstart-engine.js into a self-contained IIFE for browser use
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as esbuild from 'esbuild';
|
|
9
|
-
import { dirname, join } from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
|
|
12
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const root = join(__dirname, '..');
|
|
14
|
-
|
|
15
|
-
console.log('📦 Bundling JIT runtime...');
|
|
16
|
-
|
|
17
|
-
// Build unminified version
|
|
18
|
-
await esbuild.build({
|
|
19
|
-
entryPoints: [join(root, 'src', 'cdn', 'senangstart-engine.js')],
|
|
20
|
-
bundle: true,
|
|
21
|
-
format: 'iife',
|
|
22
|
-
outfile: join(root, 'dist', 'senangstart-css.js'),
|
|
23
|
-
minify: false,
|
|
24
|
-
banner: {
|
|
25
|
-
js: '/* SenangStart CSS - JIT Runtime v0.2.0 | MIT License */'
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
console.log('✓ Created dist/senangstart-css.js');
|
|
30
|
-
|
|
31
|
-
// Build minified version
|
|
32
|
-
await esbuild.build({
|
|
33
|
-
entryPoints: [join(root, 'src', 'cdn', 'senangstart-engine.js')],
|
|
34
|
-
bundle: true,
|
|
35
|
-
format: 'iife',
|
|
36
|
-
outfile: join(root, 'dist', 'senangstart-css.min.js'),
|
|
37
|
-
minify: true,
|
|
38
|
-
banner: {
|
|
39
|
-
js: '/* SenangStart CSS v0.2.0 | MIT */'
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
console.log('✓ Created dist/senangstart-css.min.js');
|
|
44
|
-
console.log('');
|
|
45
|
-
console.log('📦 Bundle complete!');
|