1use bitcode::{Decode, Encode};
2use serde::{Deserialize, Serialize};
3use std::{collections::HashMap, fmt::Display};
4
5use crate::{cf_types::ConditionalFormatting, expressions::token::Error};
6
7fn default_as_false() -> bool {
8 false
9}
10
11fn is_false(b: &bool) -> bool {
12 !*b
13}
14
15#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
16#[serde(untagged)]
17pub enum Color {
18 Rgb(String),
19 Theme(i32, f64),
21 #[default]
23 None,
24}
25
26fn is_valid_hex_color(color: &str) -> bool {
29 if color.chars().count() != 7 {
30 return false;
31 }
32 if !color.starts_with('#') {
33 return false;
34 }
35 if let Ok(z) = i32::from_str_radix(&color[1..], 16) {
36 if (0..=0xffffff).contains(&z) {
37 return true;
38 }
39 }
40 false
41}
42
43impl Color {
44 pub fn is_none(&self) -> bool {
45 matches!(self, Color::None)
46 }
47
48 pub fn is_some(&self) -> bool {
49 !matches!(self, Color::None)
50 }
51
52 pub fn to_rgb(&self, theme: &Theme) -> String {
55 match self {
56 Color::Rgb(s) => s.clone(),
57 Color::Theme(idx, tint) => theme.resolve(*idx, *tint),
58 Color::None => String::new(),
59 }
60 }
61
62 pub fn from_rgb(color: &str) -> Result<Self, String> {
63 if is_valid_hex_color(color) {
64 return Ok(Color::Rgb(color.to_string()));
65 }
66 Err(format!("Invalid color: '{}'.", color))
67 }
68
69 pub fn from_param(s: &str) -> Result<Self, String> {
74 if s.is_empty() {
75 return Ok(Color::None);
76 }
77 if is_valid_hex_color(s) {
78 return Ok(Color::Rgb(s.to_string()));
79 }
80 if let Some(inner) = s.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
81 let mut parts = inner.splitn(2, ',');
82 if let (Some(idx_str), Some(tint_str)) = (parts.next(), parts.next()) {
83 if let (Ok(idx), Ok(tint)) = (
84 idx_str.trim().parse::<i32>(),
85 tint_str.trim().parse::<f64>(),
86 ) {
87 return Ok(Color::Theme(idx, tint));
88 }
89 }
90 }
91 Err(format!("Invalid color: '{}'.", s))
92 }
93}
94
95#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
96pub struct Metadata {
97 pub application: String,
98 pub app_version: String,
99 pub creator: String,
100 pub last_modified_by: String,
101 pub created: String, pub last_modified: String, }
104
105#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
106pub struct WorkbookSettings {
107 pub tz: String,
108 pub locale: String,
109}
110
111#[derive(Encode, Decode, Debug, PartialEq, Clone)]
113pub struct WorkbookView {
114 pub sheet: u32,
116 pub window_width: i64,
118 pub window_height: i64,
120}
121
122#[derive(Encode, Decode, Debug, PartialEq, Clone)]
124pub struct Workbook {
125 pub shared_strings: Vec<String>,
126 pub defined_names: Vec<DefinedName>,
127 pub worksheets: Vec<Worksheet>,
128 pub styles: Styles,
129 pub name: String,
130 pub settings: WorkbookSettings,
131 pub metadata: Metadata,
132 pub tables: HashMap<String, Table>,
133 pub views: HashMap<u32, WorkbookView>,
134 pub theme: Theme,
135}
136
137#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
139pub struct DefinedName {
140 pub name: String,
141 pub formula: String,
142 pub sheet_id: Option<u32>,
143}
144
145#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
149pub enum SheetState {
150 Visible,
151 Hidden,
152 VeryHidden,
153}
154
155impl Display for SheetState {
156 fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
157 match self {
158 SheetState::Visible => write!(formatter, "visible"),
159 SheetState::Hidden => write!(formatter, "hidden"),
160 SheetState::VeryHidden => write!(formatter, "veryHidden"),
161 }
162 }
163}
164
165#[derive(Encode, Decode, Debug, PartialEq, Clone)]
169pub struct WorksheetView {
170 pub row: i32,
172 pub column: i32,
174 pub range: [i32; 4],
176 pub top_row: i32,
178 pub left_column: i32,
180}
181
182#[derive(Encode, Decode, Debug, PartialEq, Clone)]
184pub struct Worksheet {
185 pub dimension: String,
186 pub cols: Vec<Col>,
187 pub rows: Vec<Row>,
188 pub name: String,
189 pub sheet_data: SheetData,
190 pub shared_formulas: Vec<String>,
191 pub sheet_id: u32,
192 pub state: SheetState,
193 pub color: Color,
194 pub merge_cells: Vec<String>,
195 pub comments: Vec<Comment>,
196 pub frozen_rows: i32,
197 pub frozen_columns: i32,
198 pub views: HashMap<u32, WorksheetView>,
199 pub show_grid_lines: bool,
201 pub conditional_formatting: Vec<ConditionalFormatting>,
202}
203
204pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
207
208#[derive(Encode, Decode, Debug, PartialEq, Clone)]
210pub struct Row {
211 pub r: i32,
213 pub height: f64,
214 pub custom_format: bool,
215 pub custom_height: bool,
216 pub s: i32,
217 pub hidden: bool,
218}
219
220#[derive(Encode, Decode, Debug, PartialEq, Clone)]
222pub struct Col {
223 pub min: i32,
226 pub max: i32,
228 pub width: f64,
229 pub custom_width: bool,
230 pub hidden: bool,
231 pub style: Option<i32>,
232}
233
234#[derive(Debug, Eq, PartialEq)]
236pub enum CellType {
237 Number = 1,
238 Text = 2,
239 LogicalValue = 4,
240 ErrorValue = 16,
241 Array = 64,
242 CompoundData = 128,
243}
244
245#[derive(Encode, Decode, Debug, Clone, PartialEq)]
248pub enum FormulaValue {
249 Unevaluated,
250 Boolean(bool),
251 Number(f64),
252 Text(String),
253 Error {
254 ei: Error,
255 o: String,
257 m: String,
259 },
260}
261
262#[derive(Encode, Decode, Debug, Clone, PartialEq)]
264pub enum SpillValue {
265 Boolean(bool),
266 Number(f64),
267 Text(String),
268 Error(Error),
269}
270
271#[derive(Encode, Decode, Debug, Clone, PartialEq)]
273pub enum ArrayKind {
274 Cse,
276 Dynamic,
278}
279
280#[derive(Encode, Decode, Debug, Clone, PartialEq)]
290pub enum Cell {
291 EmptyCell {
292 s: i32,
293 },
294 BooleanCell {
295 v: bool,
296 s: i32,
297 },
298 NumberCell {
299 v: f64,
300 s: i32,
301 },
302 ErrorCell {
304 ei: Error,
305 s: i32,
306 },
307 SharedString {
309 si: i32,
310 s: i32,
311 },
312 CellFormula {
315 f: i32,
316 s: i32,
317 v: FormulaValue,
318 },
319 ArrayFormula {
323 f: i32,
324 s: i32,
325 r: (i32, i32),
326 kind: ArrayKind,
327 v: FormulaValue,
328 },
329 SpillCell {
331 s: i32,
332 a: (i32, i32),
333 v: SpillValue,
334 },
335}
336
337impl Default for Cell {
338 fn default() -> Self {
339 Cell::EmptyCell { s: 0 }
340 }
341}
342
343#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
344pub struct Comment {
345 pub text: String,
346 pub author_name: String,
347 pub author_id: Option<String>,
348 pub cell_ref: String,
349}
350
351#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
353pub struct Table {
354 pub name: String,
355 pub display_name: String,
356 pub sheet_name: String,
357 pub reference: String,
358 pub totals_row_count: u32,
359 pub header_row_count: u32,
360 pub header_row_dxf_id: Option<u32>,
361 pub data_dxf_id: Option<u32>,
362 pub totals_row_dxf_id: Option<u32>,
363 pub columns: Vec<TableColumn>,
364 pub style_info: TableStyleInfo,
365 pub has_filters: bool,
366}
367
368#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
371pub struct TableColumn {
372 pub id: u32,
373 pub name: String,
374 pub totals_row_label: Option<String>,
375 pub header_row_dxf_id: Option<u32>,
376 pub data_dxf_id: Option<u32>,
377 pub totals_row_dxf_id: Option<u32>,
378 pub totals_row_function: Option<String>,
379}
380
381impl Default for TableColumn {
382 fn default() -> Self {
383 TableColumn {
384 id: 0,
385 name: "Column".to_string(),
386 totals_row_label: None,
387 totals_row_function: None,
388 data_dxf_id: None,
389 header_row_dxf_id: None,
390 totals_row_dxf_id: None,
391 }
392 }
393}
394
395#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
396pub struct TableStyleInfo {
397 pub name: Option<String>,
398 pub show_first_column: bool,
399 pub show_last_column: bool,
400 pub show_row_stripes: bool,
401 pub show_column_stripes: bool,
402}
403
404#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
405pub struct DxfFont {
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub strike: Option<bool>,
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub u: Option<bool>,
410 #[serde(skip_serializing_if = "Option::is_none")]
411 pub b: Option<bool>,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 pub i: Option<bool>,
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub sz: Option<i32>,
416 #[serde(skip_serializing_if = "Color::is_none")]
417 #[serde(default)]
418 pub color: Color,
419}
420
421#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
426pub struct Dxf {
427 pub font: Option<DxfFont>,
428 pub fill: Option<Fill>,
429 pub border: Option<Border>,
430 pub num_fmt: Option<NumFmt>,
431 pub alignment: Option<Alignment>,
432}
433
434#[derive(Encode, Decode, Debug, PartialEq, Clone)]
435pub struct Styles {
436 pub num_fmts: Vec<NumFmt>,
437 pub fonts: Vec<Font>,
438 pub fills: Vec<Fill>,
439 pub borders: Vec<Border>,
440 pub cell_style_xfs: Vec<CellStyleXfs>,
441 pub cell_xfs: Vec<CellXfs>,
442 pub cell_styles: Vec<CellStyles>,
443 pub dxfs: Vec<Dxf>,
444}
445
446impl Default for Styles {
447 fn default() -> Self {
448 Styles {
449 num_fmts: vec![],
450 fonts: vec![Default::default()],
451 fills: vec![Default::default(), Default::default()],
452 borders: vec![Default::default()],
453 cell_style_xfs: vec![Default::default()],
454 cell_xfs: vec![Default::default()],
455 cell_styles: vec![Default::default()],
456 dxfs: vec![],
457 }
458 }
459}
460
461#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
462pub struct Style {
463 #[serde(skip_serializing_if = "Option::is_none")]
464 pub alignment: Option<Alignment>,
465 pub num_fmt: String,
466 pub fill: Fill,
467 pub font: Font,
468 pub border: Border,
469 pub quote_prefix: bool,
470}
471
472impl Default for Style {
473 fn default() -> Self {
474 Style {
475 alignment: None,
476 num_fmt: "general".to_string(),
477 fill: Fill::default(),
478 font: Font::default(),
479 border: Border::default(),
480 quote_prefix: false,
481 }
482 }
483}
484
485#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
486pub struct NumFmt {
487 pub num_fmt_id: i32,
488 pub format_code: String,
489}
490
491impl Default for NumFmt {
492 fn default() -> Self {
493 NumFmt {
494 num_fmt_id: 0,
495 format_code: "general".to_string(),
496 }
497 }
498}
499
500#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
504#[serde(rename_all = "lowercase")]
505#[derive(Default)]
506pub enum FontScheme {
507 #[default]
508 Minor,
509 Major,
510 None,
511}
512
513impl Display for FontScheme {
514 fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
515 match self {
516 FontScheme::Minor => write!(formatter, "minor"),
517 FontScheme::Major => write!(formatter, "major"),
518 FontScheme::None => write!(formatter, "none"),
519 }
520 }
521}
522
523#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
524pub struct Font {
525 #[serde(default = "default_as_false")]
526 #[serde(skip_serializing_if = "is_false")]
527 pub strike: bool,
528 #[serde(default = "default_as_false")]
529 #[serde(skip_serializing_if = "is_false")]
530 pub u: bool, #[serde(default = "default_as_false")]
532 #[serde(skip_serializing_if = "is_false")]
533 pub b: bool,
534 #[serde(default = "default_as_false")]
535 #[serde(skip_serializing_if = "is_false")]
536 pub i: bool,
537 pub sz: i32,
538 #[serde(skip_serializing_if = "Color::is_none")]
539 #[serde(default)]
540 pub color: Color,
541 pub name: String,
542 pub family: i32,
548 pub scheme: FontScheme,
549}
550
551impl Default for Font {
552 fn default() -> Self {
553 Font {
554 strike: false,
555 u: false,
556 b: false,
557 i: false,
558 sz: 12,
559 color: Color::None,
560 name: "Inter".to_string(),
561 family: 2,
562 scheme: FontScheme::Minor,
563 }
564 }
565}
566
567#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
568pub struct Fill {
569 #[serde(skip_serializing_if = "Color::is_none")]
570 #[serde(default)]
571 pub color: Color,
572}
573
574#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
575#[serde(rename_all = "lowercase")]
576#[derive(Default)]
577pub enum HorizontalAlignment {
578 Center,
579 CenterContinuous,
580 Distributed,
581 Fill,
582 #[default]
583 General,
584 Justify,
585 Left,
586 Right,
587}
588
589impl HorizontalAlignment {
592 fn is_default(&self) -> bool {
593 self == &HorizontalAlignment::default()
594 }
595}
596
597impl Display for HorizontalAlignment {
599 fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
600 match self {
601 HorizontalAlignment::Center => write!(formatter, "center"),
602 HorizontalAlignment::CenterContinuous => write!(formatter, "centerContinuous"),
603 HorizontalAlignment::Distributed => write!(formatter, "distributed"),
604 HorizontalAlignment::Fill => write!(formatter, "fill"),
605 HorizontalAlignment::General => write!(formatter, "general"),
606 HorizontalAlignment::Justify => write!(formatter, "justify"),
607 HorizontalAlignment::Left => write!(formatter, "left"),
608 HorizontalAlignment::Right => write!(formatter, "right"),
609 }
610 }
611}
612
613#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
614#[serde(rename_all = "lowercase")]
615#[derive(Default)]
616pub enum VerticalAlignment {
617 #[default]
618 Bottom,
619 Center,
620 Distributed,
621 Justify,
622 Top,
623}
624
625impl VerticalAlignment {
626 fn is_default(&self) -> bool {
627 self == &VerticalAlignment::default()
628 }
629}
630
631impl Display for VerticalAlignment {
632 fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
633 match self {
634 VerticalAlignment::Bottom => write!(formatter, "bottom"),
635 VerticalAlignment::Center => write!(formatter, "center"),
636 VerticalAlignment::Distributed => write!(formatter, "distributed"),
637 VerticalAlignment::Justify => write!(formatter, "justify"),
638 VerticalAlignment::Top => write!(formatter, "top"),
639 }
640 }
641}
642
643#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
645pub struct Alignment {
646 #[serde(default)]
647 #[serde(skip_serializing_if = "HorizontalAlignment::is_default")]
648 pub horizontal: HorizontalAlignment,
649 #[serde(skip_serializing_if = "VerticalAlignment::is_default")]
650 #[serde(default)]
651 pub vertical: VerticalAlignment,
652 #[serde(default = "default_as_false")]
653 #[serde(skip_serializing_if = "is_false")]
654 pub wrap_text: bool,
655}
656
657#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
658pub struct CellStyleXfs {
659 pub num_fmt_id: i32,
660 pub font_id: i32,
661 pub fill_id: i32,
662 pub border_id: i32,
663 pub apply_number_format: bool,
664 pub apply_border: bool,
665 pub apply_alignment: bool,
666 pub apply_protection: bool,
667 pub apply_font: bool,
668 pub apply_fill: bool,
669}
670
671impl Default for CellStyleXfs {
672 fn default() -> Self {
673 CellStyleXfs {
674 num_fmt_id: 0,
675 font_id: 0,
676 fill_id: 0,
677 border_id: 0,
678 apply_number_format: true,
679 apply_border: true,
680 apply_alignment: true,
681 apply_protection: true,
682 apply_font: true,
683 apply_fill: true,
684 }
685 }
686}
687
688#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Copy)]
694#[serde(default)]
695pub struct StyleIncludes {
696 pub number_format: bool,
697 pub font: bool,
698 pub fill: bool,
699 pub border: bool,
700 pub alignment: bool,
701 pub protection: bool,
702}
703
704impl Default for StyleIncludes {
705 fn default() -> Self {
706 StyleIncludes {
707 number_format: true,
708 font: true,
709 fill: true,
710 border: true,
711 alignment: true,
712 protection: true,
713 }
714 }
715}
716
717#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
718pub struct CellXfs {
719 pub xf_id: i32,
720 pub num_fmt_id: i32,
721 pub font_id: i32,
722 pub fill_id: i32,
723 pub border_id: i32,
724 pub apply_number_format: bool,
725 pub apply_border: bool,
726 pub apply_alignment: bool,
727 pub apply_protection: bool,
728 pub apply_font: bool,
729 pub apply_fill: bool,
730 pub quote_prefix: bool,
731 pub alignment: Option<Alignment>,
732}
733
734#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
735pub struct CellStyles {
736 pub name: String,
737 pub xf_id: i32,
738 pub builtin_id: i32,
739}
740
741impl Default for CellStyles {
742 fn default() -> Self {
743 CellStyles {
744 name: "normal".to_string(),
745 xf_id: 0,
746 builtin_id: 0,
747 }
748 }
749}
750
751#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Clone)]
752#[serde(rename_all = "lowercase")]
753pub enum BorderStyle {
754 Thin,
755 Medium,
756 Thick,
757 Double,
758 Dotted,
759 SlantDashDot,
760 MediumDashed,
761 MediumDashDotDot,
762 MediumDashDot,
763}
764
765impl Display for BorderStyle {
766 fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
767 match self {
768 BorderStyle::Thin => write!(formatter, "thin"),
769 BorderStyle::Thick => write!(formatter, "thick"),
770 BorderStyle::SlantDashDot => write!(formatter, "slantdashdot"),
771 BorderStyle::MediumDashed => write!(formatter, "mediumdashed"),
772 BorderStyle::MediumDashDotDot => write!(formatter, "mediumdashdotdot"),
773 BorderStyle::MediumDashDot => write!(formatter, "mediumdashdot"),
774 BorderStyle::Medium => write!(formatter, "medium"),
775 BorderStyle::Double => write!(formatter, "double"),
776 BorderStyle::Dotted => write!(formatter, "dotted"),
777 }
778 }
779}
780
781#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
782pub struct BorderItem {
783 pub style: BorderStyle,
784 #[serde(skip_serializing_if = "Color::is_none")]
785 #[serde(default)]
786 pub color: Color,
787}
788
789#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone, Default)]
790pub struct Border {
791 #[serde(default = "default_as_false")]
792 #[serde(skip_serializing_if = "is_false")]
793 pub diagonal_up: bool,
794 #[serde(default = "default_as_false")]
795 #[serde(skip_serializing_if = "is_false")]
796 pub diagonal_down: bool,
797 #[serde(skip_serializing_if = "Option::is_none")]
798 pub left: Option<BorderItem>,
799 #[serde(skip_serializing_if = "Option::is_none")]
800 pub right: Option<BorderItem>,
801 #[serde(skip_serializing_if = "Option::is_none")]
802 pub top: Option<BorderItem>,
803 #[serde(skip_serializing_if = "Option::is_none")]
804 pub bottom: Option<BorderItem>,
805 #[serde(skip_serializing_if = "Option::is_none")]
806 pub diagonal: Option<BorderItem>,
807}
808
809#[derive(Serialize, Deserialize, Debug, PartialEq)]
812pub struct SheetProperties {
813 pub name: String,
814 pub state: String,
815 pub sheet_id: u32,
816 #[serde(skip_serializing_if = "Color::is_none")]
817 #[serde(default)]
818 pub color: Color,
819}
820
821#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
822pub struct Theme {
823 pub name: String,
824 pub dk1: String,
825 pub lt1: String,
826 pub dk2: String,
827 pub lt2: String,
828 pub accent1: String,
829 pub accent2: String,
830 pub accent3: String,
831 pub accent4: String,
832 pub accent5: String,
833 pub accent6: String,
834 pub hlink: String,
835 pub fol_hlink: String,
836}
837
838impl Default for Theme {
839 fn default() -> Self {
840 Theme {
841 name: "Office".to_string(),
842 dk1: "#000000".to_string(),
843 lt1: "#FFFFFF".to_string(),
844 dk2: "#44546A".to_string(),
845 lt2: "#E7E6E6".to_string(),
846 accent1: "#4472C4".to_string(),
847 accent2: "#ED7D31".to_string(),
848 accent3: "#A5A5A5".to_string(),
849 accent4: "#FFC000".to_string(),
850 accent5: "#5B9BD5".to_string(),
851 accent6: "#70AD47".to_string(),
852 hlink: "#0563C1".to_string(),
853 fol_hlink: "#954F72".to_string(),
854 }
855 }
856}
857
858impl Theme {
859 pub fn resolve(&self, theme_index: i32, tint: f64) -> String {
862 use crate::colors::hex_with_tint_to_rgb;
863 let color = match theme_index {
864 0 => &self.lt1,
865 1 => &self.dk1,
866 2 => &self.lt2,
867 3 => &self.dk2,
868 4 => &self.accent1,
869 5 => &self.accent2,
870 6 => &self.accent3,
871 7 => &self.accent4,
872 8 => &self.accent5,
873 9 => &self.accent6,
874 10 => &self.hlink,
875 11 => &self.fol_hlink,
876 _ => &self.dk1,
877 };
878 hex_with_tint_to_rgb(color, tint)
879 }
880}
881
882#[cfg(test)]
883mod test {
884 use super::*;
885 #[test]
886 fn test_is_valid_hex_color() {
887 assert!(is_valid_hex_color("#000000"));
888 assert!(is_valid_hex_color("#ffffff"));
889
890 assert!(!is_valid_hex_color("000000"));
891 assert!(!is_valid_hex_color("ffffff"));
892
893 assert!(!is_valid_hex_color("#gggggg"));
894
895 assert!(!is_valid_hex_color("#ffffff "));
897 assert!(!is_valid_hex_color("#fff")); assert!(!is_valid_hex_color("#ffffff00")); }
900}