svgtypes/
paint.rs

1// Copyright 2018 the SVG Types Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::str::FromStr;
5
6use crate::{Color, Error, Stream};
7
8/// Representation of the fallback part of the [`<paint>`] type.
9///
10/// Used by the [`Paint`] type.
11///
12/// [`<paint>`]: https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum PaintFallback {
15    /// The `none` value.
16    None,
17    /// The `currentColor` value.
18    CurrentColor,
19    /// [`<color>`] value.
20    ///
21    /// [`<color>`]: https://www.w3.org/TR/css-color-3/
22    Color(Color),
23}
24
25/// Representation of the [`<paint>`] type.
26///
27/// Doesn't own the data. Use only for parsing.
28///
29/// `<icccolor>` isn't supported.
30///
31/// [`<paint>`]: https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint
32///
33/// # Examples
34///
35/// ```
36/// use svgtypes::{Paint, PaintFallback, Color};
37///
38/// let paint = Paint::from_str("url(#gradient) red").unwrap();
39/// assert_eq!(paint, Paint::FuncIRI("gradient",
40///                                  Some(PaintFallback::Color(Color::red()))));
41///
42/// let paint = Paint::from_str("inherit").unwrap();
43/// assert_eq!(paint, Paint::Inherit);
44/// ```
45#[derive(Clone, Copy, PartialEq, Eq, Debug)]
46pub enum Paint<'a> {
47    /// The `none` value.
48    None,
49    /// The `inherit` value.
50    Inherit,
51    /// The `currentColor` value.
52    CurrentColor,
53    /// [`<color>`] value.
54    ///
55    /// [`<color>`]: https://www.w3.org/TR/css-color-3/
56    Color(Color),
57    /// [`<FuncIRI>`] value with an optional fallback.
58    ///
59    /// [`<FuncIRI>`]: https://www.w3.org/TR/SVG11/types.html#DataTypeFuncIRI
60    FuncIRI(&'a str, Option<PaintFallback>),
61    /// The `context-fill` value.
62    ContextFill,
63    /// The `context-stroke` value.
64    ContextStroke,
65}
66
67impl<'a> Paint<'a> {
68    /// Parses a `Paint` from a string.
69    ///
70    /// We can't use the `FromStr` trait because it requires
71    /// an owned value as a return type.
72    #[allow(clippy::should_implement_trait)]
73    pub fn from_str(text: &'a str) -> Result<Self, Error> {
74        let text = text.trim();
75        match text {
76            "none" => Ok(Paint::None),
77            "inherit" => Ok(Paint::Inherit),
78            "currentColor" => Ok(Paint::CurrentColor),
79            "context-fill" => Ok(Paint::ContextFill),
80            "context-stroke" => Ok(Paint::ContextStroke),
81            _ => {
82                let mut s = Stream::from(text);
83                if s.starts_with(b"url(") {
84                    match s.parse_func_iri() {
85                        Ok(link) => {
86                            s.skip_spaces();
87
88                            // get fallback
89                            if !s.at_end() {
90                                let fallback = s.slice_tail();
91                                match fallback {
92                                    "none" => Ok(Paint::FuncIRI(link, Some(PaintFallback::None))),
93                                    "currentColor" => {
94                                        Ok(Paint::FuncIRI(link, Some(PaintFallback::CurrentColor)))
95                                    }
96                                    _ => {
97                                        let color = Color::from_str(fallback)?;
98                                        Ok(Paint::FuncIRI(link, Some(PaintFallback::Color(color))))
99                                    }
100                                }
101                            } else {
102                                Ok(Paint::FuncIRI(link, None))
103                            }
104                        }
105                        Err(_) => Err(Error::InvalidValue),
106                    }
107                } else {
108                    match Color::from_str(text) {
109                        Ok(c) => Ok(Paint::Color(c)),
110                        Err(_) => Err(Error::InvalidValue),
111                    }
112                }
113            }
114        }
115    }
116}
117
118#[rustfmt::skip]
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    macro_rules! test {
124        ($name:ident, $text:expr, $result:expr) => (
125            #[test]
126            fn $name() {
127                assert_eq!(Paint::from_str($text).unwrap(), $result);
128            }
129        )
130    }
131
132    test!(parse_1, "none", Paint::None);
133    test!(parse_2, "  none   ", Paint::None);
134    test!(parse_3, " inherit ", Paint::Inherit);
135    test!(parse_4, " currentColor ", Paint::CurrentColor);
136    test!(parse_5, " red ", Paint::Color(Color::red()));
137    test!(parse_6, " url(#qwe) ", Paint::FuncIRI("qwe", None));
138    test!(parse_7, " url(#qwe) none ", Paint::FuncIRI("qwe", Some(PaintFallback::None)));
139    test!(parse_8, " url(#qwe) currentColor ", Paint::FuncIRI("qwe", Some(PaintFallback::CurrentColor)));
140    test!(parse_9, " url(#qwe) red ", Paint::FuncIRI("qwe", Some(PaintFallback::Color(Color::red()))));
141
142    macro_rules! test_err {
143        ($name:ident, $text:expr, $result:expr) => (
144            #[test]
145            fn $name() {
146                assert_eq!(Paint::from_str($text).unwrap_err().to_string(), $result);
147            }
148        )
149    }
150
151    test_err!(parse_err_1, "qwe", "invalid value");
152    test_err!(parse_err_2, "red icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)", "invalid value");
153    // TODO: this
154//    test_err!(parse_err_3, "url(#qwe) red icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)", "invalid color at 1:15");
155}