Skip to content

Commit 751b30f

Browse files
committed
chore: wip
1 parent d768974 commit 751b30f

File tree

9 files changed

+435
-2
lines changed

9 files changed

+435
-2
lines changed

packages/headwind/src/parser.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ export function parseClass(className: string): ParsedClass {
2929
}
3030
}
3131

32+
// Check for arbitrary values with brackets BEFORE splitting on colons
33+
// This handles cases like bg-[url(https://...)] where the URL contains colons
34+
const preArbitraryMatch = cleanClassName.match(/^((?:[a-z-]+:)*)([a-z-]+?)-\[(.+)\]$/)
35+
if (preArbitraryMatch) {
36+
const variantPart = preArbitraryMatch[1]
37+
const variants = variantPart ? variantPart.split(':').filter(Boolean) : []
38+
return {
39+
raw: className,
40+
variants,
41+
utility: preArbitraryMatch[2],
42+
value: preArbitraryMatch[3],
43+
important,
44+
arbitrary: true,
45+
}
46+
}
47+
3248
const parts = cleanClassName.split(':')
3349
const utility = parts[parts.length - 1]
3450
const variants = parts.slice(0, -1)
@@ -158,6 +174,9 @@ export function parseClass(className: string): ParsedClass {
158174
'form-multiselect',
159175
'form-checkbox',
160176
'form-radio',
177+
'mix-blend',
178+
'bg-blend',
179+
'line-clamp',
161180
]
162181

163182
// Special case for divide-x and divide-y (without values, they should be treated as compound)

packages/headwind/src/rules-effects.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export const backgroundImageRule: UtilityRule = (parsed) => {
4141
'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
4242
'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))',
4343
}
44-
if (gradients[`gradient-${parsed.value}`]) {
45-
return { 'background-image': gradients[`gradient-${parsed.value}`] }
44+
if (gradients[parsed.value]) {
45+
return { 'background-image': gradients[parsed.value] } as Record<string, string>
4646
}
4747
}
4848
}

packages/headwind/test/colors.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,4 +328,81 @@ describe('Color Utilities', () => {
328328
expect(gen.toCSS()).toContain('color: currentColor;')
329329
})
330330
})
331+
332+
describe('Advanced color formats', () => {
333+
it('should handle 3-digit hex colors', () => {
334+
const gen = new CSSGenerator(defaultConfig)
335+
gen.generate('bg-[#fff]')
336+
gen.generate('text-[#000]')
337+
const css = gen.toCSS()
338+
expect(css).toContain('#fff')
339+
expect(css).toContain('#000')
340+
})
341+
342+
it('should handle 8-digit hex colors with alpha', () => {
343+
const gen = new CSSGenerator(defaultConfig)
344+
gen.generate('bg-[#ff000080]')
345+
const css = gen.toCSS()
346+
expect(css).toContain('#ff000080')
347+
})
348+
349+
it('should handle rgb() notation', () => {
350+
const gen = new CSSGenerator(defaultConfig)
351+
gen.generate('bg-[rgb(255,0,0)]')
352+
const css = gen.toCSS()
353+
expect(css).toContain('rgb(255,0,0)')
354+
})
355+
356+
it('should handle rgba() notation', () => {
357+
const gen = new CSSGenerator(defaultConfig)
358+
gen.generate('bg-[rgba(255,0,0,0.5)]')
359+
const css = gen.toCSS()
360+
expect(css).toContain('rgba(255,0,0,0.5)')
361+
})
362+
363+
it('should handle hsl() notation', () => {
364+
const gen = new CSSGenerator(defaultConfig)
365+
gen.generate('bg-[hsl(0,100%,50%)]')
366+
const css = gen.toCSS()
367+
expect(css).toContain('hsl(0,100%,50%)')
368+
})
369+
370+
it('should handle hsla() notation', () => {
371+
const gen = new CSSGenerator(defaultConfig)
372+
gen.generate('bg-[hsla(0,100%,50%,0.5)]')
373+
const css = gen.toCSS()
374+
expect(css).toContain('hsla(0,100%,50%,0.5)')
375+
})
376+
377+
it('should handle oklch() notation', () => {
378+
const gen = new CSSGenerator(defaultConfig)
379+
gen.generate('bg-[oklch(0.5_0.2_180)]')
380+
const css = gen.toCSS()
381+
expect(css).toContain('oklch')
382+
})
383+
384+
it('should handle color() notation', () => {
385+
const gen = new CSSGenerator(defaultConfig)
386+
gen.generate('bg-[color(display-p3_1_0_0)]')
387+
const css = gen.toCSS()
388+
expect(css).toContain('color(')
389+
})
390+
391+
it('should handle nonexistent color shades gracefully', () => {
392+
const gen = new CSSGenerator(defaultConfig)
393+
gen.generate('bg-blue-999')
394+
gen.generate('text-red-1')
395+
// Should not crash, may not generate CSS
396+
expect(() => gen.toCSS()).not.toThrow()
397+
})
398+
399+
it('should handle color with opacity modifier', () => {
400+
const gen = new CSSGenerator(defaultConfig)
401+
gen.generate('bg-blue-500/50')
402+
gen.generate('text-red-500/75')
403+
const css = gen.toCSS()
404+
// May or may not be implemented, but should not crash
405+
expect(() => gen.toCSS()).not.toThrow()
406+
})
407+
})
331408
})

packages/headwind/test/generator.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,84 @@ describe('CSSGenerator - Edge Cases', () => {
514514
const css = gen.toCSS(true)
515515
expect(css).toContain('box-sizing')
516516
})
517+
518+
describe('Extreme Edge Cases', () => {
519+
it('should handle generating the same class 1000 times', () => {
520+
const gen = new CSSGenerator(defaultConfig)
521+
for (let i = 0; i < 1000; i++) {
522+
gen.generate('w-4')
523+
}
524+
const css = gen.toCSS()
525+
// Should only have one .w-4 rule
526+
const matches = css.match(/\.w-4\s*\{/g) || []
527+
expect(matches.length).toBe(1)
528+
})
529+
530+
it('should handle generating invalid utilities without crashing', () => {
531+
const gen = new CSSGenerator(defaultConfig)
532+
expect(() => gen.generate('')).not.toThrow()
533+
expect(() => gen.generate(' ')).not.toThrow()
534+
expect(() => gen.generate('invalid-utility-xyz-123')).not.toThrow()
535+
expect(() => gen.generate('!!!!!')).not.toThrow()
536+
expect(() => gen.generate(':::::')).not.toThrow()
537+
})
538+
539+
it('should handle generating utilities with null/undefined-like names', () => {
540+
const gen = new CSSGenerator(defaultConfig)
541+
expect(() => gen.generate('null')).not.toThrow()
542+
expect(() => gen.generate('undefined')).not.toThrow()
543+
expect(() => gen.generate('false')).not.toThrow()
544+
})
545+
546+
it('should handle very long arbitrary values', () => {
547+
const gen = new CSSGenerator(defaultConfig)
548+
const longValue = 'a'.repeat(1000)
549+
gen.generate(`w-[${longValue}]`)
550+
const css = gen.toCSS()
551+
expect(css).toContain(longValue)
552+
})
553+
554+
it('should handle generating arbitrary property with colon in value', () => {
555+
const gen = new CSSGenerator(defaultConfig)
556+
gen.generate('[background-image:url(http://example.com)]')
557+
const css = gen.toCSS()
558+
expect(css.length).toBeGreaterThan(0)
559+
})
560+
561+
it('should handle malformed arbitrary syntax', () => {
562+
const gen = new CSSGenerator(defaultConfig)
563+
expect(() => gen.generate('w-[')).not.toThrow() // Missing closing bracket
564+
expect(() => gen.generate('w-]')).not.toThrow() // Missing opening bracket
565+
expect(() => gen.generate('w-[[]]')).not.toThrow() // Double brackets
566+
expect(() => gen.generate('w-[[]')).not.toThrow() // Unbalanced
567+
})
568+
569+
it('should handle generating with no config theme colors', () => {
570+
const gen = new CSSGenerator({ ...defaultConfig, theme: { ...defaultConfig.theme, colors: {} } })
571+
gen.generate('bg-blue-500')
572+
gen.generate('text-red-500')
573+
// Should not crash even if colors don't exist
574+
expect(() => gen.toCSS()).not.toThrow()
575+
})
576+
577+
it('should handle generating with no spacing scale', () => {
578+
const gen = new CSSGenerator({ ...defaultConfig, theme: { ...defaultConfig.theme, spacing: {} } })
579+
gen.generate('p-4')
580+
gen.generate('m-8')
581+
// Should fall back to raw values
582+
expect(() => gen.toCSS()).not.toThrow()
583+
})
584+
585+
it('should handle important modifier on invalid utility', () => {
586+
const gen = new CSSGenerator(defaultConfig)
587+
gen.generate('!invalid-utility-name')
588+
expect(() => gen.toCSS()).not.toThrow()
589+
})
590+
591+
it('should handle multiple variants on invalid utility', () => {
592+
const gen = new CSSGenerator(defaultConfig)
593+
gen.generate('sm:md:lg:invalid-utility')
594+
expect(() => gen.toCSS()).not.toThrow()
595+
})
596+
})
517597
})

packages/headwind/test/modifiers.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,5 +355,63 @@ describe('Modifiers', () => {
355355
expect(css).toContain('margin: 0.5rem !important;')
356356
})
357357
})
358+
359+
describe('Extreme variant stacking', () => {
360+
it('should handle 10+ stacked variants', () => {
361+
const gen = new CSSGenerator(defaultConfig)
362+
gen.generate('sm:md:lg:xl:2xl:hover:focus:active:dark:group-hover:w-4')
363+
const css = gen.toCSS()
364+
expect(css).toContain('width')
365+
})
366+
367+
it('should handle duplicate variants', () => {
368+
const gen = new CSSGenerator(defaultConfig)
369+
gen.generate('hover:hover:hover:bg-blue-500')
370+
const css = gen.toCSS()
371+
expect(css).toContain('background-color')
372+
})
373+
374+
it('should handle conflicting responsive variants', () => {
375+
const gen = new CSSGenerator(defaultConfig)
376+
gen.generate('sm:lg:md:xl:w-full')
377+
const css = gen.toCSS()
378+
// Should handle all variants even if order seems wrong
379+
expect(css.length).toBeGreaterThan(0)
380+
})
381+
382+
it('should handle all pseudo-class variants together', () => {
383+
const gen = new CSSGenerator(defaultConfig)
384+
gen.generate('hover:focus:active:visited:disabled:checked:bg-red-500')
385+
const css = gen.toCSS()
386+
expect(css).toContain(':hover')
387+
expect(css).toContain(':focus')
388+
})
389+
390+
it('should handle mixing responsive and pseudo-elements', () => {
391+
const gen = new CSSGenerator(defaultConfig)
392+
gen.generate('md:before:content-["test"]')
393+
gen.generate('lg:after:block')
394+
const css = gen.toCSS()
395+
expect(css).toContain('::before')
396+
expect(css).toContain('::after')
397+
})
398+
399+
it('should handle group and peer variants together', () => {
400+
const gen = new CSSGenerator(defaultConfig)
401+
gen.generate('group-hover:peer-focus:bg-blue-500')
402+
const css = gen.toCSS()
403+
// Complex stacking of group and peer variants
404+
// Generator handles these by applying the variants in order
405+
expect(css).toContain('background-color')
406+
expect(css).toContain('#3b82f6')
407+
})
408+
409+
it('should handle important with all variant types', () => {
410+
const gen = new CSSGenerator(defaultConfig)
411+
gen.generate('!sm:hover:dark:first:bg-red-500')
412+
const css = gen.toCSS()
413+
expect(css).toContain('!important')
414+
})
415+
})
358416
})
359417
})

packages/headwind/test/parser.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,3 +417,91 @@ describe('extractClasses - Edge Cases', () => {
417417
expect(result.size).toBe(3)
418418
})
419419
})
420+
421+
describe('parseClass - Extreme Edge Cases', () => {
422+
it('should handle null-like strings', () => {
423+
expect(() => parseClass('null')).not.toThrow()
424+
expect(() => parseClass('undefined')).not.toThrow()
425+
expect(() => parseClass('NaN')).not.toThrow()
426+
})
427+
428+
it('should handle numbers as class names', () => {
429+
const result = parseClass('123')
430+
expect(result.utility).toBe('123')
431+
expect(result.value).toBeUndefined()
432+
})
433+
434+
it('should handle special characters in class names', () => {
435+
expect(() => parseClass('@apply')).not.toThrow()
436+
expect(() => parseClass('$variable')).not.toThrow()
437+
expect(() => parseClass('#id-like')).not.toThrow()
438+
})
439+
440+
it('should handle unicode characters', () => {
441+
const result = parseClass('w-[20px]') // Normal
442+
expect(result.arbitrary).toBe(true)
443+
444+
const result2 = parseClass('text-[📝]') // Emoji
445+
expect(result2.arbitrary).toBe(true)
446+
})
447+
448+
it('should handle extremely nested brackets', () => {
449+
const result = parseClass('w-[calc(calc(100%-10px)-5px)]')
450+
expect(result.arbitrary).toBe(true)
451+
expect(result.value).toContain('calc')
452+
})
453+
454+
it('should handle URL in arbitrary value', () => {
455+
const result = parseClass('bg-[url(https://example.com/image.jpg)]')
456+
expect(result.arbitrary).toBe(true)
457+
expect(result.value).toContain('url')
458+
})
459+
460+
it('should handle data URL in arbitrary value', () => {
461+
const result = parseClass('bg-[url(data:image/svg+xml;base64,ABC123)]')
462+
expect(result.arbitrary).toBe(true)
463+
expect(result.value).toContain('data:')
464+
})
465+
466+
it('should handle CSS variables in arbitrary value', () => {
467+
const result = parseClass('w-[var(--custom-width)]')
468+
expect(result.arbitrary).toBe(true)
469+
expect(result.value).toContain('var(--')
470+
})
471+
472+
it('should handle clamp in arbitrary value', () => {
473+
const result = parseClass('text-[clamp(1rem,5vw,3rem)]')
474+
expect(result.arbitrary).toBe(true)
475+
expect(result.value).toContain('clamp')
476+
})
477+
478+
it('should handle min/max in arbitrary value', () => {
479+
const result = parseClass('w-[min(100%,500px)]')
480+
expect(result.arbitrary).toBe(true)
481+
expect(result.value).toContain('min')
482+
})
483+
484+
it('should handle zero with units', () => {
485+
expect(() => parseClass('w-[0px]')).not.toThrow()
486+
expect(() => parseClass('h-[0rem]')).not.toThrow()
487+
expect(() => parseClass('p-[0em]')).not.toThrow()
488+
})
489+
490+
it('should handle extremely large numbers', () => {
491+
const result = parseClass('w-[99999999px]')
492+
expect(result.arbitrary).toBe(true)
493+
expect(result.value).toBe('99999999px')
494+
})
495+
496+
it('should handle extremely small numbers', () => {
497+
const result = parseClass('w-[0.0001px]')
498+
expect(result.arbitrary).toBe(true)
499+
expect(result.value).toBe('0.0001px')
500+
})
501+
502+
it('should handle scientific notation', () => {
503+
const result = parseClass('w-[1e10px]')
504+
expect(result.arbitrary).toBe(true)
505+
expect(result.value).toBe('1e10px')
506+
})
507+
})

0 commit comments

Comments
 (0)