Skip to content

Commit aa0c8d1

Browse files
authored
Substitute variables (parcel-bundler#388)
1 parent 3fc05a9 commit aa0c8d1

File tree

5 files changed

+149
-1
lines changed

5 files changed

+149
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ serde = ["dep:serde", "smallvec/serde", "cssparser/serde", "parcel_selectors/ser
3838
sourcemap = ["parcel_sourcemap"]
3939
visitor = ["lightningcss-derive"]
4040
into_owned = ["lightningcss-derive"]
41+
substitute_variables = ["visitor", "into_owned"]
4142

4243
[dependencies]
4344
serde = { version = "1.0.123", features = ["derive"], optional = true }

src/lib.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22436,6 +22436,66 @@ mod tests {
2243622436
}
2243722437
}
2243822438

22439+
#[cfg(feature = "substitute_variables")]
22440+
#[test]
22441+
fn test_substitute_vars() {
22442+
use crate::properties::custom::TokenList;
22443+
use crate::traits::ParseWithOptions;
22444+
22445+
fn test(property: Property, vars: HashMap<&str, &str>, expected: &str) {
22446+
if let Property::Unparsed(unparsed) = property {
22447+
let vars = vars
22448+
.into_iter()
22449+
.map(|(k, v)| {
22450+
(
22451+
k,
22452+
TokenList::parse_string_with_options(v, ParserOptions::default()).unwrap(),
22453+
)
22454+
})
22455+
.collect();
22456+
let substituted = unparsed.substitute_variables(&vars).unwrap();
22457+
assert_eq!(
22458+
substituted.to_css_string(false, PrinterOptions::default()).unwrap(),
22459+
expected
22460+
);
22461+
} else {
22462+
panic!("Not an unparsed property");
22463+
}
22464+
}
22465+
22466+
let property = Property::parse_string("color".into(), "var(--test)", ParserOptions::default()).unwrap();
22467+
test(property, HashMap::from([("--test", "yellow")]), "color: #ff0");
22468+
22469+
let property =
22470+
Property::parse_string("color".into(), "var(--test, var(--foo))", ParserOptions::default()).unwrap();
22471+
test(property, HashMap::from([("--foo", "yellow")]), "color: #ff0");
22472+
let property = Property::parse_string(
22473+
"color".into(),
22474+
"var(--test, var(--foo, yellow))",
22475+
ParserOptions::default(),
22476+
)
22477+
.unwrap();
22478+
test(property, HashMap::new(), "color: #ff0");
22479+
22480+
let property =
22481+
Property::parse_string("width".into(), "calc(var(--a) + var(--b))", ParserOptions::default()).unwrap();
22482+
test(property, HashMap::from([("--a", "2px"), ("--b", "4px")]), "width: 6px");
22483+
22484+
let property = Property::parse_string("color".into(), "var(--a)", ParserOptions::default()).unwrap();
22485+
test(
22486+
property,
22487+
HashMap::from([("--a", "var(--b)"), ("--b", "yellow")]),
22488+
"color: #ff0",
22489+
);
22490+
22491+
let property = Property::parse_string("color".into(), "var(--a)", ParserOptions::default()).unwrap();
22492+
test(
22493+
property,
22494+
HashMap::from([("--a", "var(--b)"), ("--b", "var(--c)"), ("--c", "var(--a)")]),
22495+
"color: var(--a)",
22496+
);
22497+
}
22498+
2243922499
#[test]
2244022500
fn test_layer() {
2244122501
minify_test("@layer foo;", "@layer foo;");

src/properties/custom.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,28 @@ impl<'i> UnparsedProperty<'i> {
174174
value: self.value.clone(),
175175
}
176176
}
177+
178+
/// Substitutes variables and re-parses the property.
179+
#[cfg(feature = "substitute_variables")]
180+
pub fn substitute_variables<'x>(
181+
mut self,
182+
vars: &std::collections::HashMap<&str, TokenList<'i>>,
183+
) -> Result<super::Property<'x>, ()> {
184+
use super::Property;
185+
use crate::stylesheet::PrinterOptions;
186+
187+
// Substitute variables in the token list.
188+
self.value.substitute_variables(vars);
189+
190+
// Now stringify and re-parse the property to its fully parsed form.
191+
// Ideally we'd be able to reuse the tokens rather than printing, but cssparser doesn't provide a way to do that.
192+
let mut css = String::new();
193+
let mut dest = Printer::new(&mut css, PrinterOptions::default());
194+
self.value.to_css(&mut dest, false).unwrap();
195+
let property =
196+
Property::parse_string(self.property_id.clone(), &css, ParserOptions::default()).map_err(|_| ())?;
197+
Ok(property.into_owned())
198+
}
177199
}
178200

179201
/// A raw list of CSS tokens, with embedded parsed values.
@@ -235,6 +257,15 @@ impl<'i> TokenOrValue<'i> {
235257
}
236258
}
237259

260+
impl<'i, T> ParseWithOptions<'i, T> for TokenList<'i> {
261+
fn parse_with_options<'t>(
262+
input: &mut Parser<'i, 't>,
263+
options: &ParserOptions<T>,
264+
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
265+
TokenList::parse(input, options, 0)
266+
}
267+
}
268+
238269
impl<'i> TokenList<'i> {
239270
pub(crate) fn parse<'t, T>(
240271
input: &mut Parser<'i, 't>,
@@ -960,6 +991,49 @@ impl<'i> TokenList<'i> {
960991

961992
res
962993
}
994+
995+
/// Substitutes variables with the provided values.
996+
#[cfg(feature = "substitute_variables")]
997+
pub fn substitute_variables(&mut self, vars: &std::collections::HashMap<&str, TokenList<'i>>) {
998+
self.visit(&mut VarInliner { vars })
999+
}
1000+
}
1001+
1002+
#[cfg(feature = "substitute_variables")]
1003+
struct VarInliner<'a, 'i> {
1004+
vars: &'a std::collections::HashMap<&'a str, TokenList<'i>>,
1005+
}
1006+
1007+
#[cfg(feature = "substitute_variables")]
1008+
impl<'a, 'i> crate::visitor::Visitor<'i> for VarInliner<'a, 'i> {
1009+
const TYPES: crate::visitor::VisitTypes = crate::visit_types!(TOKENS | VARIABLES);
1010+
1011+
fn visit_token_list(&mut self, tokens: &mut TokenList<'i>) {
1012+
let mut i = 0;
1013+
let mut seen = std::collections::HashSet::new();
1014+
while i < tokens.0.len() {
1015+
let token = &mut tokens.0[i];
1016+
token.visit(self);
1017+
if let TokenOrValue::Var(var) = token {
1018+
if let Some(value) = self.vars.get(var.name.ident.0.as_ref()) {
1019+
// Ignore circular references.
1020+
if seen.insert(var.name.ident.0.clone()) {
1021+
tokens.0.splice(i..i + 1, value.0.iter().cloned());
1022+
// Don't advance. We need to replace any variables in the value.
1023+
continue;
1024+
}
1025+
} else if let Some(fallback) = &var.fallback {
1026+
let fallback = fallback.0.clone();
1027+
if seen.insert(var.name.ident.0.clone()) {
1028+
tokens.0.splice(i..i + 1, fallback.into_iter());
1029+
continue;
1030+
}
1031+
}
1032+
}
1033+
seen.clear();
1034+
i += 1;
1035+
}
1036+
}
9631037
}
9641038

9651039
/// A CSS variable reference.

src/rules/style.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub struct StyleRule<'i, R = DefaultAtRule> {
4343
pub loc: Location,
4444
}
4545

46+
#[cfg(feature = "serde")]
4647
fn default_rule_list<'i, R>() -> CssRuleList<'i, R> {
4748
CssRuleList(Vec::new())
4849
}

src/traits.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,23 @@ pub trait Parse<'i>: Sized {
2727
}
2828
}
2929

30-
pub(crate) trait ParseWithOptions<'i, T>: Sized {
30+
/// Trait for things that can be parsed from CSS syntax and require ParserOptions.
31+
pub trait ParseWithOptions<'i, T>: Sized {
32+
/// Parse a value of this type with the given options.
3133
fn parse_with_options<'t>(
3234
input: &mut Parser<'i, 't>,
3335
options: &ParserOptions<T>,
3436
) -> Result<Self, ParseError<'i, ParserError<'i>>>;
37+
38+
/// Parse a value from a string with the given options.
39+
fn parse_string_with_options(
40+
input: &'i str,
41+
options: ParserOptions<T>,
42+
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
43+
let mut input = ParserInput::new(input);
44+
let mut parser = Parser::new(&mut input);
45+
Self::parse_with_options(&mut parser, &options)
46+
}
3547
}
3648

3749
impl<'i, T: Parse<'i>, U> ParseWithOptions<'i, U> for T {

0 commit comments

Comments
 (0)