1use std::collections::BTreeMap;
14
15use serde_json::Value;
16
17use super::{GradientStopSpec, canonicalize_gradient_stops};
18
19pub const USER_VAR_PREFIX: &str = "--psp-user--";
20
21#[derive(Clone, Debug, PartialEq)]
22pub struct NamedValue {
23 pub name: String,
24 pub value: String,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub enum CssKind {
29 Gradient,
30 Palette,
31 Color,
32}
33
34impl CssKind {
35 pub const ALL: [CssKind; 3] = [CssKind::Gradient, CssKind::Palette, CssKind::Color];
36
37 pub fn var_prefix(self) -> &'static str {
38 match self {
39 CssKind::Gradient => "--psp-user--gradient-",
40 CssKind::Palette => "--psp-user--palette-",
41 CssKind::Color => "--psp-user--color-",
42 }
43 }
44
45 pub fn of_var(name: &str) -> Option<CssKind> {
46 CssKind::ALL
47 .into_iter()
48 .find(|kind| name.starts_with(kind.var_prefix()))
49 }
50
51 pub fn canonicalize(self, src: &str) -> Result<String, String> {
52 match self {
53 CssKind::Gradient => CssGradient::parse(src).map(|x| x.to_css()),
54 CssKind::Palette => CssPalette::parse(src).map(|x| x.to_css()),
55 CssKind::Color => CssColor::parse(src).map(|x| x.to_css()),
56 }
57 }
58
59 pub fn short_name(self, var_name: &str) -> String {
60 var_name
61 .strip_prefix(self.var_prefix())
62 .unwrap_or(var_name)
63 .to_owned()
64 }
65}
66
67pub fn parse_var_ref(src: &str) -> Option<&str> {
70 let inner = src.trim().strip_prefix("var(")?.strip_suffix(')')?.trim();
71 if !inner.starts_with(USER_VAR_PREFIX)
72 || inner.len() == USER_VAR_PREFIX.len()
73 || inner.contains(|c: char| c.is_whitespace() || c == ',' || c == ')')
74 {
75 return None;
76 }
77
78 Some(inner)
79}
80
81pub fn format_var_ref(name: &str) -> String {
83 format!("var({name})")
84}
85
86fn parse_kind_var(src: &str, kind: CssKind) -> Result<Option<String>, String> {
87 let Some(name) = parse_var_ref(src) else {
88 return Ok(None);
89 };
90
91 if name.starts_with(kind.var_prefix()) {
92 Ok(Some(name.to_owned()))
93 } else {
94 Err(format!(
95 "`{src}` is not a {} reference (expected `var({}…)`)",
96 kind_label(kind),
97 kind.var_prefix()
98 ))
99 }
100}
101
102fn kind_label(kind: CssKind) -> &'static str {
103 match kind {
104 CssKind::Gradient => "gradient",
105 CssKind::Palette => "palette",
106 CssKind::Color => "color",
107 }
108}
109
110fn hex_channel(src: &str) -> Result<u8, String> {
111 u8::from_str_radix(src, 16).map_err(|_| format!("invalid hex color `#{src}`"))
112}
113
114fn channel_from_token(token: &str) -> Result<u8, String> {
115 let token = token.trim();
116 let value = match token.strip_suffix('%') {
117 Some(pct) => {
118 pct.trim()
119 .parse::<f64>()
120 .map_err(|_| format!("invalid color channel `{token}`"))?
121 / 100.0
122 * 255.0
123 },
124 None => token
125 .parse::<f64>()
126 .map_err(|_| format!("invalid color channel `{token}`"))?,
127 };
128
129 Ok(value.round().clamp(0.0, 255.0) as u8)
130}
131
132pub fn normalize_css_color(src: &str) -> Result<String, String> {
135 let src = src.trim();
136 if let Some(hex) = src.strip_prefix('#') {
137 let (r, g, b) = match hex.len() {
138 3 | 4 => {
139 let d = |i: usize| hex_channel(&hex[i..i + 1]).map(|x| x * 17);
140 (d(0)?, d(1)?, d(2)?)
141 },
142 6 | 8 => (
143 hex_channel(&hex[0..2])?,
144 hex_channel(&hex[2..4])?,
145 hex_channel(&hex[4..6])?,
146 ),
147 _ => return Err(format!("invalid hex color `{src}`")),
148 };
149
150 return Ok(format!("#{r:02x}{g:02x}{b:02x}"));
151 }
152
153 let lower = src.to_ascii_lowercase();
154 let body = lower
155 .strip_prefix("rgba(")
156 .or_else(|| lower.strip_prefix("rgb("))
157 .and_then(|x| x.strip_suffix(')'))
158 .ok_or_else(|| format!("unsupported color `{src}`"))?;
159
160 let body = body.split('/').next().unwrap_or("");
161 let tokens: Vec<&str> = body
162 .split(|c: char| c == ',' || c.is_whitespace())
163 .filter(|x| !x.is_empty())
164 .collect();
165
166 if tokens.len() < 3 || tokens.len() > 4 {
167 return Err(format!("unsupported color `{src}`"));
168 }
169
170 Ok(format!(
171 "#{:02x}{:02x}{:02x}",
172 channel_from_token(tokens[0])?,
173 channel_from_token(tokens[1])?,
174 channel_from_token(tokens[2])?
175 ))
176}
177
178fn split_entries(body: &str) -> Vec<&str> {
179 let mut parts = vec![];
180 let mut depth = 0i32;
181 let mut start = 0;
182 for (i, ch) in body.char_indices() {
183 match ch {
184 '(' => depth += 1,
185 ')' => depth -= 1,
186 ',' if depth == 0 => {
187 parts.push(&body[start..i]);
188 start = i + 1;
189 },
190 _ => {},
191 }
192 }
193
194 parts.push(&body[start..]);
195 parts
196}
197
198fn is_direction_token(entry: &str) -> bool {
199 let entry = entry.trim().to_ascii_lowercase();
200 if entry.starts_with("to ") {
201 return true;
202 }
203
204 ["deg", "rad", "grad", "turn"].iter().any(|unit| {
205 entry
206 .strip_suffix(unit)
207 .map(|n| n.parse::<f64>().is_ok())
208 .unwrap_or(false)
209 })
210}
211
212fn tokenize_linear_gradient(src: &str) -> Result<Vec<(String, Option<f64>)>, String> {
213 let src = src.trim();
214 let lower = src.to_ascii_lowercase();
215 let body = lower
216 .strip_prefix("linear-gradient(")
217 .and_then(|x| x.strip_suffix(')'))
218 .ok_or_else(|| format!("expected `linear-gradient(…)`, got `{src}`"))?;
219
220 let entries = split_entries(body);
221 let mut out = vec![];
222 for (index, entry) in entries.iter().enumerate() {
223 let entry = entry.trim();
224 if entry.is_empty() {
225 return Err(format!("empty entry in `{src}`"));
226 }
227
228 if index == 0 && is_direction_token(entry) {
229 continue;
230 }
231
232 let (color, position) = match entry.rfind(|c: char| c.is_whitespace()) {
233 Some(at) if !entry[at..].contains(')') => {
234 let tail = entry[at..].trim();
235 if let Some(pct) = tail.strip_suffix('%') {
236 let value = pct
237 .parse::<f64>()
238 .map_err(|_| format!("invalid stop position `{tail}`"))?;
239 (entry[..at].trim(), Some(value / 100.0))
240 } else if tail.starts_with(|c: char| c.is_ascii_digit() || c == '.' || c == '-') {
241 return Err(format!(
242 "unsupported stop position `{tail}` (only `%` is accepted)"
243 ));
244 } else {
245 (entry, None)
246 }
247 },
248 _ => (entry, None),
249 };
250
251 out.push((normalize_css_color(color)?, position));
252 }
253
254 Ok(out)
255}
256
257fn format_percent(offset: f64) -> String {
258 let text = format!("{:.1}", offset * 100.0);
259 let text = text.strip_suffix(".0").unwrap_or(&text).to_owned();
260 format!("{text}%")
261}
262
263pub fn gradient_to_css(stops: &[GradientStopSpec]) -> String {
266 let body = canonicalize_gradient_stops(stops.to_vec())
267 .iter()
268 .map(|stop| format!("{} {}", stop.color, format_percent(stop.offset)))
269 .collect::<Vec<_>>()
270 .join(", ");
271
272 format!("linear-gradient(to right, {body})")
273}
274
275pub fn palette_to_css(colors: &[String]) -> String {
278 format!("linear-gradient(to right, {})", colors.join(", "))
279}
280
281#[derive(Clone, Debug, PartialEq)]
285pub enum CssGradient {
286 Literal(Vec<GradientStopSpec>),
288 Var(String),
290}
291
292impl CssGradient {
293 pub fn parse(src: &str) -> Result<Self, String> {
296 if let Some(name) = parse_kind_var(src, CssKind::Gradient)? {
297 return Ok(CssGradient::Var(name));
298 }
299
300 let entries = tokenize_linear_gradient(src)?;
301 if entries.len() < 2 {
302 return Err(format!("a gradient needs at least 2 stops: `{src}`"));
303 }
304
305 let mut offsets: Vec<Option<f64>> = entries.iter().map(|(_, p)| *p).collect();
306 let last = offsets.len() - 1;
307 offsets[0].get_or_insert(0.0);
308 offsets[last].get_or_insert(1.0);
309 let mut i = 1;
310 while i < last {
311 if offsets[i].is_some() {
312 i += 1;
313 continue;
314 }
315
316 let mut j = i + 1;
317 while offsets[j].is_none() {
318 j += 1;
319 }
320
321 let before = offsets[i - 1].unwrap();
322 let after = offsets[j].unwrap();
323 let span = (j - (i - 1)) as f64;
324 for (k, slot) in offsets.iter_mut().enumerate().take(j).skip(i) {
325 *slot = Some(before + ((k - (i - 1)) as f64 / span) * (after - before));
326 }
327
328 i = j;
329 }
330
331 let stops = entries
332 .into_iter()
333 .zip(offsets)
334 .map(|((color, _), offset)| GradientStopSpec {
335 color,
336 offset: offset.unwrap(),
337 })
338 .collect();
339
340 Ok(CssGradient::Literal(canonicalize_gradient_stops(stops)))
341 }
342
343 pub fn to_css(&self) -> String {
344 match self {
345 CssGradient::Literal(stops) => gradient_to_css(stops),
346 CssGradient::Var(name) => format_var_ref(name),
347 }
348 }
349}
350
351#[derive(Clone, Debug, PartialEq)]
355pub enum CssPalette {
356 Literal(Vec<String>),
358 Var(String),
360}
361
362impl CssPalette {
363 pub fn parse(src: &str) -> Result<Self, String> {
366 if let Some(name) = parse_kind_var(src, CssKind::Palette)? {
367 return Ok(CssPalette::Var(name));
368 }
369
370 let entries = tokenize_linear_gradient(src)?;
371 if entries.is_empty() {
372 return Err(format!("a palette needs at least 1 color: `{src}`"));
373 }
374
375 let mut colors = Vec::with_capacity(entries.len());
376 for (color, position) in entries {
377 if position.is_some() {
378 return Err(format!(
379 "palette entries must not carry positions (a positioned `linear-gradient()` \
380 is a gradient, not a palette): `{src}`"
381 ));
382 }
383
384 colors.push(color);
385 }
386
387 Ok(CssPalette::Literal(colors))
388 }
389
390 pub fn to_css(&self) -> String {
391 match self {
392 CssPalette::Literal(colors) => palette_to_css(colors),
393 CssPalette::Var(name) => format_var_ref(name),
394 }
395 }
396}
397
398#[derive(Clone, Debug, PartialEq)]
402pub enum CssColor {
403 Literal(String),
405 Var(String),
407}
408
409impl CssColor {
410 pub fn parse(src: &str) -> Result<Self, String> {
411 if let Some(name) = parse_kind_var(src, CssKind::Color)? {
412 return Ok(CssColor::Var(name));
413 }
414
415 normalize_css_color(src).map(CssColor::Literal)
416 }
417
418 pub fn to_css(&self) -> String {
419 match self {
420 CssColor::Literal(color) => color.clone(),
421 CssColor::Var(name) => format_var_ref(name),
422 }
423 }
424}
425
426pub fn canonicalize_css_gradient(src: &str) -> Result<String, String> {
427 CssKind::Gradient.canonicalize(src)
428}
429
430pub fn canonicalize_css_palette(src: &str) -> Result<String, String> {
431 CssKind::Palette.canonicalize(src)
432}
433
434pub fn canonicalize_css_color(src: &str) -> Result<String, String> {
435 CssKind::Color.canonicalize(src)
436}
437
438#[derive(Clone, Debug, PartialEq)]
441pub struct CssLiteralUse {
442 pub column: String,
443 pub key: String,
444 pub kind: CssKind,
445 pub literal: String,
446}
447
448pub fn assign_palette_names(
451 restored: &BTreeMap<String, String>,
452 host: &[NamedValue],
453 in_use: &[(CssKind, String)],
454 is_taken: &dyn Fn(&str) -> bool,
455) -> BTreeMap<String, String> {
456 let mut set = restored.clone();
457 for (kind, literal) in in_use {
458 if palette_name_for(&set, *kind, literal).is_some() {
459 continue;
460 }
461
462 let name = host
463 .iter()
464 .find(|entry| entry.name.starts_with(kind.var_prefix()) && entry.value == *literal)
465 .map(|entry| entry.name.clone())
466 .unwrap_or_else(|| {
467 (1..)
468 .map(|n| format!("{}{n}", kind.var_prefix()))
469 .find(|name| !set.contains_key(name) && !is_taken(name))
470 .unwrap()
471 });
472
473 set.insert(name, literal.clone());
474 }
475
476 set
477}
478
479pub fn palette_name_for(
481 set: &BTreeMap<String, String>,
482 kind: CssKind,
483 literal: &str,
484) -> Option<String> {
485 set.iter()
486 .find(|(name, value)| name.starts_with(kind.var_prefix()) && *value == literal)
487 .map(|(name, _)| name.clone())
488}
489
490pub fn resolve_css_refs(
494 entry: &mut serde_json::Map<String, Value>,
495 lookup: &dyn Fn(&str) -> Option<String>,
496) -> Vec<(String, String)> {
497 let mut dropped = vec![];
498 let refs: Vec<(String, String)> = entry
499 .iter()
500 .filter_map(|(key, value)| {
501 let name = parse_var_ref(value.as_str()?)?;
502 Some((key.clone(), name.to_owned()))
503 })
504 .collect();
505
506 for (key, name) in refs {
507 let resolved = CssKind::of_var(&name)
508 .and_then(|kind| lookup(&name).and_then(|raw| kind.canonicalize(&raw).ok()));
509
510 match resolved {
511 Some(literal) => {
512 entry.insert(key, Value::String(literal));
513 },
514 None => {
515 entry.remove(&key);
516 dropped.push((key, name));
517 },
518 }
519 }
520
521 dropped
522}
523
524#[cfg(test)]
525mod tests {
526 use serde_json::json;
527
528 use super::*;
529
530 fn stop(color: &str, offset: f64) -> GradientStopSpec {
531 GradientStopSpec {
532 color: color.to_owned(),
533 offset,
534 }
535 }
536
537 #[test]
538 fn colors_normalize_to_lowercase_hex() {
539 assert_eq!(normalize_css_color("#ABC").unwrap(), "#aabbcc");
540 assert_eq!(normalize_css_color("#abcd").unwrap(), "#aabbcc");
541 assert_eq!(normalize_css_color(" #FF7F0E ").unwrap(), "#ff7f0e");
542 assert_eq!(normalize_css_color("#ff7f0e80").unwrap(), "#ff7f0e");
543 assert_eq!(normalize_css_color("rgb(255, 127, 14)").unwrap(), "#ff7f0e");
544 assert_eq!(
545 normalize_css_color("rgba(255,127,14,0.5)").unwrap(),
546 "#ff7f0e"
547 );
548 assert_eq!(
549 normalize_css_color("rgb(255 127 14 / 50%)").unwrap(),
550 "#ff7f0e"
551 );
552 assert_eq!(
553 normalize_css_color("rgb(100%, 0%, 50%)").unwrap(),
554 "#ff0080"
555 );
556 assert_eq!(normalize_css_color("rgb(300, -5, 14)").unwrap(), "#ff000e");
557 assert!(normalize_css_color("red").is_err());
558 assert!(normalize_css_color("#12345").is_err());
559 assert!(normalize_css_color("hsl(0, 100%, 50%)").is_err());
560 assert!(normalize_css_color("#gggggg").is_err());
561 assert!(normalize_css_color("").is_err());
562 }
563
564 #[test]
565 fn var_refs_are_whole_value_and_namespaced() {
566 assert_eq!(
567 parse_var_ref("var(--psp-user--gradient-1)"),
568 Some("--psp-user--gradient-1")
569 );
570 assert_eq!(
571 parse_var_ref(" var( --psp-user--color-hot ) "),
572 Some("--psp-user--color-hot")
573 );
574 assert_eq!(parse_var_ref("var(--psp-charts--gradient)"), None);
575 assert_eq!(parse_var_ref("var(--psp-user--)"), None);
576 assert_eq!(parse_var_ref("var(--psp-user--gradient-1, red)"), None);
577 assert_eq!(parse_var_ref("#ff0000"), None);
578 assert_eq!(
579 parse_var_ref("linear-gradient(var(--psp-user--color-1), #fff)"),
580 None
581 );
582 }
583
584 #[test]
585 fn kind_is_carried_by_the_reference_prefix() {
586 assert_eq!(
587 CssKind::of_var("--psp-user--gradient-1"),
588 Some(CssKind::Gradient)
589 );
590 assert_eq!(
591 CssKind::of_var("--psp-user--palette-warm"),
592 Some(CssKind::Palette)
593 );
594 assert_eq!(CssKind::of_var("--psp-user--color-1"), Some(CssKind::Color));
595 assert_eq!(CssKind::of_var("--psp-user--other-1"), None);
596 assert_eq!(
597 CssKind::Palette.short_name("--psp-user--palette-warm"),
598 "warm"
599 );
600
601 assert_eq!(
602 CssGradient::parse("var(--psp-user--gradient-1)").unwrap(),
603 CssGradient::Var("--psp-user--gradient-1".to_owned())
604 );
605 assert!(CssGradient::parse("var(--psp-user--palette-1)").is_err());
606 assert!(CssPalette::parse("var(--psp-user--gradient-1)").is_err());
607 assert!(CssColor::parse("var(--psp-user--palette-1)").is_err());
608 assert_eq!(
609 CssColor::parse("var(--psp-user--color-1)")
610 .unwrap()
611 .to_css(),
612 "var(--psp-user--color-1)"
613 );
614
615 assert_eq!(
616 canonicalize_css_palette("var(--psp-user--palette-1)").unwrap(),
617 "var(--psp-user--palette-1)"
618 );
619 }
620
621 #[test]
622 fn gradient_round_trip_is_canonical_and_idempotent() {
623 let src = "linear-gradient(to right, #0366d6 0%, #ffffff 33.3%, #ff7f0e 100%)";
624 let canon = canonicalize_css_gradient(src).unwrap();
625 assert_eq!(canon, src);
626 assert_eq!(canonicalize_css_gradient(&canon).unwrap(), canon);
627
628 let CssGradient::Literal(stops) = CssGradient::parse(src).unwrap() else {
629 panic!("expected literal");
630 };
631
632 assert_eq!(stops, vec![
633 stop("#0366d6", 0.0),
634 stop("#ffffff", 0.333),
635 stop("#ff7f0e", 1.0),
636 ]);
637
638 let css = gradient_to_css(&[
639 stop("#222222", 1.7),
640 stop("#111111", 1.0 / 3.0),
641 stop("#000000", -0.5),
642 ]);
643
644 assert_eq!(
645 css,
646 "linear-gradient(to right, #000000 0%, #111111 33.3%, #222222 100%)"
647 );
648
649 assert_eq!(format_percent(0.375), "37.5%");
650 assert_eq!(format_percent(0.1), "10%");
651 assert_eq!(format_percent(1.0), "100%");
652 }
653
654 #[test]
655 fn gradient_accepts_loose_input() {
656 assert_eq!(
657 canonicalize_css_gradient("linear-gradient(#0366d6 0%, #ff7f0e 100%)").unwrap(),
658 "linear-gradient(to right, #0366d6 0%, #ff7f0e 100%)"
659 );
660
661 assert_eq!(
662 canonicalize_css_gradient("LINEAR-GRADIENT(90deg, RGB(3, 102, 214), #FFF, #ff7f0e)")
663 .unwrap(),
664 "linear-gradient(to right, #0366d6 0%, #ffffff 50%, #ff7f0e 100%)"
665 );
666
667 assert_eq!(
668 canonicalize_css_gradient(
669 "linear-gradient(to bottom, #000 10%, #111, #222, #333 70%, #444)"
670 )
671 .unwrap(),
672 "linear-gradient(to right, #000000 10%, #111111 30%, #222222 50%, #333333 70%, \
673 #444444 100%)"
674 );
675
676 assert_eq!(
677 canonicalize_css_gradient("linear-gradient(#fff 120%, #000 -5%)").unwrap(),
678 "linear-gradient(to right, #000000 0%, #ffffff 100%)"
679 );
680 }
681
682 #[test]
683 fn gradient_rejects_malformed_input() {
684 assert!(canonicalize_css_gradient("#ff0000").is_err());
685 assert!(canonicalize_css_gradient("linear-gradient(#ff0000)").is_err());
686 assert!(canonicalize_css_gradient("linear-gradient(#ff0000 0px, #fff 10px)").is_err());
687 assert!(canonicalize_css_gradient("linear-gradient(red, blue)").is_err());
688 assert!(canonicalize_css_gradient("radial-gradient(#000, #fff)").is_err());
689 assert!(canonicalize_css_gradient("linear-gradient(#000, , #fff)").is_err());
690 assert!(canonicalize_css_gradient("").is_err());
691 }
692
693 #[test]
694 fn palette_round_trip_and_rules() {
695 let src = "linear-gradient(to right, #0366d6, #ff7f0e, #2ca02c)";
696 assert_eq!(canonicalize_css_palette(src).unwrap(), src);
697 assert_eq!(
698 CssPalette::parse(src).unwrap(),
699 CssPalette::Literal(vec![
700 "#0366d6".to_owned(),
701 "#ff7f0e".to_owned(),
702 "#2ca02c".to_owned()
703 ])
704 );
705
706 assert_eq!(
707 canonicalize_css_palette("linear-gradient(90deg, rgb(3,102,214), #FFF)").unwrap(),
708 "linear-gradient(to right, #0366d6, #ffffff)"
709 );
710
711 assert_eq!(
712 canonicalize_css_palette("linear-gradient(#abc)").unwrap(),
713 "linear-gradient(to right, #aabbcc)"
714 );
715
716 assert_eq!(
717 palette_to_css(&["#000000".to_owned(), "#ffffff".to_owned()]),
718 "linear-gradient(to right, #000000, #ffffff)"
719 );
720 }
721
722 #[test]
723 fn palette_rejects_positions() {
724 let gradient = gradient_to_css(&[stop("#000000", 0.0), stop("#ffffff", 1.0)]);
725 assert!(CssPalette::parse(&gradient).is_err());
726 assert!(canonicalize_css_palette("linear-gradient(#000, #fff 50%)").is_err());
727 assert!(canonicalize_css_palette("linear-gradient()").is_err());
728 assert!(canonicalize_css_palette("#000000").is_err());
729
730 let palette = palette_to_css(&["#000000".to_owned(), "#ffffff".to_owned()]);
731 assert_eq!(
732 canonicalize_css_gradient(&palette).unwrap(),
733 "linear-gradient(to right, #000000 0%, #ffffff 100%)"
734 );
735 }
736
737 #[test]
738 fn color_canonicalizes_literals_and_passes_refs() {
739 assert_eq!(canonicalize_css_color("#ABC").unwrap(), "#aabbcc");
740 assert_eq!(
741 canonicalize_css_color("var(--psp-user--color-hot)").unwrap(),
742 "var(--psp-user--color-hot)"
743 );
744 assert!(canonicalize_css_color("var(--psp-user--gradient-1)").is_err());
745 assert!(canonicalize_css_color("linear-gradient(#000, #fff)").is_err());
746 }
747
748 #[test]
749 fn palette_set_unions_restored_and_in_use_with_stable_names() {
750 let mut restored = BTreeMap::new();
751 restored.insert(
752 "--psp-user--gradient-heat".to_owned(),
753 "linear-gradient(to right, #000000 0%, #ffffff 100%)".to_owned(),
754 );
755 restored.insert("--psp-user--color-unused".to_owned(), "#123456".to_owned());
756 let host = vec![NamedValue {
757 name: "--psp-user--palette-1".to_owned(),
758 value: "linear-gradient(to right, #ff0000, #00ff00)".to_owned(),
759 }];
760 let taken =
761 |name: &str| name == "--psp-user--gradient-1" || name == "--psp-user--palette-1";
762 let in_use = vec![
763 (
764 CssKind::Gradient,
765 "linear-gradient(to right, #000000 0%, #ffffff 100%)".to_owned(),
766 ),
767 (
768 CssKind::Gradient,
769 "linear-gradient(to right, #111111 0%, #222222 100%)".to_owned(),
770 ),
771 (
772 CssKind::Gradient,
773 "linear-gradient(to right, #111111 0%, #222222 100%)".to_owned(),
774 ),
775 (
776 CssKind::Palette,
777 "linear-gradient(to right, #ff0000, #00ff00)".to_owned(),
778 ),
779 (CssKind::Color, "#abcdef".to_owned()),
780 ];
781
782 let set = assign_palette_names(&restored, &host, &in_use, &taken);
783 assert_eq!(
784 set.iter()
785 .map(|(k, v)| (k.as_str(), v.as_str()))
786 .collect::<Vec<_>>(),
787 vec![
788 ("--psp-user--color-1", "#abcdef"),
789 ("--psp-user--color-unused", "#123456"),
790 (
791 "--psp-user--gradient-2",
792 "linear-gradient(to right, #111111 0%, #222222 100%)"
793 ),
794 (
795 "--psp-user--gradient-heat",
796 "linear-gradient(to right, #000000 0%, #ffffff 100%)"
797 ),
798 (
799 "--psp-user--palette-1",
800 "linear-gradient(to right, #ff0000, #00ff00)"
801 ),
802 ]
803 );
804
805 assert_eq!(
806 palette_name_for(
807 &set,
808 CssKind::Gradient,
809 "linear-gradient(to right, #111111 0%, #222222 100%)"
810 )
811 .as_deref(),
812 Some("--psp-user--gradient-2")
813 );
814 assert_eq!(palette_name_for(&set, CssKind::Gradient, "#abcdef"), None);
815
816 assert_eq!(assign_palette_names(&set, &host, &in_use, &taken), set);
817 }
818
819 #[test]
820 fn resolver_inlines_known_refs_and_drops_the_rest() {
821 let host = |name: &str| -> Option<String> {
822 match name {
823 "--psp-user--gradient-1" => Some("linear-gradient(#000, #fff)".to_owned()),
824 "--psp-user--palette-warm" => Some("linear-gradient(#f00, #ff0)".to_owned()),
825 "--psp-user--color-hot" => Some("rgb(255, 0, 0)".to_owned()),
826 "--psp-user--palette-bad" => Some("linear-gradient(#000 0%, #fff 100%)".to_owned()),
827 "--psp-user--color-empty" => Some(" ".to_owned()),
828 _ => None,
829 }
830 };
831
832 let mut entry = json!({
833 "gradient": "var(--psp-user--gradient-1)",
834 "palette": "var(--psp-user--palette-warm)",
835 "color": "var(--psp-user--color-hot)",
836 "bad_palette": "var(--psp-user--palette-bad)",
837 "missing": "var(--psp-user--gradient-missing)",
838 "empty": "var(--psp-user--color-empty)",
839 "unknown_kind": "var(--psp-user--other-1)",
840 "literal": "#123456",
841 "number": 3,
842 })
843 .as_object()
844 .unwrap()
845 .clone();
846
847 let mut dropped = resolve_css_refs(&mut entry, &host);
848 dropped.sort();
849 assert_eq!(dropped, vec![
850 (
851 "bad_palette".to_owned(),
852 "--psp-user--palette-bad".to_owned()
853 ),
854 ("empty".to_owned(), "--psp-user--color-empty".to_owned()),
855 (
856 "missing".to_owned(),
857 "--psp-user--gradient-missing".to_owned()
858 ),
859 ("unknown_kind".to_owned(), "--psp-user--other-1".to_owned()),
860 ]);
861
862 assert_eq!(
863 entry,
864 json!({
865 "gradient": "linear-gradient(to right, #000000 0%, #ffffff 100%)",
866 "palette": "linear-gradient(to right, #ff0000, #ffff00)",
867 "color": "#ff0000",
868 "literal": "#123456",
869 "number": 3,
870 })
871 .as_object()
872 .unwrap()
873 .clone()
874 );
875
876 assert!(entry.values().all(|v| {
877 v.as_str()
878 .map(|s| parse_var_ref(s).is_none())
879 .unwrap_or(true)
880 }));
881 }
882}