Skip to content

Commit ada1f61

Browse files
committed
chore: wip
1 parent 94188fd commit ada1f61

5 files changed

Lines changed: 208 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ node_modules
1111
temp
1212
docs/.vitepress/cache
1313
packages/headwind/bin/headwind
14+
packages/crosswind/bin/crosswind

packages/crosswind/bin/crosswind

-58 MB
Binary file not shown.

packages/crosswind/bin/headwind

-57.3 MB
Binary file not shown.

packages/crosswind/src/generator.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,20 +1434,52 @@ export class CSSGenerator {
14341434

14351435
// Width: w-{size}
14361436
if (utility === 'w' && value) {
1437-
const sizeVal = SIZE_VALUES[value]
1437+
// Try SIZE_VALUES first, then spacing config, then use raw value (for arbitrary values like calc())
1438+
const sizeVal = SIZE_VALUES[value] || this.config.theme.spacing[value]
14381439
if (sizeVal) {
14391440
this.addRule(parsed, { width: sizeVal })
14401441
return
14411442
}
1443+
// Handle arbitrary values (calc, min, max, clamp, etc.)
1444+
if (parsed.arbitrary) {
1445+
// Check if it's a fraction in arbitrary syntax: w-[1/2] or w-[100/100]
1446+
const fractionMatch = value.match(/^(\d+)\/(\d+)$/)
1447+
if (fractionMatch) {
1448+
const num = Number(fractionMatch[1])
1449+
const denom = Number(fractionMatch[2])
1450+
if (!Number.isNaN(num) && !Number.isNaN(denom) && denom !== 0) {
1451+
this.addRule(parsed, { width: `${(num / denom) * 100}%` })
1452+
return
1453+
}
1454+
}
1455+
this.addRule(parsed, { width: value })
1456+
return
1457+
}
14421458
}
14431459

14441460
// Height: h-{size}
14451461
if (utility === 'h' && value) {
1446-
const sizeVal = SIZE_VALUES[value]
1462+
// Try SIZE_VALUES first, then spacing config, then use raw value (for arbitrary values like calc())
1463+
const sizeVal = SIZE_VALUES[value] || this.config.theme.spacing[value]
14471464
if (sizeVal) {
14481465
this.addRule(parsed, { height: value === 'screen' ? '100vh' : sizeVal })
14491466
return
14501467
}
1468+
// Handle arbitrary values (calc, min, max, clamp, etc.)
1469+
if (parsed.arbitrary) {
1470+
// Check if it's a fraction in arbitrary syntax: h-[1/2] or h-[100/100]
1471+
const fractionMatch = value.match(/^(\d+)\/(\d+)$/)
1472+
if (fractionMatch) {
1473+
const num = Number(fractionMatch[1])
1474+
const denom = Number(fractionMatch[2])
1475+
if (!Number.isNaN(num) && !Number.isNaN(denom) && denom !== 0) {
1476+
this.addRule(parsed, { height: `${(num / denom) * 100}%` })
1477+
return
1478+
}
1479+
}
1480+
this.addRule(parsed, { height: value })
1481+
return
1482+
}
14511483
}
14521484

14531485
// Padding: p-{size}
@@ -2028,16 +2060,19 @@ export class CSSGenerator {
20282060
let needsEscape = false
20292061
for (let i = 0; i < className.length; i++) {
20302062
const c = className.charCodeAt(i)
2031-
// Check for : (58), . (46), / (47), @ (64), space (32), [ (91), ] (93)
2032-
if (c === 58 || c === 46 || c === 47 || c === 64 || c === 32 || c === 91 || c === 93) {
2063+
// Check for special CSS selector characters:
2064+
// : (58), . (46), / (47), @ (64), space (32), [ (91), ] (93)
2065+
// ( (40), ) (41), % (37), # (35), , (44), > (62), + (43), ~ (126)
2066+
if (c === 58 || c === 46 || c === 47 || c === 64 || c === 32 || c === 91 || c === 93 ||
2067+
c === 40 || c === 41 || c === 37 || c === 35 || c === 44 || c === 62 || c === 43 || c === 126) {
20332068
needsEscape = true
20342069
break
20352070
}
20362071
}
20372072
if (!needsEscape) {
20382073
return className
20392074
}
2040-
return className.replace(/[:./@ \[\]]/g, '\\$&')
2075+
return className.replace(/[:./@ \[\]()%#,>+~]/g, '\\$&')
20412076
}
20422077

20432078
/**

packages/crosswind/test/arbitrary.test.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,172 @@ describe('Arbitrary Values and Properties', () => {
392392
expect(gen.toCSS(false)).toContain('transform: rotate(0.25turn);')
393393
})
394394
})
395+
396+
describe('CSS Selector Escaping', () => {
397+
it('should properly escape calc() with viewport units', () => {
398+
const gen = new CSSGenerator(defaultConfig)
399+
gen.generate('h-[calc(100vh-4rem)]')
400+
const css = gen.toCSS(false)
401+
expect(css).toContain('height: calc(100vh-4rem);')
402+
// Verify the selector is properly escaped with backslashes
403+
expect(css).toContain('.h-\\[calc\\(100vh-4rem\\)\\]')
404+
})
405+
406+
it('should properly escape calc() with percentage', () => {
407+
const gen = new CSSGenerator(defaultConfig)
408+
gen.generate('w-[calc(100%-2rem)]')
409+
const css = gen.toCSS(false)
410+
expect(css).toContain('width: calc(100%-2rem);')
411+
expect(css).toContain('.w-\\[calc\\(100\\%-2rem\\)\\]')
412+
})
413+
414+
it('should properly escape min() function', () => {
415+
const gen = new CSSGenerator(defaultConfig)
416+
gen.generate('w-[min(100%,500px)]')
417+
const css = gen.toCSS(false)
418+
expect(css).toContain('width: min(100%,500px);')
419+
expect(css).toContain('.w-\\[min\\(100\\%\\,500px\\)\\]')
420+
})
421+
422+
it('should properly escape max() function', () => {
423+
const gen = new CSSGenerator(defaultConfig)
424+
gen.generate('h-[max(50vh,300px)]')
425+
const css = gen.toCSS(false)
426+
expect(css).toContain('height: max(50vh,300px);')
427+
expect(css).toContain('.h-\\[max\\(50vh\\,300px\\)\\]')
428+
})
429+
430+
it('should properly escape clamp() function', () => {
431+
const gen = new CSSGenerator(defaultConfig)
432+
gen.generate('text-[clamp(1rem,2.5vw,3rem)]')
433+
const css = gen.toCSS(false)
434+
expect(css).toContain('font-size: clamp(1rem,2.5vw,3rem);')
435+
expect(css).toContain('.text-\\[clamp\\(1rem\\,2\\.5vw\\,3rem\\)\\]')
436+
})
437+
438+
it('should properly escape nested functions', () => {
439+
const gen = new CSSGenerator(defaultConfig)
440+
gen.generate('[grid-template-columns:repeat(auto-fit,minmax(200px,1fr))]')
441+
const css = gen.toCSS(false)
442+
expect(css).toContain('grid-template-columns: repeat(auto-fit,minmax(200px,1fr));')
443+
// The selector should have all parentheses and commas escaped
444+
expect(css).toMatch(/\.\\\[grid-template-columns\\:repeat\\\(auto-fit\\,minmax\\\(200px\\,1fr\\\)\\\)\\\]/)
445+
})
446+
447+
it('should properly escape rgba colors', () => {
448+
const gen = new CSSGenerator(defaultConfig)
449+
gen.generate('bg-[rgba(0,0,0,0.5)]')
450+
const css = gen.toCSS(false)
451+
expect(css).toContain('background-color: rgba(0,0,0,0.5);')
452+
expect(css).toContain('.bg-\\[rgba\\(0\\,0\\,0\\,0\\.5\\)\\]')
453+
})
454+
455+
it('should properly escape hsla colors', () => {
456+
const gen = new CSSGenerator(defaultConfig)
457+
gen.generate('bg-[hsla(120,50%,50%,0.8)]')
458+
const css = gen.toCSS(false)
459+
expect(css).toContain('background-color: hsla(120,50%,50%,0.8);')
460+
expect(css).toContain('.bg-\\[hsla\\(120\\,50\\%\\,50\\%\\,0\\.8\\)\\]')
461+
})
462+
463+
it('should properly escape hash in hex colors', () => {
464+
const gen = new CSSGenerator(defaultConfig)
465+
gen.generate('bg-[#ff6600]')
466+
const css = gen.toCSS(false)
467+
expect(css).toContain('background-color: #ff6600;')
468+
expect(css).toContain('.bg-\\[\\#ff6600\\]')
469+
})
470+
471+
it('should handle complex calc with multiple operations', () => {
472+
const gen = new CSSGenerator(defaultConfig)
473+
gen.generate('w-[calc((100vw-64px)/2)]')
474+
const css = gen.toCSS(false)
475+
expect(css).toContain('width: calc((100vw-64px)/2);')
476+
})
477+
478+
it('should handle sidebar-style height calculation', () => {
479+
// This is the specific use case that triggered the fix
480+
const gen = new CSSGenerator(defaultConfig)
481+
gen.generate('h-[calc(100vh-4rem)]')
482+
const css = gen.toCSS(false)
483+
// Must contain valid CSS
484+
expect(css).toContain('height: calc(100vh-4rem);')
485+
// Selector must be properly escaped so browser can match it
486+
expect(css).toMatch(/\.h-\\\[calc\\\(100vh-4rem\\\)\\\]/)
487+
})
488+
489+
it('should handle min-height with calc', () => {
490+
const gen = new CSSGenerator(defaultConfig)
491+
gen.generate('min-h-[calc(100vh-64px)]')
492+
const css = gen.toCSS(false)
493+
expect(css).toContain('min-height: calc(100vh-64px);')
494+
})
495+
496+
it('should handle max-width with calc', () => {
497+
const gen = new CSSGenerator(defaultConfig)
498+
gen.generate('max-w-[calc(100%-2rem)]')
499+
const css = gen.toCSS(false)
500+
expect(css).toContain('max-width: calc(100%-2rem);')
501+
})
502+
503+
it('should escape special characters with variants', () => {
504+
const gen = new CSSGenerator(defaultConfig)
505+
gen.generate('lg:h-[calc(100vh-4rem)]')
506+
const css = gen.toCSS(false)
507+
expect(css).toContain('@media (min-width: 1024px)')
508+
expect(css).toContain('height: calc(100vh-4rem);')
509+
})
510+
511+
it('should escape special characters with hover variant', () => {
512+
const gen = new CSSGenerator(defaultConfig)
513+
gen.generate('hover:bg-[rgba(0,0,0,0.1)]')
514+
const css = gen.toCSS(false)
515+
expect(css).toContain(':hover')
516+
expect(css).toContain('background-color: rgba(0,0,0,0.1);')
517+
})
518+
519+
it('should handle CSS var with fallback containing comma', () => {
520+
const gen = new CSSGenerator(defaultConfig)
521+
gen.generate('[color:var(--primary,#000)]')
522+
const css = gen.toCSS(false)
523+
expect(css).toContain('color: var(--primary,#000);')
524+
})
525+
526+
it('should handle transform with translate', () => {
527+
const gen = new CSSGenerator(defaultConfig)
528+
gen.generate('[transform:translateX(calc(100%+1rem))]')
529+
const css = gen.toCSS(false)
530+
expect(css).toContain('transform: translateX(calc(100%+1rem));')
531+
})
532+
533+
it('should handle filter with multiple functions', () => {
534+
const gen = new CSSGenerator(defaultConfig)
535+
gen.generate('[filter:blur(4px)brightness(0.9)]')
536+
const css = gen.toCSS(false)
537+
expect(css).toContain('filter: blur(4px)brightness(0.9);')
538+
})
539+
540+
it('should properly escape plus sign in selectors', () => {
541+
const gen = new CSSGenerator(defaultConfig)
542+
gen.generate('[margin:calc(1rem+2px)]')
543+
const css = gen.toCSS(false)
544+
expect(css).toContain('margin: calc(1rem+2px);')
545+
expect(css).toContain('.\\[margin\\:calc\\(1rem\\+2px\\)\\]')
546+
})
547+
548+
it('should properly escape tilde in selectors', () => {
549+
const gen = new CSSGenerator(defaultConfig)
550+
gen.generate('[content:~test]')
551+
const css = gen.toCSS(false)
552+
expect(css).toContain('.\\[content\\:\\~test\\]')
553+
})
554+
555+
it('should properly escape greater than sign in selectors', () => {
556+
const gen = new CSSGenerator(defaultConfig)
557+
gen.generate('[content:a>b]')
558+
const css = gen.toCSS(false)
559+
expect(css).toContain('.\\[content\\:a\\>b\\]')
560+
})
561+
})
395562
})
396563
})

0 commit comments

Comments
 (0)