@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.
@@ -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
- * Sanitize arbitrary value to prevent CSS injection
10
- * @param {string} value - Value to sanitize
11
- * @returns {string} - Sanitized value
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
- if (typeof value !== 'string') {
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
- const cssValue = isArbitrary ? value : `var(--s-${value})`;
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
- const typographyKeywords = {
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
  }
@@ -7,7 +7,6 @@
7
7
  export {
8
8
  tokenize,
9
9
  tokenizeAll,
10
- parseToken,
11
10
  sanitizeValue,
12
11
  isValidToken
13
12
  } from '../core/tokenizer-core.js';
@@ -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 function sanitizeValue(value) {
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, parseToken, sanitizeValue, isValidToken };
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', () => {