1use crate::{Error, Rgb, Variant};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Deserialize)]
9struct RawPalette {
10 name: String,
11 description: String,
12 colors: Colors,
13 base: Base,
14 layers: RawLayers,
15 state: RawState,
16 semantic: RawSemantic,
17 ansi: RawAnsi,
18}
19
20#[derive(Debug, Deserialize)]
21struct RawLayers {
22 base: ColorExpr,
23 surface: ColorExpr,
24 sunken: ColorExpr,
25 raised: ColorExpr,
26 border: ColorExpr,
27 inset: ColorExpr,
28}
29
30impl RawLayers {
31 fn resolve(&self, resolver: &Resolver) -> Result<Layers, Error> {
32 Ok(Layers {
33 base: resolve_expr(resolver, &self.base)?,
34 surface: resolve_expr(resolver, &self.surface)?,
35 sunken: resolve_expr(resolver, &self.sunken)?,
36 raised: resolve_expr(resolver, &self.raised)?,
37 border: resolve_expr(resolver, &self.border)?,
38 inset: resolve_expr(resolver, &self.inset)?,
39 })
40 }
41}
42
43#[derive(Debug, Deserialize)]
44struct RawState {
45 selection_bg: ColorExpr,
46 selection_fg: ColorExpr,
47 match_bg: ColorExpr,
48 cursor: ColorExpr,
49 cursor_text: ColorExpr,
50 info: ColorExpr,
51 hint: ColorExpr,
52 warning: ColorExpr,
53 error: ColorExpr,
54 active_bg: ColorExpr,
55 diff_added: ColorExpr,
56 diff_removed: ColorExpr,
57 diff_changed: ColorExpr,
58 diff_moved: ColorExpr,
59 conflict: ColorExpr,
60}
61
62impl RawState {
63 fn resolve(&self, resolver: &Resolver) -> Result<State, Error> {
64 Ok(State {
65 selection_bg: resolve_expr(resolver, &self.selection_bg)?,
66 selection_fg: resolve_expr(resolver, &self.selection_fg)?,
67 match_bg: resolve_expr(resolver, &self.match_bg)?,
68 cursor: resolve_expr(resolver, &self.cursor)?,
69 cursor_text: resolve_expr(resolver, &self.cursor_text)?,
70 info: resolve_expr(resolver, &self.info)?,
71 hint: resolve_expr(resolver, &self.hint)?,
72 warning: resolve_expr(resolver, &self.warning)?,
73 error: resolve_expr(resolver, &self.error)?,
74 active_bg: resolve_expr(resolver, &self.active_bg)?,
75 diff_added: resolve_expr(resolver, &self.diff_added)?,
76 diff_removed: resolve_expr(resolver, &self.diff_removed)?,
77 diff_changed: resolve_expr(resolver, &self.diff_changed)?,
78 diff_moved: resolve_expr(resolver, &self.diff_moved)?,
79 conflict: resolve_expr(resolver, &self.conflict)?,
80 })
81 }
82}
83
84#[derive(Debug, Deserialize)]
86struct RawAnsiColors {
87 black: ColorExpr,
88 red: ColorExpr,
89 green: ColorExpr,
90 yellow: ColorExpr,
91 blue: ColorExpr,
92 magenta: ColorExpr,
93 cyan: ColorExpr,
94 white: ColorExpr,
95}
96
97impl RawAnsiColors {
98 fn resolve(&self, resolver: &impl ResolveRef) -> Result<Ansi, Error> {
99 Ok(Ansi {
100 black: resolve_expr(resolver, &self.black)?,
101 red: resolve_expr(resolver, &self.red)?,
102 green: resolve_expr(resolver, &self.green)?,
103 yellow: resolve_expr(resolver, &self.yellow)?,
104 blue: resolve_expr(resolver, &self.blue)?,
105 magenta: resolve_expr(resolver, &self.magenta)?,
106 cyan: resolve_expr(resolver, &self.cyan)?,
107 white: resolve_expr(resolver, &self.white)?,
108 })
109 }
110}
111
112#[derive(Debug, Deserialize)]
113struct RawAnsi {
114 #[serde(flatten)]
115 base: RawAnsiColors,
116 bright: RawAnsiColors,
117}
118
119#[derive(Debug, Deserialize)]
120struct RawSemantic {
121 text: ColorExpr,
122 comment: ColorExpr,
123 string: ColorExpr,
124 keyword: ColorExpr,
125 number: ColorExpr,
126 constant: ColorExpr,
127 r#type: ColorExpr,
128 function: ColorExpr,
129 variable: ColorExpr,
130 success: ColorExpr,
131 path: ColorExpr,
132 r#macro: ColorExpr,
133 escape: ColorExpr,
134 regexp: ColorExpr,
135 link: ColorExpr,
136 directory: ColorExpr,
137}
138
139impl RawSemantic {
140 fn resolve(&self, resolver: &Resolver) -> Result<Semantic, Error> {
141 Ok(Semantic {
142 text: resolve_expr(resolver, &self.text)?,
143 comment: resolve_expr(resolver, &self.comment)?,
144 string: resolve_expr(resolver, &self.string)?,
145 keyword: resolve_expr(resolver, &self.keyword)?,
146 number: resolve_expr(resolver, &self.number)?,
147 constant: resolve_expr(resolver, &self.constant)?,
148 r#type: resolve_expr(resolver, &self.r#type)?,
149 function: resolve_expr(resolver, &self.function)?,
150 variable: resolve_expr(resolver, &self.variable)?,
151 success: resolve_expr(resolver, &self.success)?,
152 path: resolve_expr(resolver, &self.path)?,
153 r#macro: resolve_expr(resolver, &self.r#macro)?,
154 escape: resolve_expr(resolver, &self.escape)?,
155 regexp: resolve_expr(resolver, &self.regexp)?,
156 link: resolve_expr(resolver, &self.link)?,
157 directory: resolve_expr(resolver, &self.directory)?,
158 })
159 }
160}
161
162#[derive(Debug, Clone, Deserialize, Serialize)]
164pub struct Lantern {
165 pub ember: String, pub near: String, pub mid: String, pub far: String, }
170
171#[derive(Debug, Clone, Deserialize, Serialize)]
172pub struct Colors {
173 pub lantern: Lantern,
174 pub life: String,
175 pub night: String,
176 pub rain: String,
177 pub muted: String,
178}
179
180#[derive(Debug, Clone, Deserialize, Serialize)]
181pub struct Base {
182 pub background: String,
183 pub foreground: String,
184}
185
186#[derive(Debug, Clone, Deserialize, Serialize)]
187pub struct Layers {
188 pub base: String,
189 pub surface: String,
190 pub sunken: String,
191 pub raised: String,
192 pub border: String,
193 pub inset: String,
194}
195
196#[derive(Debug, Clone, Deserialize, Serialize)]
197pub struct State {
198 pub selection_bg: String,
199 pub selection_fg: String,
200 pub match_bg: String,
201 pub cursor: String,
202 pub cursor_text: String,
203 pub info: String,
204 pub hint: String,
205 pub warning: String,
206 pub error: String,
207 pub active_bg: String,
208 pub diff_added: String,
209 pub diff_removed: String,
210 pub diff_changed: String,
211 pub diff_moved: String,
212 pub conflict: String,
213}
214
215#[derive(Debug, Clone, Serialize)]
216pub struct Semantic {
217 pub text: String,
218 pub comment: String,
219 pub string: String,
220 pub keyword: String,
221 pub number: String,
222 pub constant: String,
223 pub r#type: String,
224 pub function: String,
225 pub variable: String,
226 pub success: String,
227 pub path: String,
228 pub r#macro: String,
229 pub escape: String,
230 pub regexp: String,
231 pub link: String,
232 pub directory: String,
233}
234
235#[derive(Debug, Clone, Serialize)]
236pub struct Ansi {
237 pub black: String,
238 pub red: String,
239 pub green: String,
240 pub yellow: String,
241 pub blue: String,
242 pub magenta: String,
243 pub cyan: String,
244 pub white: String,
245}
246
247impl Ansi {
248 fn to_map(&self) -> BTreeMap<&'static str, String> {
250 self.into_iter().map(|(k, v)| (k, v.to_string())).collect()
251 }
252}
253
254impl<'a> IntoIterator for &'a Ansi {
255 type Item = (&'static str, &'a str);
256 type IntoIter = std::array::IntoIter<Self::Item, 8>;
257
258 fn into_iter(self) -> Self::IntoIter {
259 [
260 ("black", self.black.as_str()),
261 ("red", self.red.as_str()),
262 ("green", self.green.as_str()),
263 ("yellow", self.yellow.as_str()),
264 ("blue", self.blue.as_str()),
265 ("magenta", self.magenta.as_str()),
266 ("cyan", self.cyan.as_str()),
267 ("white", self.white.as_str()),
268 ]
269 .into_iter()
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280enum Section {
281 Colors,
282 Base,
283 Ansi,
284}
285
286impl Section {
287 const ALLOWED: &[&str] = &["colors", "base", "ansi"];
289
290 fn parse(s: &str) -> Result<Self, Error> {
291 match s {
292 "colors" => Ok(Self::Colors),
293 "base" => Ok(Self::Base),
294 "ansi" => Ok(Self::Ansi),
295 _ => Err(Error::InvalidColorExpr(format!(
296 "'{s}' cannot be referenced (allowed: {})",
297 Self::ALLOWED.join(", ")
298 ))),
299 }
300 }
301
302 const fn as_str(&self) -> &'static str {
303 match self {
304 Self::Colors => "colors",
305 Self::Base => "base",
306 Self::Ansi => "ansi",
307 }
308 }
309}
310
311#[derive(Debug, Clone, Deserialize)]
322#[serde(try_from = "String")]
323enum ColorExpr {
324 Literal(String),
326 Ref { section: Section, key: String },
328 Lighten(Box<ColorExpr>, f64),
330 Darken(Box<ColorExpr>, f64),
332 Brighten(Box<ColorExpr>, f64),
334 Mix(Box<ColorExpr>, Box<ColorExpr>, f64),
336}
337
338impl TryFrom<String> for ColorExpr {
339 type Error = Error;
340
341 fn try_from(s: String) -> Result<Self, Self::Error> {
342 parse_color_expr(&s)
343 }
344}
345
346fn strip_fn_call<'a>(s: &'a str, name: &str) -> Option<&'a str> {
348 s.strip_prefix(name)
349 .and_then(|r| r.strip_prefix('('))
350 .and_then(|r| r.strip_suffix(')'))
351}
352
353fn parse_color_expr(s: &str) -> Result<ColorExpr, Error> {
355 let s = s.trim();
356
357 if s.starts_with('#') {
359 return Ok(ColorExpr::Literal(s.to_string()));
360 }
361
362 if let Some(args) = strip_fn_call(s, "lighten") {
364 let (inner, factor) = parse_unary_fn_args(args)?;
365 return Ok(ColorExpr::Lighten(Box::new(inner), factor));
366 }
367 if let Some(args) = strip_fn_call(s, "darken") {
368 let (inner, factor) = parse_unary_fn_args(args)?;
369 return Ok(ColorExpr::Darken(Box::new(inner), factor));
370 }
371 if let Some(args) = strip_fn_call(s, "brighten") {
372 let (inner, amount) = parse_unary_fn_args(args)?;
373 return Ok(ColorExpr::Brighten(Box::new(inner), amount));
374 }
375 if let Some(args) = strip_fn_call(s, "mix") {
376 let (color1, color2, factor) = parse_mix_args(args)?;
377 return Ok(ColorExpr::Mix(Box::new(color1), Box::new(color2), factor));
378 }
379
380 let (section_str, key) = s
382 .split_once('.')
383 .ok_or_else(|| Error::InvalidColorExpr(s.to_string()))?;
384 let section = Section::parse(section_str)?;
385 Ok(ColorExpr::Ref {
386 section,
387 key: key.to_string(),
388 })
389}
390
391fn parse_unary_fn_args(args: &str) -> Result<(ColorExpr, f64), Error> {
393 let (color_str, factor_str) = args
394 .rsplit_once(',')
395 .ok_or_else(|| Error::InvalidColorExpr(format!("expected 'color, factor': {args}")))?;
396 let inner = parse_color_expr(color_str.trim())?;
397 let factor = factor_str
398 .trim()
399 .parse::<f64>()
400 .map_err(|_| Error::InvalidColorExpr(format!("invalid factor: {}", factor_str.trim())))?;
401 Ok((inner, factor))
402}
403
404fn parse_mix_args(args: &str) -> Result<(ColorExpr, ColorExpr, f64), Error> {
406 let (rest, factor_str) = args.rsplit_once(',').ok_or_else(|| {
408 Error::InvalidColorExpr(format!("expected 'color1, color2, factor': {args}"))
409 })?;
410 let factor = factor_str
411 .trim()
412 .parse::<f64>()
413 .map_err(|_| Error::InvalidColorExpr(format!("invalid factor: {}", factor_str.trim())))?;
414
415 let (color1_str, color2_str) = rest.rsplit_once(',').ok_or_else(|| {
417 Error::InvalidColorExpr(format!("expected 'color1, color2, factor': {args}"))
418 })?;
419 let color1 = parse_color_expr(color1_str.trim())?;
420 let color2 = parse_color_expr(color2_str.trim())?;
421
422 Ok((color1, color2, factor))
423}
424
425trait ResolveRef {
427 fn resolve_ref(&self, section: Section, key: &str) -> Result<String, Error>;
428}
429
430fn resolve_expr(resolver: &impl ResolveRef, expr: &ColorExpr) -> Result<String, Error> {
432 match expr {
433 ColorExpr::Literal(hex) => Ok(hex.clone()),
434 ColorExpr::Ref { section, key } => resolver.resolve_ref(*section, key),
435 ColorExpr::Lighten(inner, factor) => {
436 let hex = resolve_expr(resolver, inner)?;
437 Ok(hex.parse::<Rgb>()?.lighten(*factor).to_string())
438 }
439 ColorExpr::Darken(inner, factor) => {
440 let hex = resolve_expr(resolver, inner)?;
441 Ok(hex.parse::<Rgb>()?.darken(*factor).to_string())
442 }
443 ColorExpr::Brighten(inner, amount) => {
444 let hex = resolve_expr(resolver, inner)?;
445 Ok(hex.parse::<Rgb>()?.brighten(*amount).to_string())
446 }
447 ColorExpr::Mix(color1, color2, factor) => {
448 let rgb1: Rgb = resolve_expr(resolver, color1)?.parse()?;
449 let rgb2: Rgb = resolve_expr(resolver, color2)?.parse()?;
450 Ok(rgb1.mix(rgb2, *factor).to_string())
451 }
452 }
453}
454
455struct Resolver<'a> {
456 colors: BTreeMap<&'a str, &'a str>,
457 base: BTreeMap<&'a str, &'a str>,
458 ansi_map: BTreeMap<String, String>,
460 resolved_ansi: Ansi,
462 resolved_ansi_bright: Ansi,
464}
465
466impl<'a> Resolver<'a> {
467 fn new(raw: &'a RawPalette) -> Result<Self, Error> {
468 let colors: BTreeMap<&str, &str> = [
470 ("lantern.ember", raw.colors.lantern.ember.as_str()),
471 ("lantern.near", raw.colors.lantern.near.as_str()),
472 ("lantern.mid", raw.colors.lantern.mid.as_str()),
473 ("lantern.far", raw.colors.lantern.far.as_str()),
474 ("life", raw.colors.life.as_str()),
475 ("night", raw.colors.night.as_str()),
476 ("rain", raw.colors.rain.as_str()),
477 ("muted", raw.colors.muted.as_str()),
478 ]
479 .into_iter()
480 .collect();
481 let base: BTreeMap<&str, &str> = [
482 ("background", raw.base.background.as_str()),
483 ("foreground", raw.base.foreground.as_str()),
484 ]
485 .into_iter()
486 .collect();
487
488 let partial = PartialResolver {
490 colors: &colors,
491 base: &base,
492 ansi: None,
493 };
494 let resolved_ansi = raw.ansi.base.resolve(&partial)?;
495
496 let mut ansi_map: BTreeMap<String, String> = resolved_ansi
498 .to_map()
499 .into_iter()
500 .map(|(k, v)| (k.to_string(), v))
501 .collect();
502
503 let partial_with_ansi = PartialResolver {
505 colors: &colors,
506 base: &base,
507 ansi: Some(&ansi_map),
508 };
509 let resolved_ansi_bright = raw.ansi.bright.resolve(&partial_with_ansi)?;
510
511 for (k, v) in resolved_ansi_bright.to_map() {
513 ansi_map.insert(format!("bright.{k}"), v);
514 }
515
516 Ok(Self {
517 colors,
518 base,
519 ansi_map,
520 resolved_ansi,
521 resolved_ansi_bright,
522 })
523 }
524}
525
526impl ResolveRef for Resolver<'_> {
527 fn resolve_ref(&self, section: Section, key: &str) -> Result<String, Error> {
528 let ref_str = || format!("{}.{key}", section.as_str());
529 match section {
530 Section::Colors => self
531 .colors
532 .get(key)
533 .copied()
534 .map(str::to_string)
535 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
536 Section::Base => self
537 .base
538 .get(key)
539 .copied()
540 .map(str::to_string)
541 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
542 Section::Ansi => self
543 .ansi_map
544 .get(key)
545 .cloned()
546 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
547 }
548 }
549}
550
551struct PartialResolver<'a> {
554 colors: &'a BTreeMap<&'a str, &'a str>,
555 base: &'a BTreeMap<&'a str, &'a str>,
556 ansi: Option<&'a BTreeMap<String, String>>,
557}
558
559impl ResolveRef for PartialResolver<'_> {
560 fn resolve_ref(&self, section: Section, key: &str) -> Result<String, Error> {
561 let ref_str = || format!("{}.{key}", section.as_str());
562 match section {
563 Section::Colors => self
564 .colors
565 .get(key)
566 .copied()
567 .map(str::to_string)
568 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
569 Section::Base => self
570 .base
571 .get(key)
572 .copied()
573 .map(str::to_string)
574 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
575 Section::Ansi => self
576 .ansi
577 .and_then(|m| m.get(key).cloned())
578 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
579 }
580 }
581}
582
583impl RawPalette {
584 fn resolve(&self, variant: Variant) -> Result<Palette, Error> {
585 let resolver = Resolver::new(self)?;
586 Ok(Palette {
587 variant,
588 name: self.name.clone(),
589 description: self.description.clone(),
590 colors: self.colors.clone(),
591 base: self.base.clone(),
592 layers: self.layers.resolve(&resolver)?,
593 state: self.state.resolve(&resolver)?,
594 semantic: self.semantic.resolve(&resolver)?,
595 ansi: resolver.resolved_ansi,
596 ansi_bright: resolver.resolved_ansi_bright,
597 })
598 }
599}
600
601#[derive(Debug, Serialize)]
602pub struct Palette {
603 pub variant: Variant,
604 pub name: String,
605 pub description: String,
606 pub colors: Colors,
607 pub base: Base,
608 pub layers: Layers,
609 pub state: State,
610 pub semantic: Semantic,
611 pub ansi: Ansi,
612 pub ansi_bright: Ansi,
613}
614
615impl Palette {
616 const NIGHT_TOML: &'static str = include_str!("../palette/akari-night.toml");
618
619 const DAWN_TOML: &'static str = include_str!("../palette/akari-dawn.toml");
621
622 #[must_use]
628 pub fn night() -> Self {
629 Self::from_str(Self::NIGHT_TOML, Variant::Night)
630 .expect("embedded Night palette should be valid")
631 }
632
633 #[must_use]
639 pub fn dawn() -> Self {
640 Self::from_str(Self::DAWN_TOML, Variant::Dawn)
641 .expect("embedded Dawn palette should be valid")
642 }
643
644 pub fn from_path(path: impl AsRef<Path>, variant: Variant) -> Result<Self, Error> {
646 let content = fs::read_to_string(path)?;
647 Self::from_str(&content, variant)
648 }
649
650 pub fn from_str(content: &str, variant: Variant) -> Result<Self, Error> {
652 let raw: RawPalette = toml::from_str(content)?;
653 raw.resolve(variant)
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660 use std::path::PathBuf;
661
662 fn palette_path() -> PathBuf {
663 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("palette/akari-night.toml")
664 }
665
666 #[test]
667 fn load_night_palette() {
668 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
669 assert_eq!(palette.name, "akari-night");
670 assert_eq!(palette.variant, Variant::Night);
671 }
672
673 #[test]
674 fn colors_are_loaded() {
675 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
676 assert_eq!(palette.colors.lantern.mid, "#E26A3B");
677 assert_eq!(palette.colors.lantern.ember, "#D65A3A");
678 assert_eq!(palette.colors.lantern.near, "#D25046");
679 assert_eq!(palette.colors.lantern.far, "#D4A05A");
680 }
681
682 #[test]
683 fn base_colors_are_loaded() {
684 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
685 assert_eq!(palette.base.background, "#25231F");
686 assert_eq!(palette.base.foreground, "#E6DED3");
687 }
688
689 #[test]
690 fn semantic_references_resolved() {
691 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
692 assert_eq!(palette.semantic.keyword, "#E26A3B");
694 assert_eq!(palette.semantic.string, "#7FAF6A");
696 }
697
698 #[test]
699 fn ansi_references_resolved() {
700 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
701 assert_eq!(palette.ansi.green, "#7FAF6A");
703 assert_eq!(palette.ansi.white, "#E6DED3");
705 }
706
707 #[test]
708 fn missing_semantic_field_fails() {
709 use std::io::Write;
710 use tempfile::NamedTempFile;
711
712 let toml = r##"
713name = "test"
714description = "test"
715
716[colors.lantern]
717ember = "#D65A3A"
718near = "#D25046"
719mid = "#E26A3B"
720far = "#D4A05A"
721
722[colors]
723life = "#7FAF6A"
724night = "#5A6F82"
725rain = "#6F8F8A"
726muted = "#7C6A8A"
727
728[base]
729background = "#171B22"
730foreground = "#E6DED3"
731
732[layers]
733base = "#171B22"
734surface = "#1E2329"
735sunken = "#13171D"
736raised = "#252B33"
737border = "#2E353E"
738inset = "#3A424D"
739
740[state]
741selection_bg = "#3A424D"
742selection_fg = "#E6DED3"
743match_bg = "#4A3A2A"
744cursor = "#E26A3B"
745cursor_text = "#171B22"
746info = "#5A6F82"
747hint = "#7C6A8A"
748warning = "#D4A05A"
749error = "#D65A3A"
750active_bg = "#2A3540"
751diff_added = "#7FAF6A"
752diff_removed = "#D65A3A"
753diff_changed = "#D4A05A"
754
755[semantic]
756comment = "#7D8797"
757string = "colors.life"
758keyword = "colors.lantern.mid"
759number = "colors.lantern.far"
760constant = "colors.lantern.far"
761type = "colors.lantern.far"
762function = "colors.lantern.mid"
763variable = "base.foreground"
764success = "colors.life"
765
766[ansi]
767black = "#171B22"
768red = "colors.lantern.near"
769green = "colors.life"
770yellow = "colors.lantern.far"
771blue = "colors.night"
772magenta = "colors.muted"
773cyan = "colors.rain"
774white = "base.foreground"
775
776[ansi.bright]
777black = "#3A424D"
778red = "colors.lantern.mid"
779green = "colors.life"
780yellow = "colors.lantern.far"
781blue = "colors.night"
782magenta = "colors.muted"
783cyan = "colors.rain"
784white = "base.foreground"
785"##;
786
787 let mut file = NamedTempFile::new().unwrap();
788 file.write_all(toml.as_bytes()).unwrap();
789
790 let result = Palette::from_path(file.path(), Variant::Night);
791 assert!(result.is_err());
792 let err = result.unwrap_err();
793 assert!(matches!(err, Error::ParsePalette(_)));
794 }
795
796 #[test]
797 fn invalid_reference_fails() {
798 use std::io::Write;
799 use tempfile::NamedTempFile;
800
801 let toml = r##"
802name = "test"
803description = "test"
804
805[colors.lantern]
806ember = "#D65A3A"
807near = "#D25046"
808mid = "#E26A3B"
809far = "#D4A05A"
810
811[colors]
812life = "#7FAF6A"
813night = "#5A6F82"
814rain = "#6F8F8A"
815muted = "#7C6A8A"
816
817[base]
818background = "#171B22"
819foreground = "#E6DED3"
820
821[layers]
822base = "#171B22"
823surface = "#1E2329"
824sunken = "#13171D"
825raised = "#252B33"
826border = "#2E353E"
827inset = "#3A424D"
828
829[state]
830selection_bg = "#3A424D"
831selection_fg = "#E6DED3"
832match_bg = "#4A3A2A"
833cursor = "#E26A3B"
834cursor_text = "#171B22"
835info = "#5A6F82"
836hint = "#7C6A8A"
837warning = "#D4A05A"
838error = "#D65A3A"
839active_bg = "#2A3540"
840diff_added = "#7FAF6A"
841diff_removed = "#D65A3A"
842diff_changed = "#D4A05A"
843diff_moved = "#5A6F82"
844conflict = "#D65A3A"
845
846[semantic]
847text = "base.foreground"
848comment = "#7D8797"
849string = "colors.nonexistent"
850keyword = "colors.lantern.mid"
851number = "colors.lantern.far"
852constant = "colors.lantern.far"
853type = "colors.lantern.far"
854function = "colors.lantern.mid"
855variable = "base.foreground"
856success = "colors.life"
857path = "ansi.green"
858macro = "ansi.bright.magenta"
859escape = "ansi.bright.magenta"
860regexp = "ansi.bright.green"
861link = "ansi.bright.blue"
862directory = "ansi.cyan"
863
864[ansi]
865black = "#171B22"
866red = "colors.lantern.near"
867green = "colors.life"
868yellow = "colors.lantern.far"
869blue = "colors.night"
870magenta = "colors.muted"
871cyan = "colors.rain"
872white = "base.foreground"
873
874[ansi.bright]
875black = "#3A424D"
876red = "colors.lantern.mid"
877green = "colors.life"
878yellow = "colors.lantern.far"
879blue = "colors.night"
880magenta = "colors.muted"
881cyan = "colors.rain"
882white = "base.foreground"
883"##;
884
885 let mut file = NamedTempFile::new().unwrap();
886 file.write_all(toml.as_bytes()).unwrap();
887
888 let result = Palette::from_path(file.path(), Variant::Night);
889 assert!(result.is_err());
890 let err = result.unwrap_err();
891 assert!(matches!(err, Error::UnresolvedRef(_)));
892 }
893
894 #[test]
895 fn parse_color_expr_literal() {
896 let expr = parse_color_expr("#E26A3B").unwrap();
897 assert!(matches!(expr, ColorExpr::Literal(s) if s == "#E26A3B"));
898 }
899
900 #[test]
901 fn parse_color_expr_reference() {
902 let expr = parse_color_expr("colors.lantern.mid").unwrap();
903 assert!(
904 matches!(expr, ColorExpr::Ref { section, key } if section == Section::Colors && key == "lantern.mid")
905 );
906 }
907
908 #[test]
909 fn parse_color_expr_lighten() {
910 let expr = parse_color_expr("lighten(colors.lantern.mid, 0.1)").unwrap();
911 match expr {
912 ColorExpr::Lighten(inner, factor) => {
913 assert!(
914 matches!(*inner, ColorExpr::Ref { section, key } if section == Section::Colors && key == "lantern.mid")
915 );
916 assert!((factor - 0.1).abs() < 0.001);
917 }
918 _ => panic!("expected Lighten"),
919 }
920 }
921
922 #[test]
923 fn parse_color_expr_darken() {
924 let expr = parse_color_expr("darken(base.background, 0.2)").unwrap();
925 match expr {
926 ColorExpr::Darken(inner, factor) => {
927 assert!(
928 matches!(*inner, ColorExpr::Ref { section, key } if section == Section::Base && key == "background")
929 );
930 assert!((factor - 0.2).abs() < 0.001);
931 }
932 _ => panic!("expected Darken"),
933 }
934 }
935
936 #[test]
937 fn parse_color_expr_nested() {
938 let expr = parse_color_expr("lighten(darken(colors.lantern.mid, 0.1), 0.2)").unwrap();
939 match expr {
940 ColorExpr::Lighten(inner, outer_factor) => {
941 assert!((outer_factor - 0.2).abs() < 0.001);
942 match *inner {
943 ColorExpr::Darken(innermost, inner_factor) => {
944 assert!(
945 matches!(*innermost, ColorExpr::Ref { section, key } if section == Section::Colors && key == "lantern.mid")
946 );
947 assert!((inner_factor - 0.1).abs() < 0.001);
948 }
949 _ => panic!("expected Darken"),
950 }
951 }
952 _ => panic!("expected Lighten"),
953 }
954 }
955
956 #[test]
957 fn parse_color_expr_mix() {
958 let expr = parse_color_expr("mix(base.background, colors.night, 0.15)").unwrap();
959 match expr {
960 ColorExpr::Mix(color1, color2, factor) => {
961 assert!(
962 matches!(*color1, ColorExpr::Ref { section, key } if section == Section::Base && key == "background")
963 );
964 assert!(
965 matches!(*color2, ColorExpr::Ref { section, key } if section == Section::Colors && key == "night")
966 );
967 assert!((factor - 0.15).abs() < 0.001);
968 }
969 _ => panic!("expected Mix"),
970 }
971 }
972
973 #[test]
974 fn parse_color_expr_rejects_non_referenceable_sections() {
975 let err = parse_color_expr("layers.base").unwrap_err();
977 assert!(
978 matches!(err, Error::InvalidColorExpr(msg) if msg.contains("cannot be referenced"))
979 );
980
981 let err = parse_color_expr("state.cursor").unwrap_err();
982 assert!(
983 matches!(err, Error::InvalidColorExpr(msg) if msg.contains("cannot be referenced"))
984 );
985
986 let err = parse_color_expr("semantic.keyword").unwrap_err();
987 assert!(
988 matches!(err, Error::InvalidColorExpr(msg) if msg.contains("cannot be referenced"))
989 );
990 }
991
992 #[test]
993 fn parse_color_expr_ansi_bright() {
994 let expr = parse_color_expr("ansi.bright.red").unwrap();
996 assert!(
997 matches!(expr, ColorExpr::Ref { section, key } if section == Section::Ansi && key == "bright.red")
998 );
999 }
1000}