@bookklik/senangstart-css 0.2.6 → 0.2.8
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 +6671 -370
- package/dist/senangstart-css.min.js +1205 -120
- package/dist/senangstart-tw.js +9 -2
- package/dist/senangstart-tw.min.js +1 -1
- package/docs/guide/getting-started.md +6 -0
- package/docs/ms/guide/getting-started.md +6 -0
- package/docs/public/assets/senangstart-css.min.js +1205 -120
- package/docs/public/llms.txt +2 -1
- package/package.json +1 -1
- package/public/senangstart.css +1196 -0
- package/scripts/convert-tailwind.js +12 -2
- package/scripts/generate-llms-txt.js +2 -1
- package/src/cdn/senangstart-engine.js +34 -364
- package/src/cdn/tw-conversion-engine.js +12 -2
- package/src/compiler/generators/css.js +30 -198
- package/src/compiler/tokenizer.js +0 -1
- package/src/core/tokenizer-core.js +3 -58
- package/src/definitions/layout.js +4 -0
- package/src/utils/common.js +27 -0
- package/tests/unit/convert-tailwind.test.js +12 -0
|
@@ -4,22 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { generatePreflight } from './preflight.js';
|
|
7
|
+
import { sanitizeValue } from '../../utils/common.js';
|
|
8
|
+
import { buildAllMaps } from '../../definitions/index.js';
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*/
|
|
10
|
+
// Initialize maps from definitions - Single Source of Truth
|
|
11
|
+
const { layoutMap, typographyKeywords } = buildAllMaps();
|
|
12
|
+
|
|
13
|
+
// Helper to sanitize arbitrary values using common utility
|
|
13
14
|
function sanitizeArbitraryValue(value) {
|
|
14
|
-
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
// Remove potentially dangerous characters that could break CSS syntax
|
|
18
|
-
const dangerousChars = /[;}{]/g;
|
|
19
|
-
if (dangerousChars.test(value)) {
|
|
20
|
-
return value.replace(dangerousChars, '_');
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
15
|
+
return sanitizeValue(value);
|
|
23
16
|
}
|
|
24
17
|
|
|
25
18
|
/**
|
|
@@ -88,8 +81,9 @@ export function generateCSSVariables(config) {
|
|
|
88
81
|
'60': '15rem', '64': '16rem', '72': '18rem', '80': '20rem', '96': '24rem'
|
|
89
82
|
};
|
|
90
83
|
for (const [key, value] of Object.entries(twSpacing)) {
|
|
91
|
-
css += ` --tw-${key}: ${value};\n`;
|
|
84
|
+
css += ` --tw-${key.replace(/\./g, '\\\\.')}: ${value};\n`;
|
|
92
85
|
}
|
|
86
|
+
|
|
93
87
|
|
|
94
88
|
// Tailwind Border Radius Scale
|
|
95
89
|
const twRadius = {
|
|
@@ -161,95 +155,8 @@ export function generateCSSVariables(config) {
|
|
|
161
155
|
function generateLayoutRule(token, config) {
|
|
162
156
|
const { property, value, isArbitrary } = token;
|
|
163
157
|
|
|
164
|
-
const layoutMap = {
|
|
165
|
-
// Display
|
|
166
|
-
'flex': 'display: flex;',
|
|
167
|
-
'grid': 'display: grid;',
|
|
168
|
-
'inline-flex': 'display: inline-flex;',
|
|
169
|
-
'inline-grid': 'display: inline-grid;',
|
|
170
|
-
'block': 'display: block;',
|
|
171
|
-
'inline': 'display: inline-block;',
|
|
172
|
-
'hidden': 'display: none;',
|
|
173
|
-
|
|
174
|
-
// Flex Direction
|
|
175
|
-
'row': 'flex-direction: row;',
|
|
176
|
-
'col': 'flex-direction: column;',
|
|
177
|
-
'row-reverse': 'flex-direction: row-reverse;',
|
|
178
|
-
'col-reverse': 'flex-direction: column-reverse;',
|
|
179
|
-
|
|
180
|
-
// Flex Wrap
|
|
181
|
-
'wrap': 'flex-wrap: wrap;',
|
|
182
|
-
'nowrap': 'flex-wrap: nowrap;',
|
|
183
|
-
'wrap-reverse': 'flex-wrap: wrap-reverse;',
|
|
184
|
-
|
|
185
|
-
// Flex Item
|
|
186
|
-
'grow': 'flex-grow: 1;',
|
|
187
|
-
'grow-0': 'flex-grow: 0;',
|
|
188
|
-
'shrink': 'flex-shrink: 1;',
|
|
189
|
-
'shrink-0': 'flex-shrink: 0;',
|
|
190
|
-
|
|
191
|
-
// Grid Auto Flow
|
|
192
|
-
'grid-flow-row': 'grid-auto-flow: row;',
|
|
193
|
-
'grid-flow-col': 'grid-auto-flow: column;',
|
|
194
|
-
'grid-flow-dense': 'grid-auto-flow: dense;',
|
|
195
|
-
'grid-flow-row-dense': 'grid-auto-flow: row dense;',
|
|
196
|
-
'grid-flow-col-dense': 'grid-auto-flow: column dense;',
|
|
197
|
-
|
|
198
|
-
// Shorthand Alignment (backwards compat - simple keywords)
|
|
199
|
-
'center': 'justify-content: center; align-items: center;',
|
|
200
|
-
'start': 'justify-content: flex-start; align-items: flex-start;',
|
|
201
|
-
'end': 'justify-content: flex-end; align-items: flex-end;',
|
|
202
|
-
'between': 'justify-content: space-between;',
|
|
203
|
-
'around': 'justify-content: space-around;',
|
|
204
|
-
'evenly': 'justify-content: space-evenly;',
|
|
205
|
-
|
|
206
|
-
// Position
|
|
207
|
-
'absolute': 'position: absolute;',
|
|
208
|
-
'relative': 'position: relative;',
|
|
209
|
-
'fixed': 'position: fixed;',
|
|
210
|
-
'sticky': 'position: sticky;',
|
|
211
|
-
'static': 'position: static;',
|
|
212
|
-
|
|
213
|
-
// Visibility
|
|
214
|
-
'visible': 'visibility: visible;',
|
|
215
|
-
'invisible': 'visibility: hidden;',
|
|
216
|
-
|
|
217
|
-
// Isolation
|
|
218
|
-
'isolate': 'isolation: isolate;',
|
|
219
|
-
'isolate-auto': 'isolation: auto;',
|
|
220
|
-
|
|
221
|
-
// Box Sizing
|
|
222
|
-
'box-border': 'box-sizing: border-box;',
|
|
223
|
-
'box-content': 'box-sizing: content-box;',
|
|
224
|
-
|
|
225
|
-
// Float
|
|
226
|
-
'float-left': 'float: left;',
|
|
227
|
-
'float-right': 'float: right;',
|
|
228
|
-
'float-none': 'float: none;',
|
|
229
|
-
|
|
230
|
-
// Clear
|
|
231
|
-
'clear-left': 'clear: left;',
|
|
232
|
-
'clear-right': 'clear: right;',
|
|
233
|
-
'clear-both': 'clear: both;',
|
|
234
|
-
'clear-none': 'clear: none;',
|
|
235
|
-
|
|
236
|
-
// Table - Border Collapse
|
|
237
|
-
'border-collapse': 'border-collapse: collapse;',
|
|
238
|
-
'border-separate': 'border-collapse: separate;',
|
|
239
|
-
|
|
240
|
-
// Table - Table Layout
|
|
241
|
-
'table-auto': 'table-layout: auto;',
|
|
242
|
-
'table-fixed': 'table-layout: fixed;',
|
|
243
|
-
|
|
244
|
-
// Table - Caption Side
|
|
245
|
-
'caption-top': 'caption-side: top;',
|
|
246
|
-
'caption-bottom': 'caption-side: bottom;',
|
|
247
|
-
|
|
248
|
-
// Container
|
|
249
|
-
'container': 'width: 100%; margin-left: auto; margin-right: auto;'
|
|
250
|
-
};
|
|
251
|
-
|
|
252
158
|
// Check for simple layout keywords first (property === value means it's a keyword like 'flex', 'grid', etc.)
|
|
159
|
+
// layoutMap is now imported from definitions
|
|
253
160
|
if (property === value && layoutMap[property]) {
|
|
254
161
|
return layoutMap[property];
|
|
255
162
|
}
|
|
@@ -587,7 +494,25 @@ function generateSpaceRule(token, config) {
|
|
|
587
494
|
}
|
|
588
495
|
|
|
589
496
|
// Determine the CSS value
|
|
590
|
-
|
|
497
|
+
let cssValue;
|
|
498
|
+
if (isArbitrary) {
|
|
499
|
+
cssValue = value;
|
|
500
|
+
} else {
|
|
501
|
+
// Check for negative value
|
|
502
|
+
const isNegative = value.startsWith('-');
|
|
503
|
+
const cleanValue = isNegative ? value.substring(1) : value;
|
|
504
|
+
|
|
505
|
+
let baseValue;
|
|
506
|
+
if (cleanValue.startsWith('tw-')) {
|
|
507
|
+
const twValue = cleanValue.slice(3); // Remove 'tw-' prefix
|
|
508
|
+
baseValue = `var(--tw-${twValue.replace(/\./g, '\\\\.')})`;
|
|
509
|
+
} else {
|
|
510
|
+
baseValue = `var(--s-${cleanValue})`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Apply negative calculation if needed
|
|
514
|
+
cssValue = isNegative ? `calc(${baseValue} * -1)` : baseValue;
|
|
515
|
+
}
|
|
591
516
|
|
|
592
517
|
// Handle special values
|
|
593
518
|
if (value === 'auto') {
|
|
@@ -649,100 +574,7 @@ function generateVisualRule(token, config) {
|
|
|
649
574
|
const { property, value, isArbitrary } = token;
|
|
650
575
|
|
|
651
576
|
// Static typography keywords
|
|
652
|
-
|
|
653
|
-
// Font Style
|
|
654
|
-
'italic': 'font-style: italic;',
|
|
655
|
-
'not-italic': 'font-style: normal;',
|
|
656
|
-
|
|
657
|
-
// Font Stretch
|
|
658
|
-
'font-stretch-condensed': 'font-stretch: condensed;',
|
|
659
|
-
'font-stretch-expanded': 'font-stretch: expanded;',
|
|
660
|
-
'font-stretch-normal': 'font-stretch: normal;',
|
|
661
|
-
|
|
662
|
-
// Font Smoothing
|
|
663
|
-
'antialiased': '-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;',
|
|
664
|
-
'subpixel-antialiased': '-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;',
|
|
665
|
-
|
|
666
|
-
// Font Variant Numeric
|
|
667
|
-
'normal-nums': 'font-variant-numeric: normal;',
|
|
668
|
-
'ordinal': 'font-variant-numeric: ordinal;',
|
|
669
|
-
'slashed-zero': 'font-variant-numeric: slashed-zero;',
|
|
670
|
-
'lining-nums': 'font-variant-numeric: lining-nums;',
|
|
671
|
-
'oldstyle-nums': 'font-variant-numeric: oldstyle-nums;',
|
|
672
|
-
'proportional-nums': 'font-variant-numeric: proportional-nums;',
|
|
673
|
-
'tabular-nums': 'font-variant-numeric: tabular-nums;',
|
|
674
|
-
|
|
675
|
-
// Text Transform
|
|
676
|
-
'uppercase': 'text-transform: uppercase;',
|
|
677
|
-
'lowercase': 'text-transform: lowercase;',
|
|
678
|
-
'capitalize': 'text-transform: capitalize;',
|
|
679
|
-
'normal-case': 'text-transform: none;',
|
|
680
|
-
|
|
681
|
-
// Text Decoration Line
|
|
682
|
-
'underline': 'text-decoration-line: underline;',
|
|
683
|
-
'overline': 'text-decoration-line: overline;',
|
|
684
|
-
'line-through': 'text-decoration-line: line-through;',
|
|
685
|
-
'no-underline': 'text-decoration-line: none;',
|
|
686
|
-
|
|
687
|
-
// Text Decoration Style
|
|
688
|
-
'decoration-solid': 'text-decoration-style: solid;',
|
|
689
|
-
'decoration-double': 'text-decoration-style: double;',
|
|
690
|
-
'decoration-dotted': 'text-decoration-style: dotted;',
|
|
691
|
-
'decoration-dashed': 'text-decoration-style: dashed;',
|
|
692
|
-
'decoration-wavy': 'text-decoration-style: wavy;',
|
|
693
|
-
|
|
694
|
-
// Text Overflow
|
|
695
|
-
'truncate': 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
|
696
|
-
'text-ellipsis': 'text-overflow: ellipsis;',
|
|
697
|
-
'text-clip': 'text-overflow: clip;',
|
|
698
|
-
|
|
699
|
-
// Text Wrap
|
|
700
|
-
'text-wrap': 'text-wrap: wrap;',
|
|
701
|
-
'text-nowrap': 'text-wrap: nowrap;',
|
|
702
|
-
'text-balance': 'text-wrap: balance;',
|
|
703
|
-
'text-pretty': 'text-wrap: pretty;',
|
|
704
|
-
|
|
705
|
-
// Whitespace
|
|
706
|
-
'whitespace-normal': 'white-space: normal;',
|
|
707
|
-
'whitespace-nowrap': 'white-space: nowrap;',
|
|
708
|
-
'whitespace-pre': 'white-space: pre;',
|
|
709
|
-
'whitespace-pre-line': 'white-space: pre-line;',
|
|
710
|
-
'whitespace-pre-wrap': 'white-space: pre-wrap;',
|
|
711
|
-
'whitespace-break-spaces': 'white-space: break-spaces;',
|
|
712
|
-
|
|
713
|
-
// Word Break
|
|
714
|
-
'break-normal': 'overflow-wrap: normal; word-break: normal;',
|
|
715
|
-
'break-words': 'overflow-wrap: break-word;',
|
|
716
|
-
'break-all': 'word-break: break-all;',
|
|
717
|
-
'break-keep': 'word-break: keep-all;',
|
|
718
|
-
|
|
719
|
-
// Hyphens
|
|
720
|
-
'hyphens-none': 'hyphens: none;',
|
|
721
|
-
'hyphens-manual': 'hyphens: manual;',
|
|
722
|
-
'hyphens-auto': 'hyphens: auto;',
|
|
723
|
-
|
|
724
|
-
// Vertical Align
|
|
725
|
-
'align-baseline': 'vertical-align: baseline;',
|
|
726
|
-
'align-top': 'vertical-align: top;',
|
|
727
|
-
'align-middle': 'vertical-align: middle;',
|
|
728
|
-
'align-bottom': 'vertical-align: bottom;',
|
|
729
|
-
'align-text-top': 'vertical-align: text-top;',
|
|
730
|
-
'align-text-bottom': 'vertical-align: text-bottom;',
|
|
731
|
-
'align-sub': 'vertical-align: sub;',
|
|
732
|
-
'align-super': 'vertical-align: super;',
|
|
733
|
-
|
|
734
|
-
// List Style Type
|
|
735
|
-
'list-none': 'list-style-type: none;',
|
|
736
|
-
'list-disc': 'list-style-type: disc;',
|
|
737
|
-
'list-decimal': 'list-style-type: decimal;',
|
|
738
|
-
'list-square': 'list-style-type: square;',
|
|
739
|
-
|
|
740
|
-
// List Style Position
|
|
741
|
-
'list-inside': 'list-style-position: inside;',
|
|
742
|
-
'list-outside': 'list-style-position: outside;'
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
// Check static keywords first
|
|
577
|
+
// Check static keywords first (imported from definitions)
|
|
746
578
|
if (typographyKeywords[property]) {
|
|
747
579
|
return typographyKeywords[property];
|
|
748
580
|
}
|
|
@@ -4,20 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { BREAKPOINTS, STATES, LAYOUT_KEYWORDS } from './constants.js';
|
|
7
|
+
import { sanitizeValue } from '../utils/common.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Sanitize token value to prevent CSS injection
|
|
10
11
|
* @param {string} value - Value to sanitize
|
|
11
12
|
* @returns {string} - Sanitized value
|
|
12
13
|
*/
|
|
13
|
-
export
|
|
14
|
-
if (typeof value !== 'string') {
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
// Only remove semicolons which can terminate CSS rules
|
|
18
|
-
// Allow braces for legitimate use cases like content:["{icon}"]
|
|
19
|
-
return value.replace(/;/g, '_');
|
|
20
|
-
}
|
|
14
|
+
export { sanitizeValue };
|
|
21
15
|
|
|
22
16
|
/**
|
|
23
17
|
* Validate token structure
|
|
@@ -46,56 +40,7 @@ export function isValidToken(token) {
|
|
|
46
40
|
return true;
|
|
47
41
|
}
|
|
48
42
|
|
|
49
|
-
/**
|
|
50
|
-
* Lightweight token parser (no validation, used for internal processing)
|
|
51
|
-
* @param {string} raw - Raw token string
|
|
52
|
-
* @returns {Object} - Parsed token object
|
|
53
|
-
*/
|
|
54
|
-
export function parseToken(raw) {
|
|
55
|
-
const token = {
|
|
56
|
-
raw,
|
|
57
|
-
breakpoint: null,
|
|
58
|
-
state: null,
|
|
59
|
-
property: null,
|
|
60
|
-
value: null,
|
|
61
|
-
isArbitrary: false
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const parts = raw.split(':');
|
|
65
|
-
let idx = 0;
|
|
66
43
|
|
|
67
|
-
// Check for breakpoint
|
|
68
|
-
if (BREAKPOINTS.includes(parts[0])) {
|
|
69
|
-
token.breakpoint = parts[0];
|
|
70
|
-
idx++;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Check for state
|
|
74
|
-
if (STATES.includes(parts[idx])) {
|
|
75
|
-
token.state = parts[idx];
|
|
76
|
-
idx++;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Property
|
|
80
|
-
if (idx < parts.length) {
|
|
81
|
-
token.property = parts[idx];
|
|
82
|
-
idx++;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Value
|
|
86
|
-
if (idx < parts.length) {
|
|
87
|
-
let value = parts.slice(idx).join(':');
|
|
88
|
-
const arbitraryMatch = value.match(/^\[(.+)\]$/);
|
|
89
|
-
if (arbitraryMatch) {
|
|
90
|
-
token.value = arbitraryMatch[1].replace(/_/g, ' ');
|
|
91
|
-
token.isArbitrary = true;
|
|
92
|
-
} else {
|
|
93
|
-
token.value = value;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return token;
|
|
98
|
-
}
|
|
99
44
|
|
|
100
45
|
/**
|
|
101
46
|
* Tokenize a single attribute value string
|
|
@@ -230,4 +175,4 @@ export function tokenizeAll(parsed) {
|
|
|
230
175
|
return tokens;
|
|
231
176
|
}
|
|
232
177
|
|
|
233
|
-
export default { tokenize, tokenizeAll,
|
|
178
|
+
export default { tokenize, tokenizeAll, sanitizeValue, isValidToken };
|
|
@@ -181,6 +181,10 @@ export function buildLayoutMap() {
|
|
|
181
181
|
for (const def of Object.values(layoutDefinitions)) {
|
|
182
182
|
if (def.dynamic) continue; // Skip dynamic properties that need special handling
|
|
183
183
|
|
|
184
|
+
// Only include definitions that act as global keywords (no prefix in syntax)
|
|
185
|
+
// format: layout="[value]"
|
|
186
|
+
if (!def.syntax || !def.syntax.includes('layout="[')) continue;
|
|
187
|
+
|
|
184
188
|
for (const v of def.values) {
|
|
185
189
|
// Skip range values like '1-12' that need special handling
|
|
186
190
|
if (v.value.match(/^\d+-\d+$/)) continue;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangStart CSS - Common Utilities
|
|
3
|
+
* Shared helper functions for compiler and runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize token value to prevent CSS injection
|
|
8
|
+
* Removes potentially dangerous characters/sequences
|
|
9
|
+
* @param {string} value - Value to sanitize
|
|
10
|
+
* @returns {string} - Sanitized value
|
|
11
|
+
*/
|
|
12
|
+
export function sanitizeValue(value) {
|
|
13
|
+
if (typeof value !== 'string') {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Remove potentially dangerous characters that could break CSS syntax
|
|
18
|
+
// Note: We used to filter {} but some tests expect them (e.g. content icons), so we only filter ; for now
|
|
19
|
+
const dangerousChars = /[;]/g;
|
|
20
|
+
if (dangerousChars.test(value)) {
|
|
21
|
+
return value.replace(dangerousChars, '_');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default { sanitizeValue };
|
|
@@ -71,6 +71,18 @@ describe('convertClass', () => {
|
|
|
71
71
|
assert.deepStrictEqual(convertClass('h-screen'), { category: 'space', value: 'h:[100vh]' });
|
|
72
72
|
assert.deepStrictEqual(convertClass('max-w-4'), { category: 'space', value: 'max-w:small' });
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
it('should convert negative margin classes', () => {
|
|
76
|
+
// Standard exact=false
|
|
77
|
+
assert.deepStrictEqual(convertClass('-m-4'), { category: 'space', value: 'm:-small' });
|
|
78
|
+
assert.deepStrictEqual(convertClass('-mt-8'), { category: 'space', value: 'm-t:-big' });
|
|
79
|
+
|
|
80
|
+
// Exact=true
|
|
81
|
+
assert.deepStrictEqual(convertClass('-m-4', { exact: true }), { category: 'space', value: 'm:-tw-4' });
|
|
82
|
+
|
|
83
|
+
// Arbitrary
|
|
84
|
+
assert.deepStrictEqual(convertClass('-m-[10px]'), { category: 'space', value: 'm:[-10px]' });
|
|
85
|
+
});
|
|
74
86
|
});
|
|
75
87
|
|
|
76
88
|
describe('Visual classes', () => {
|