Skip to content

Commit 531044b

Browse files
Automatically add quotes to content values (#361)
1 parent 70e802f commit 531044b

File tree

3 files changed

+250
-2
lines changed

3 files changed

+250
-2
lines changed

.changeset/weak-pants-prove.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@vanilla-extract/css': minor
3+
---
4+
5+
Automatically add quotes to `content` values when necessary
6+
7+
For example `{ content: '' }` will now return CSS of `{ content: "" }`

packages/css/src/transformCss.test.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,219 @@ describe('transformCss', () => {
548548
`);
549549
});
550550

551+
it('should handle blank content', () => {
552+
expect(
553+
transformCss({
554+
composedClassLists: [],
555+
localClassNames: ['testClass'],
556+
cssObjs: [
557+
{
558+
type: 'local',
559+
selector: 'testClass',
560+
rule: {
561+
content: '',
562+
},
563+
},
564+
],
565+
}).join('\n'),
566+
).toMatchInlineSnapshot(`
567+
".testClass {
568+
content: \\"\\";
569+
}"
570+
`);
571+
});
572+
573+
it('should add quotes to custom content values', () => {
574+
expect(
575+
transformCss({
576+
composedClassLists: [],
577+
localClassNames: ['testClass'],
578+
cssObjs: [
579+
{
580+
type: 'local',
581+
selector: 'testClass',
582+
rule: {
583+
content: 'hello',
584+
selectors: {
585+
'&': { content: 'there' },
586+
},
587+
},
588+
},
589+
],
590+
}).join('\n'),
591+
).toMatchInlineSnapshot(`
592+
".testClass {
593+
content: \\"hello\\";
594+
}
595+
.testClass {
596+
content: \\"there\\";
597+
}"
598+
`);
599+
});
600+
601+
it('should handle content with fallbacks', () => {
602+
expect(
603+
transformCss({
604+
composedClassLists: [],
605+
localClassNames: ['testClass'],
606+
cssObjs: [
607+
{
608+
type: 'local',
609+
selector: 'testClass',
610+
rule: {
611+
content: ['hello', 'there'],
612+
},
613+
},
614+
],
615+
}).join('\n'),
616+
).toMatchInlineSnapshot(`
617+
".testClass {
618+
content: \\"hello\\";
619+
content: \\"there\\";
620+
}"
621+
`);
622+
});
623+
624+
it('should not add quotes to content that already has quotes', () => {
625+
expect(
626+
transformCss({
627+
composedClassLists: [],
628+
localClassNames: ['testClass'],
629+
cssObjs: [
630+
{
631+
type: 'local',
632+
selector: 'testClass',
633+
rule: {
634+
content: "'hello there'",
635+
selectors: {
636+
'&': { content: '"hello there"' },
637+
},
638+
},
639+
},
640+
],
641+
}).join('\n'),
642+
).toMatchInlineSnapshot(`
643+
".testClass {
644+
content: 'hello there';
645+
}
646+
.testClass {
647+
content: \\"hello there\\";
648+
}"
649+
`);
650+
});
651+
652+
it('should not add quotes to meaningful content values (examples from mdn: https://developer.mozilla.org/en-US/docs/Web/CSS/content)', () => {
653+
expect(
654+
transformCss({
655+
composedClassLists: [],
656+
localClassNames: ['testClass'],
657+
cssObjs: [
658+
{
659+
type: 'local',
660+
selector: 'testClass',
661+
rule: {
662+
content: 'normal',
663+
selectors: {
664+
'._01 &': { content: 'none' },
665+
'._02 &': { content: 'url("http://www.example.com/test.png")' },
666+
'._03 &': { content: 'linear-gradient(#e66465, #9198e5)' },
667+
'._04 &': {
668+
content: 'image-set("image1x.png" 1x, "image2x.png" 2x)',
669+
},
670+
'._05 &': {
671+
content:
672+
'url("http://www.example.com/test.png") / "This is the alt text"',
673+
},
674+
'._06 &': { content: '"prefix"' },
675+
'._07 &': { content: 'counter(chapter_counter)' },
676+
'._08 &': { content: 'counter(chapter_counter, upper-roman)' },
677+
'._09 &': { content: 'counters(section_counter, ".")' },
678+
'._10 &': {
679+
content:
680+
'counters(section_counter, ".", decimal-leading-zero)',
681+
},
682+
'._11 &': { content: 'attr(value string)' },
683+
'._12 &': { content: 'open-quote' },
684+
'._13 &': { content: 'close-quote' },
685+
'._14 &': { content: 'no-open-quote' },
686+
'._15 &': { content: 'no-close-quote' },
687+
'._16 &': { content: 'open-quote counter(chapter_counter)' },
688+
'._17 &': { content: 'inherit' },
689+
'._18 &': { content: 'initial' },
690+
'._19 &': { content: 'revert' },
691+
'._20 &': { content: 'unset' },
692+
},
693+
},
694+
},
695+
],
696+
}).join('\n'),
697+
).toMatchInlineSnapshot(`
698+
".testClass {
699+
content: normal;
700+
}
701+
._01 .testClass {
702+
content: none;
703+
}
704+
._02 .testClass {
705+
content: url(\\"http://www.example.com/test.png\\");
706+
}
707+
._03 .testClass {
708+
content: linear-gradient(#e66465, #9198e5);
709+
}
710+
._04 .testClass {
711+
content: image-set(\\"image1x.png\\" 1x, \\"image2x.png\\" 2x);
712+
}
713+
._05 .testClass {
714+
content: url(\\"http://www.example.com/test.png\\") / \\"This is the alt text\\";
715+
}
716+
._06 .testClass {
717+
content: \\"prefix\\";
718+
}
719+
._07 .testClass {
720+
content: counter(chapter_counter);
721+
}
722+
._08 .testClass {
723+
content: counter(chapter_counter, upper-roman);
724+
}
725+
._09 .testClass {
726+
content: counters(section_counter, \\".\\");
727+
}
728+
._10 .testClass {
729+
content: counters(section_counter, \\".\\", decimal-leading-zero);
730+
}
731+
._11 .testClass {
732+
content: attr(value string);
733+
}
734+
._12 .testClass {
735+
content: open-quote;
736+
}
737+
._13 .testClass {
738+
content: close-quote;
739+
}
740+
._14 .testClass {
741+
content: no-open-quote;
742+
}
743+
._15 .testClass {
744+
content: no-close-quote;
745+
}
746+
._16 .testClass {
747+
content: open-quote counter(chapter_counter);
748+
}
749+
._17 .testClass {
750+
content: inherit;
751+
}
752+
._18 .testClass {
753+
content: initial;
754+
}
755+
._19 .testClass {
756+
content: revert;
757+
}
758+
._20 .testClass {
759+
content: unset;
760+
}"
761+
`);
762+
});
763+
551764
it('should handle simple pseudos within conditionals', () => {
552765
expect(
553766
transformCss({

packages/css/src/transformCss.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ class Stylesheet {
158158

159159
addConditionalRule(cssRule: CSSRule, conditions: Array<string>) {
160160
// Run `pixelifyProperties` before `transformVars` as we don't want to pixelify CSS Vars
161-
const rule = this.transformVars(this.pixelifyProperties(cssRule.rule));
161+
const rule = this.transformVars(
162+
this.transformContent(this.pixelifyProperties(cssRule.rule)),
163+
);
162164
const selector = this.transformSelector(cssRule.selector);
163165

164166
if (!this.currConditionalRuleset) {
@@ -180,7 +182,9 @@ class Stylesheet {
180182

181183
addRule(cssRule: CSSRule) {
182184
// Run `pixelifyProperties` before `transformVars` as we don't want to pixelify CSS Vars
183-
const rule = this.transformVars(this.pixelifyProperties(cssRule.rule));
185+
const rule = this.transformVars(
186+
this.transformContent(this.pixelifyProperties(cssRule.rule)),
187+
);
184188
const selector = this.transformSelector(cssRule.selector);
185189

186190
this.rules.push({
@@ -215,6 +219,30 @@ class Stylesheet {
215219
};
216220
}
217221

222+
transformContent({ content, ...rest }: CSSPropertiesWithVars) {
223+
if (typeof content === 'undefined') {
224+
return rest;
225+
}
226+
227+
// Handle fallback arrays:
228+
const contentArray = Array.isArray(content) ? content : [content];
229+
230+
return {
231+
content: contentArray.map((value) =>
232+
// This logic was adapted from Stitches :)
233+
value &&
234+
(value.includes('"') ||
235+
value.includes("'") ||
236+
/^([A-Za-z\-]+\([^]*|[^]*-quote|inherit|initial|none|normal|revert|unset)(\s|$)/.test(
237+
value,
238+
))
239+
? value
240+
: `"${value}"`,
241+
),
242+
...rest,
243+
};
244+
}
245+
218246
transformSelector(selector: string) {
219247
// Map class list compositions to single identifiers
220248
let transformedSelector = selector;

0 commit comments

Comments
 (0)