@bookklik/senangstart-css 0.2.9 → 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 +254 -38
- package/dist/senangstart-css.min.js +191 -153
- package/dist/senangstart-tw.js +271 -5
- package/dist/senangstart-tw.min.js +1 -1
- package/docs/SYNTAX-REFERENCE.md +1731 -1590
- package/docs/guide/preflight.md +20 -1
- package/docs/ms/guide/preflight.md +19 -0
- package/docs/ms/reference/breakpoints.md +14 -0
- 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 +191 -153
- package/docs/public/llms.txt +35 -2
- package/docs/reference/breakpoints.md +14 -0
- 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/scripts/convert-tailwind.js +300 -26
- package/scripts/generate-docs.js +403 -403
- package/src/cdn/senangstart-engine.js +5 -5
- package/src/cdn/tw-conversion-engine.js +302 -5
- package/src/cli/commands/build.js +10 -0
- package/src/compiler/generators/css.js +102 -15
- 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 +13 -6
- package/src/config/defaults.js +3 -0
- package/src/definitions/index.js +4 -1
- package/src/definitions/visual-performance.js +126 -0
- package/src/definitions/visual.js +25 -9
- package/src/utils/common.js +17 -5
- package/tests/unit/compiler/generators/css.test.js +102 -5
- package/tests/unit/convert-tailwind.test.js +15 -4
|
@@ -19,7 +19,9 @@ export function generateTypeScript(config) {
|
|
|
19
19
|
|
|
20
20
|
// Generate radius unions
|
|
21
21
|
const radiusKeys = Object.keys(theme.radius);
|
|
22
|
-
const
|
|
22
|
+
const directions = ['t', 'b', 'l', 'r', 'tl', 'tr', 'bl', 'br'];
|
|
23
|
+
const roundedUnions = radiusKeys.map(k => `'rounded:${k}'`).join(' | ') +
|
|
24
|
+
' | ' + directions.flatMap(d => radiusKeys.map(k => `'rounded-${d}:${k}'`)).join(' | ');
|
|
23
25
|
|
|
24
26
|
// Generate shadow unions
|
|
25
27
|
const shadowKeys = Object.keys(theme.shadow);
|
package/src/compiler/index.js
CHANGED
|
@@ -18,11 +18,24 @@ import { generateTypeScript } from './generators/typescript.js';
|
|
|
18
18
|
export function compileSource(content, config) {
|
|
19
19
|
const parsed = parseSource(content);
|
|
20
20
|
const tokens = tokenizeAll(parsed);
|
|
21
|
+
|
|
22
|
+
// Check for invalid tokens and log warnings
|
|
23
|
+
const invalidTokens = tokens.filter(token => token.error);
|
|
24
|
+
if (invalidTokens.length > 0) {
|
|
25
|
+
if (typeof console !== 'undefined') {
|
|
26
|
+
console.warn(`\n${invalidTokens.length} error(s) found in source:`);
|
|
27
|
+
for (const token of invalidTokens) {
|
|
28
|
+
console.warn(` • ${token.raw} (${token.attrType}): ${token.error}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
const css = generateCSS(tokens, config);
|
|
22
34
|
|
|
23
35
|
return {
|
|
24
36
|
tokens,
|
|
25
37
|
css,
|
|
38
|
+
errors: invalidTokens.length > 0 ? invalidTokens : null,
|
|
26
39
|
minifiedCSS: config.output?.minify ? minifyCSS(css) : null
|
|
27
40
|
};
|
|
28
41
|
}
|
|
@@ -36,14 +49,25 @@ export function compileSource(content, config) {
|
|
|
36
49
|
export function compileMultiple(files, config) {
|
|
37
50
|
const parsed = parseMultipleSources(files);
|
|
38
51
|
const tokens = tokenizeAll(parsed);
|
|
52
|
+
|
|
53
|
+
// Check for invalid tokens and log warnings
|
|
54
|
+
const invalidTokens = tokens.filter(token => token.error);
|
|
55
|
+
if (invalidTokens.length > 0) {
|
|
56
|
+
if (typeof console !== 'undefined') {
|
|
57
|
+
console.warn(`\n${invalidTokens.length} error(s) found in source:`);
|
|
58
|
+
for (const token of invalidTokens) {
|
|
59
|
+
console.warn(` • ${token.raw} (${token.attrType}): ${token.error}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
const css = generateCSS(tokens, config);
|
|
40
65
|
|
|
41
66
|
return {
|
|
42
67
|
tokens,
|
|
43
68
|
css,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
typescript: generateTypeScript(config)
|
|
69
|
+
errors: invalidTokens.length > 0 ? invalidTokens : null,
|
|
70
|
+
minifiedCSS: config.output?.minify ? minifyCSS(css) : null
|
|
47
71
|
};
|
|
48
72
|
}
|
|
49
73
|
|
package/src/compiler/parser.js
CHANGED
|
@@ -18,11 +18,11 @@ const ATTRIBUTE_PATTERNS = {
|
|
|
18
18
|
*/
|
|
19
19
|
function createAttributePatterns() {
|
|
20
20
|
return {
|
|
21
|
-
layout: /layout\s*=\s*["'
|
|
22
|
-
space: /space\s*=\s*["'
|
|
23
|
-
visual: /visual\s*=\s*["'
|
|
24
|
-
interact: /interact\s*=\s*["'
|
|
25
|
-
listens: /listens\s*=\s*["'
|
|
21
|
+
layout: /layout\s*=\s*("[^"]*"|'[^']*')/g,
|
|
22
|
+
space: /space\s*=\s*("[^"]*"|'[^']*')/g,
|
|
23
|
+
visual: /visual\s*=\s*("[^"]*"|'[^']*')/g,
|
|
24
|
+
interact: /interact\s*=\s*("[^"]*"|'[^']*')/g,
|
|
25
|
+
listens: /listens\s*=\s*("[^"]*"|'[^']*')/g
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -45,7 +45,14 @@ export function parseSource(content) {
|
|
|
45
45
|
for (const [attr, pattern] of Object.entries(patterns)) {
|
|
46
46
|
let match;
|
|
47
47
|
while ((match = pattern.exec(content)) !== null) {
|
|
48
|
-
|
|
48
|
+
let value = match[1].trim();
|
|
49
|
+
if (value.length >= 2) {
|
|
50
|
+
const firstChar = value[0];
|
|
51
|
+
const lastChar = value[value.length - 1];
|
|
52
|
+
if ((firstChar === '"' && lastChar === '"') || (firstChar === "'" && lastChar === "'")) {
|
|
53
|
+
value = value.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
49
56
|
if (value.length > 10000) {
|
|
50
57
|
continue;
|
|
51
58
|
}
|
package/src/config/defaults.js
CHANGED
|
@@ -138,6 +138,7 @@ export const defaultConfig = {
|
|
|
138
138
|
'tab': '768px', // Tablet
|
|
139
139
|
'lap': '1024px', // Laptop
|
|
140
140
|
'desk': '1280px', // Desktop
|
|
141
|
+
'print': 'print', // Print media query
|
|
141
142
|
|
|
142
143
|
// Tailwind Compatibility
|
|
143
144
|
'tw-sm': '640px',
|
|
@@ -148,6 +149,8 @@ export const defaultConfig = {
|
|
|
148
149
|
},
|
|
149
150
|
|
|
150
151
|
// 7. COLORS: Palette Scales
|
|
152
|
+
// Placeholder color for form inputs
|
|
153
|
+
placeholder: '#9ca3af',
|
|
151
154
|
colors: {
|
|
152
155
|
// Base colors
|
|
153
156
|
'white': '#FFFFFF',
|
package/src/definitions/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import transformDefinitions from './visual-transforms.js';
|
|
|
16
16
|
import borderDefinitions from './visual-borders.js';
|
|
17
17
|
import divideDefinitions from './visual-divide.js';
|
|
18
18
|
import svgDefinitions from './visual-svg.js';
|
|
19
|
+
import performanceDefinitions from './visual-performance.js';
|
|
19
20
|
|
|
20
21
|
// Import split layout definitions
|
|
21
22
|
import flexDefinitions from './layout-flex.js';
|
|
@@ -37,7 +38,8 @@ const allVisualDefinitions = {
|
|
|
37
38
|
...transformDefinitions,
|
|
38
39
|
...borderDefinitions,
|
|
39
40
|
...divideDefinitions,
|
|
40
|
-
...svgDefinitions
|
|
41
|
+
...svgDefinitions,
|
|
42
|
+
...performanceDefinitions
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
// Re-export all definitions
|
|
@@ -54,6 +56,7 @@ export { transformDefinitions } from './visual-transforms.js';
|
|
|
54
56
|
export { borderDefinitions } from './visual-borders.js';
|
|
55
57
|
export { divideDefinitions } from './visual-divide.js';
|
|
56
58
|
export { svgDefinitions } from './visual-svg.js';
|
|
59
|
+
export { performanceDefinitions } from './visual-performance.js';
|
|
57
60
|
|
|
58
61
|
// Re-export split layout definitions
|
|
59
62
|
export { flexDefinitions } from './layout-flex.js';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Performance Utility Definitions
|
|
3
|
+
* Content visibility, contain, and other performance optimizations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ======================
|
|
7
|
+
// CONTENT VISIBILITY
|
|
8
|
+
// ======================
|
|
9
|
+
|
|
10
|
+
export const contentVisibility = {
|
|
11
|
+
name: 'content-visibility',
|
|
12
|
+
property: 'visual',
|
|
13
|
+
syntax: 'visual="content-visibility:[value]"',
|
|
14
|
+
description: 'Optimize rendering by skipping off-screen content',
|
|
15
|
+
descriptionMs: 'Optimumkan rendering dengan melangkau kandungan luar skrin',
|
|
16
|
+
category: 'visual',
|
|
17
|
+
values: [
|
|
18
|
+
{ value: 'visible', css: 'content-visibility: visible;', description: 'Render all content', descriptionMs: 'Render semua kandungan' },
|
|
19
|
+
{ value: 'auto', css: 'content-visibility: auto;', description: 'Skip when off-screen', descriptionMs: 'Langkau bila luar skrin' },
|
|
20
|
+
{ value: 'hidden', css: 'content-visibility: hidden;', description: 'Never render off-screen', descriptionMs: 'Jangan render luar skrin' }
|
|
21
|
+
],
|
|
22
|
+
examples: [
|
|
23
|
+
{ code: '<section visual="content-visibility:auto">Large list</section>', description: 'Auto-optimize large content' },
|
|
24
|
+
{ code: '<div visual="content-visibility:hidden">Hidden until needed</div>', description: 'Hide until revealed' }
|
|
25
|
+
],
|
|
26
|
+
preview: [
|
|
27
|
+
{
|
|
28
|
+
title: 'Content Visibility',
|
|
29
|
+
titleMs: 'Ketampakan Kandungan',
|
|
30
|
+
description: 'Performance optimization for off-screen content',
|
|
31
|
+
descriptionMs: 'Pengoptimuman prestasi untuk kandungan luar skrin',
|
|
32
|
+
html: `<div layout="flex" space="g:medium p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium">
|
|
33
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">visible</div>
|
|
34
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">auto</div>
|
|
35
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">hidden</div>
|
|
36
|
+
</div>`,
|
|
37
|
+
highlightValue: 'content-visibility:auto'
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ======================
|
|
43
|
+
// CONTAIN
|
|
44
|
+
// ======================
|
|
45
|
+
|
|
46
|
+
export const contain = {
|
|
47
|
+
name: 'contain',
|
|
48
|
+
property: 'visual',
|
|
49
|
+
syntax: 'visual="contain:[value]"',
|
|
50
|
+
description: 'Isolate element rendering for performance',
|
|
51
|
+
descriptionMs: 'Pencil rendering elemen untuk prestasi',
|
|
52
|
+
category: 'visual',
|
|
53
|
+
values: [
|
|
54
|
+
{ value: 'none', css: 'contain: none;', description: 'No containment', descriptionMs: 'Tiada pengandungan' },
|
|
55
|
+
{ value: 'strict', css: 'contain: strict;', description: 'Full containment', descriptionMs: 'Pengandungan penuh' },
|
|
56
|
+
{ value: 'content', css: 'contain: content;', description: 'Content containment', descriptionMs: 'Pengandungan kandungan' },
|
|
57
|
+
{ value: 'size', css: 'contain: size;', description: 'Size containment', descriptionMs: 'Pengandungan saiz' },
|
|
58
|
+
{ value: 'layout', css: 'contain: layout;', description: 'Layout containment', descriptionMs: 'Pengandungan susun atur' },
|
|
59
|
+
{ value: 'style', css: 'contain: style;', description: 'Style containment', descriptionMs: 'Pengandungan gaya' },
|
|
60
|
+
{ value: 'paint', css: 'contain: paint;', description: 'Paint containment', descriptionMs: 'Pengandungan lukis' }
|
|
61
|
+
],
|
|
62
|
+
examples: [
|
|
63
|
+
{ code: '<div visual="contain:strict">Isolated rendering</div>', description: 'Full containment' },
|
|
64
|
+
{ code: '<div visual="contain:content">Content isolation</div>', description: 'Content only' }
|
|
65
|
+
],
|
|
66
|
+
preview: [
|
|
67
|
+
{
|
|
68
|
+
title: 'Contain',
|
|
69
|
+
titleMs: 'Mengandung',
|
|
70
|
+
description: 'Isolate element from rest of page for performance',
|
|
71
|
+
descriptionMs: 'Pencil elemen dari halaman lain untuk prestasi',
|
|
72
|
+
html: `<div layout="flex" space="g:medium p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium">
|
|
73
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">none</div>
|
|
74
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">content</div>
|
|
75
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">strict</div>
|
|
76
|
+
</div>`,
|
|
77
|
+
highlightValue: 'contain:strict'
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ======================
|
|
83
|
+
// WRITING MODE
|
|
84
|
+
// ======================
|
|
85
|
+
|
|
86
|
+
export const writingMode = {
|
|
87
|
+
name: 'writing-mode',
|
|
88
|
+
property: 'visual',
|
|
89
|
+
syntax: 'visual="writing-mode:[value]"',
|
|
90
|
+
description: 'Set writing direction for RTL/vertical text',
|
|
91
|
+
descriptionMs: 'Tetapkan arah penulisan untuk teks RTL/menegak',
|
|
92
|
+
category: 'visual',
|
|
93
|
+
values: [
|
|
94
|
+
{ value: 'horizontal-tb', css: 'writing-mode: horizontal-tb;', description: 'Left to right', descriptionMs: 'Kiri ke kanan' },
|
|
95
|
+
{ value: 'vertical-rl', css: 'writing-mode: vertical-rl;', description: 'Top to bottom RTL', descriptionMs: 'Atas ke bawah RTL' },
|
|
96
|
+
{ value: 'vertical-lr', css: 'writing-mode: vertical-lr;', description: 'Top to bottom LTR', descriptionMs: 'Atas ke bawah LTR' },
|
|
97
|
+
{ value: 'sideways-rl', css: 'writing-mode: sideways-rl;', description: 'Sideways RTL', descriptionMs: 'Menyerong RTL' },
|
|
98
|
+
{ value: 'sideways-lr', css: 'writing-mode: sideways-lr;', description: 'Sideways LTR', descriptionMs: 'Menyerong LTR' }
|
|
99
|
+
],
|
|
100
|
+
examples: [
|
|
101
|
+
{ code: '<div visual="writing-mode:vertical-rl">Vertical text</div>', description: 'Vertical text RTL' },
|
|
102
|
+
{ code: '<div visual="writing-mode:horizontal-tb">Horizontal text</div>', description: 'Horizontal text LTR' }
|
|
103
|
+
],
|
|
104
|
+
preview: [
|
|
105
|
+
{
|
|
106
|
+
title: 'Writing Mode',
|
|
107
|
+
titleMs: 'Mod Penulisan',
|
|
108
|
+
description: 'Control text direction and orientation',
|
|
109
|
+
descriptionMs: 'Kawal arah dan orientasi teks',
|
|
110
|
+
html: `<div layout="flex" space="g:medium p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium">
|
|
111
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">horizontal-tb</div>
|
|
112
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">vertical-rl</div>
|
|
113
|
+
</div>`,
|
|
114
|
+
highlightValue: 'writing-mode:vertical-rl'
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Export all performance definitions
|
|
120
|
+
export const performanceDefinitions = {
|
|
121
|
+
contentVisibility,
|
|
122
|
+
contain,
|
|
123
|
+
writingMode
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default performanceDefinitions;
|
|
@@ -414,11 +414,12 @@ export const lineHeight = {
|
|
|
414
414
|
export const borderRadius = {
|
|
415
415
|
name: 'border-radius',
|
|
416
416
|
property: 'visual',
|
|
417
|
-
syntax: 'visual="rounded:[value]"',
|
|
418
|
-
description: 'Set border radius',
|
|
419
|
-
descriptionMs: 'Tetapkan jejari sempadan',
|
|
417
|
+
syntax: 'visual="rounded:[value]" | visual="rounded-{t|b|l|r|tl|tr|bl|br}:[value]"',
|
|
418
|
+
description: 'Set border radius for all corners or specific corners',
|
|
419
|
+
descriptionMs: 'Tetapkan jejari sempadan untuk semua bucu atau bucu tertentu',
|
|
420
420
|
category: 'visual',
|
|
421
421
|
usesScale: 'radius',
|
|
422
|
+
supportsArbitrary: true,
|
|
422
423
|
values: [
|
|
423
424
|
{ value: 'none', css: 'border-radius: var(--r-none);', description: 'No rounding', descriptionMs: 'Tiada pembulatan' },
|
|
424
425
|
{ value: 'small', css: 'border-radius: var(--r-small);', description: 'Small radius', descriptionMs: 'Jejari kecil' },
|
|
@@ -427,8 +428,10 @@ export const borderRadius = {
|
|
|
427
428
|
{ value: 'round', css: 'border-radius: var(--r-round);', description: 'Fully round', descriptionMs: 'Sepenuhnya bulat' }
|
|
428
429
|
],
|
|
429
430
|
examples: [
|
|
430
|
-
{ code: '<div visual="rounded:medium">Rounded corners</div>', description: '
|
|
431
|
-
{ code: '<div visual="rounded:round">Pill shape</div>', description: '
|
|
431
|
+
{ code: '<div visual="rounded:medium">Rounded corners</div>', description: 'All corners rounded' },
|
|
432
|
+
{ code: '<div visual="rounded:round">Pill shape</div>', description: 'Fully round' },
|
|
433
|
+
{ code: '<div visual="rounded-t:medium">Top rounded</div>', description: 'Top corners only' },
|
|
434
|
+
{ code: '<div visual="rounded-tl:big rounded-br:big">Opposite corners</div>', description: 'Specific corners' }
|
|
432
435
|
],
|
|
433
436
|
footnotes: [
|
|
434
437
|
{
|
|
@@ -446,12 +449,25 @@ export const borderRadius = {
|
|
|
446
449
|
description: 'Round element corners from subtle to pill-shaped',
|
|
447
450
|
descriptionMs: 'Bulatkan sudut elemen dari halus hingga berbentuk pil',
|
|
448
451
|
html: `<div layout="flex" space="g:medium p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium">
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
452
|
+
<div space="p:small" visual="bg:primary text:white rounded:none">none</div>
|
|
453
|
+
<div space="p:small" visual="bg:primary text:white rounded:small">small</div>
|
|
454
|
+
<div space="p:small" visual="bg:primary text:white rounded:medium">medium</div>
|
|
455
|
+
<div space="p:small" visual="bg:primary text:white rounded:round">round</div>
|
|
453
456
|
</div>`,
|
|
454
457
|
highlightValue: 'rounded:medium'
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
title: 'Directional Border Radius',
|
|
461
|
+
titleMs: 'Jejari Sempadan Arah',
|
|
462
|
+
description: 'Round specific corners for unique shapes',
|
|
463
|
+
descriptionMs: 'Bulatkan bucu tertentu untuk bentuk unik',
|
|
464
|
+
html: `<div layout="flex" space="g:medium p:medium" visual="bg:neutral-100 dark:bg:neutral-900 rounded:medium">
|
|
465
|
+
<div space="p:small" visual="bg:primary text:white rounded-t:medium">top</div>
|
|
466
|
+
<div space="p:small" visual="bg:primary text:white rounded-b:medium">bottom</div>
|
|
467
|
+
<div space="p:small" visual="bg:primary text:white rounded-l:medium">left</div>
|
|
468
|
+
<div space="p:small" visual="bg:primary text:white rounded-r:medium">right</div>
|
|
469
|
+
</div>`,
|
|
470
|
+
highlightValue: 'rounded-t:medium'
|
|
455
471
|
}
|
|
456
472
|
]
|
|
457
473
|
};
|
package/src/utils/common.js
CHANGED
|
@@ -14,14 +14,26 @@ export function sanitizeValue(value) {
|
|
|
14
14
|
return '';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
let sanitized = value;
|
|
18
|
+
|
|
17
19
|
// Remove potentially dangerous characters that could break CSS syntax
|
|
18
|
-
// Note: We
|
|
20
|
+
// Note: We don't filter {} because some tests expect them (e.g. content icons like content: "→")
|
|
19
21
|
const dangerousChars = /[;]/g;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
sanitized = sanitized.replace(dangerousChars, '_');
|
|
23
|
+
|
|
24
|
+
// Filter CSS injection attempts via at-rules
|
|
25
|
+
const atRules = /@import|@charset|@namespace|@supports|@keyframes/gi;
|
|
26
|
+
sanitized = sanitized.replace(atRules, '');
|
|
27
|
+
|
|
28
|
+
// Filter expression() (IE vulnerability)
|
|
29
|
+
const expression = /expression\s*\(/gi;
|
|
30
|
+
sanitized = sanitized.replace(expression, '');
|
|
31
|
+
|
|
32
|
+
// Filter javascript: and data: URLs in url() that could execute scripts
|
|
33
|
+
const dangerousUrls = /(url\s*\(\s*['"]?)(javascript:|data:)([^)]*\))/gi;
|
|
34
|
+
sanitized = sanitized.replace(dangerousUrls, '$1about:blank$3');
|
|
23
35
|
|
|
24
|
-
return
|
|
36
|
+
return sanitized;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
export default { sanitizeValue };
|
|
@@ -547,6 +547,96 @@ describe('CSS Generator', () => {
|
|
|
547
547
|
assert.ok(css.includes('border-radius: var(--r-full)'));
|
|
548
548
|
});
|
|
549
549
|
|
|
550
|
+
it('generates rounded-t', () => {
|
|
551
|
+
const token = { property: 'rounded-t', value: 'medium', attrType: 'visual', raw: 'rounded-t:medium' };
|
|
552
|
+
const config = createTestConfig();
|
|
553
|
+
const css = generateCSS([token], config);
|
|
554
|
+
|
|
555
|
+
assert.ok(css.includes('border-top-left-radius: var(--r-medium)'));
|
|
556
|
+
assert.ok(css.includes('border-top-right-radius: var(--r-medium)'));
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('generates rounded-b', () => {
|
|
560
|
+
const token = { property: 'rounded-b', value: 'big', attrType: 'visual', raw: 'rounded-b:big' };
|
|
561
|
+
const config = createTestConfig();
|
|
562
|
+
const css = generateCSS([token], config);
|
|
563
|
+
|
|
564
|
+
assert.ok(css.includes('border-bottom-left-radius: var(--r-big)'));
|
|
565
|
+
assert.ok(css.includes('border-bottom-right-radius: var(--r-big)'));
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('generates rounded-l', () => {
|
|
569
|
+
const token = { property: 'rounded-l', value: 'small', attrType: 'visual', raw: 'rounded-l:small' };
|
|
570
|
+
const config = createTestConfig();
|
|
571
|
+
const css = generateCSS([token], config);
|
|
572
|
+
|
|
573
|
+
assert.ok(css.includes('border-top-left-radius: var(--r-small)'));
|
|
574
|
+
assert.ok(css.includes('border-bottom-left-radius: var(--r-small)'));
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('generates rounded-r', () => {
|
|
578
|
+
const token = { property: 'rounded-r', value: 'round', attrType: 'visual', raw: 'rounded-r:round' };
|
|
579
|
+
const config = createTestConfig();
|
|
580
|
+
const css = generateCSS([token], config);
|
|
581
|
+
|
|
582
|
+
assert.ok(css.includes('border-top-right-radius: var(--r-round)'));
|
|
583
|
+
assert.ok(css.includes('border-bottom-right-radius: var(--r-round)'));
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('generates rounded-tl', () => {
|
|
587
|
+
const token = { property: 'rounded-tl', value: 'medium', attrType: 'visual', raw: 'rounded-tl:medium' };
|
|
588
|
+
const config = createTestConfig();
|
|
589
|
+
const css = generateCSS([token], config);
|
|
590
|
+
|
|
591
|
+
assert.ok(css.includes('border-top-left-radius: var(--r-medium)'));
|
|
592
|
+
assert.ok(!css.includes('border-top-right-radius'));
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('generates rounded-tr', () => {
|
|
596
|
+
const token = { property: 'rounded-tr', value: 'big', attrType: 'visual', raw: 'rounded-tr:big' };
|
|
597
|
+
const config = createTestConfig();
|
|
598
|
+
const css = generateCSS([token], config);
|
|
599
|
+
|
|
600
|
+
assert.ok(css.includes('border-top-right-radius: var(--r-big)'));
|
|
601
|
+
assert.ok(!css.includes('border-top-left-radius'));
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('generates rounded-bl', () => {
|
|
605
|
+
const token = { property: 'rounded-bl', value: 'small', attrType: 'visual', raw: 'rounded-bl:small' };
|
|
606
|
+
const config = createTestConfig();
|
|
607
|
+
const css = generateCSS([token], config);
|
|
608
|
+
|
|
609
|
+
assert.ok(css.includes('border-bottom-left-radius: var(--r-small)'));
|
|
610
|
+
assert.ok(!css.includes('border-bottom-right-radius'));
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('generates rounded-br', () => {
|
|
614
|
+
const token = { property: 'rounded-br', value: 'round', attrType: 'visual', raw: 'rounded-br:round' };
|
|
615
|
+
const config = createTestConfig();
|
|
616
|
+
const css = generateCSS([token], config);
|
|
617
|
+
|
|
618
|
+
assert.ok(css.includes('border-bottom-right-radius: var(--r-round)'));
|
|
619
|
+
assert.ok(!css.includes('border-bottom-left-radius'));
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('generates rounded-t with arbitrary value', () => {
|
|
623
|
+
const token = { property: 'rounded-t', value: '12px', attrType: 'visual', raw: 'rounded-t:[12px]', isArbitrary: true };
|
|
624
|
+
const config = createTestConfig();
|
|
625
|
+
const css = generateCSS([token], config);
|
|
626
|
+
|
|
627
|
+
assert.ok(css.includes('border-top-left-radius: 12px'));
|
|
628
|
+
assert.ok(css.includes('border-top-right-radius: 12px'));
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('generates rounded-tl with arbitrary value', () => {
|
|
632
|
+
const token = { property: 'rounded-tl', value: '8px', attrType: 'visual', raw: 'rounded-tl:[8px]', isArbitrary: true };
|
|
633
|
+
const config = createTestConfig();
|
|
634
|
+
const css = generateCSS([token], config);
|
|
635
|
+
|
|
636
|
+
assert.ok(css.includes('border-top-left-radius: 8px'));
|
|
637
|
+
assert.ok(!css.includes('border-top-right-radius'));
|
|
638
|
+
});
|
|
639
|
+
|
|
550
640
|
});
|
|
551
641
|
|
|
552
642
|
describe('Shadow', () => {
|
|
@@ -581,6 +671,8 @@ describe('CSS Generator', () => {
|
|
|
581
671
|
assert.ok(css.includes('> :not([hidden]) ~ :not([hidden])'));
|
|
582
672
|
assert.ok(css.includes('border-left-color: var(--c-gray-300)'));
|
|
583
673
|
assert.ok(css.includes('border-right-color: var(--c-gray-300)'));
|
|
674
|
+
assert.ok(css.includes('border-left-style: solid'));
|
|
675
|
+
assert.ok(css.includes('border-right-style: solid'));
|
|
584
676
|
});
|
|
585
677
|
|
|
586
678
|
it('generates divide-y', () => {
|
|
@@ -591,6 +683,8 @@ describe('CSS Generator', () => {
|
|
|
591
683
|
assert.ok(css.includes('> :not([hidden]) ~ :not([hidden])'));
|
|
592
684
|
assert.ok(css.includes('border-top-color: var(--c-danger)'));
|
|
593
685
|
assert.ok(css.includes('border-bottom-color: var(--c-danger)'));
|
|
686
|
+
assert.ok(css.includes('border-top-style: solid'));
|
|
687
|
+
assert.ok(css.includes('border-bottom-style: solid'));
|
|
594
688
|
});
|
|
595
689
|
|
|
596
690
|
it('generates divide width', () => {
|
|
@@ -599,7 +693,10 @@ describe('CSS Generator', () => {
|
|
|
599
693
|
const css = generateCSS([token], config);
|
|
600
694
|
|
|
601
695
|
assert.ok(css.includes('> :not([hidden]) ~ :not([hidden])'));
|
|
602
|
-
assert.ok(css.includes('border-width: var(--s-thin)'));
|
|
696
|
+
assert.ok(css.includes('border-top-width: calc(var(--s-thin) * (1 - var(--ss-divide-y-reverse)))'));
|
|
697
|
+
assert.ok(css.includes('border-bottom-width: calc(var(--s-thin) * var(--ss-divide-y-reverse))'));
|
|
698
|
+
assert.ok(css.includes('border-left-width: calc(var(--s-thin) * (1 - var(--ss-divide-x-reverse)))'));
|
|
699
|
+
assert.ok(css.includes('border-right-width: calc(var(--s-thin) * var(--ss-divide-x-reverse))'));
|
|
603
700
|
});
|
|
604
701
|
|
|
605
702
|
it('generates divide-x-w', () => {
|
|
@@ -608,8 +705,8 @@ describe('CSS Generator', () => {
|
|
|
608
705
|
const css = generateCSS([token], config);
|
|
609
706
|
|
|
610
707
|
assert.ok(css.includes('> :not([hidden]) ~ :not([hidden])'));
|
|
611
|
-
assert.ok(css.includes('border-
|
|
612
|
-
assert.ok(css.includes('border-
|
|
708
|
+
assert.ok(css.includes('border-right-width: calc(var(--s-regular) * var(--ss-divide-x-reverse))'));
|
|
709
|
+
assert.ok(css.includes('border-left-width: calc(var(--s-regular) * (1 - var(--ss-divide-x-reverse)))'));
|
|
613
710
|
});
|
|
614
711
|
|
|
615
712
|
it('generates divide-y-w', () => {
|
|
@@ -618,8 +715,8 @@ describe('CSS Generator', () => {
|
|
|
618
715
|
const css = generateCSS([token], config);
|
|
619
716
|
|
|
620
717
|
assert.ok(css.includes('> :not([hidden]) ~ :not([hidden])'));
|
|
621
|
-
assert.ok(css.includes('border-
|
|
622
|
-
assert.ok(css.includes('border-
|
|
718
|
+
assert.ok(css.includes('border-bottom-width: calc(var(--s-thick) * var(--ss-divide-y-reverse))'));
|
|
719
|
+
assert.ok(css.includes('border-top-width: calc(var(--s-thick) * (1 - var(--ss-divide-y-reverse)))'));
|
|
623
720
|
});
|
|
624
721
|
|
|
625
722
|
it('generates divide style', () => {
|
|
@@ -42,12 +42,12 @@ describe('convertClass', () => {
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
it('should convert positional offsets (top, left, right, bottom)', () => {
|
|
45
|
-
assert.deepStrictEqual(convertClass('top-0'), { category: 'layout', value: 'top:
|
|
46
|
-
assert.deepStrictEqual(convertClass('left-0'), { category: 'layout', value: 'left:
|
|
45
|
+
assert.deepStrictEqual(convertClass('top-0'), { category: 'layout', value: 'top:0' });
|
|
46
|
+
assert.deepStrictEqual(convertClass('left-0'), { category: 'layout', value: 'left:0' });
|
|
47
47
|
assert.deepStrictEqual(convertClass('bottom-full'), { category: 'layout', value: 'bottom:full' });
|
|
48
48
|
assert.deepStrictEqual(convertClass('left-1/2'), { category: 'layout', value: 'left:half' });
|
|
49
49
|
assert.deepStrictEqual(convertClass('top-1/3'), { category: 'layout', value: 'top:third' });
|
|
50
|
-
assert.deepStrictEqual(convertClass('inset-0'), { category: 'layout', value: 'inset:
|
|
50
|
+
assert.deepStrictEqual(convertClass('inset-0'), { category: 'layout', value: 'inset:0' });
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it('should convert grid classes', () => {
|
|
@@ -127,7 +127,7 @@ describe('convertClass', () => {
|
|
|
127
127
|
it('should convert text size classes', () => {
|
|
128
128
|
assert.deepStrictEqual(convertClass('text-sm'), { category: 'visual', value: 'text-size:small' });
|
|
129
129
|
assert.deepStrictEqual(convertClass('text-xl'), { category: 'visual', value: 'text-size:big' });
|
|
130
|
-
assert.deepStrictEqual(convertClass('text-2xl'), { category: 'visual', value: 'text-size:
|
|
130
|
+
assert.deepStrictEqual(convertClass('text-2xl'), { category: 'visual', value: 'text-size:huge' });
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
it('should convert border radius classes', () => {
|
|
@@ -136,6 +136,17 @@ describe('convertClass', () => {
|
|
|
136
136
|
assert.deepStrictEqual(convertClass('rounded-full'), { category: 'visual', value: 'rounded:round' });
|
|
137
137
|
});
|
|
138
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
|
+
|
|
139
150
|
it('should convert shadow classes', () => {
|
|
140
151
|
assert.deepStrictEqual(convertClass('shadow'), { category: 'visual', value: 'shadow:small' });
|
|
141
152
|
assert.deepStrictEqual(convertClass('shadow-md'), { category: 'visual', value: 'shadow:medium' });
|