style/servo/
url.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Common handling for the specified value CSS url() values.
6
7use crate::derives::*;
8use crate::parser::{Parse, ParserContext};
9use crate::stylesheets::CorsMode;
10use crate::values::computed::{Context, ToComputedValue};
11use cssparser::Parser;
12use servo_arc::Arc;
13use std::fmt::{self, Write};
14use std::ops::Deref;
15use style_traits::{CssWriter, ParseError, ToCss};
16use to_shmem::{SharedMemoryBuilder, ToShmem};
17use url::Url;
18
19/// A CSS url() value for servo.
20///
21/// Servo eagerly resolves SpecifiedUrls, which it can then take advantage of
22/// when computing values. In contrast, Gecko uses a different URL backend, so
23/// eagerly resolving with rust-url would be duplicated work.
24///
25/// However, this approach is still not necessarily optimal: See
26/// <https://bugzilla.mozilla.org/show_bug.cgi?id=1347435#c6>
27#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize, SpecifiedValueInfo)]
28#[css(function = "url")]
29#[repr(C)]
30pub struct CssUrl(#[ignore_malloc_size_of = "Arc"] pub Arc<CssUrlData>);
31
32/// Data shared between CssUrls.
33///
34#[derive(Debug, Deserialize, MallocSizeOf, Serialize, SpecifiedValueInfo)]
35#[repr(C)]
36pub struct CssUrlData {
37    /// The original URI. This might be optional since we may insert computed
38    /// values of images into the cascade directly, and we don't bother to
39    /// convert their serialization.
40    ///
41    /// Refcounted since cloning this should be cheap and data: uris can be
42    /// really large.
43    #[ignore_malloc_size_of = "Arc"]
44    original: Option<Arc<String>>,
45
46    /// The resolved value for the url, if valid.
47    #[ignore_malloc_size_of = "Arc"]
48    resolved: Option<Arc<Url>>,
49}
50
51impl ToShmem for CssUrl {
52    fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
53        unimplemented!("If servo wants to share stylesheets across processes, ToShmem for Url must be implemented");
54    }
55}
56
57impl Deref for CssUrl {
58    type Target = CssUrlData;
59    fn deref(&self) -> &Self::Target {
60        &self.0
61    }
62}
63
64impl CssUrl {
65    /// Try to parse a URL from a string value that is a valid CSS token for a
66    /// URL.
67    ///
68    /// FIXME(emilio): Should honor CorsMode.
69    pub fn parse_from_string(url: String, context: &ParserContext, _: CorsMode) -> Self {
70        let serialization = Arc::new(url);
71        // Per https://drafts.csswg.org/css-values-4/#url-empty
72        // If the original url is empty, then the resolved url is considered invalid.
73        let resolved = (!serialization.is_empty())
74            .then(|| context.url_data.0.join(&serialization))
75            .and_then(Result::ok)
76            .map(Arc::new);
77        CssUrl(Arc::new(CssUrlData {
78            original: Some(serialization),
79            resolved: resolved,
80        }))
81    }
82
83    /// Returns true if the URL is definitely invalid. For Servo URLs, we can
84    /// use its |resolved| status.
85    pub fn is_invalid(&self) -> bool {
86        self.resolved.is_none()
87    }
88
89    /// Returns true if this URL looks like a fragment.
90    /// See https://drafts.csswg.org/css-values/#local-urls
91    ///
92    /// Since Servo currently stores resolved URLs, this is hard to implement. We
93    /// either need to change servo to lazily resolve (like Gecko), or note this
94    /// information in the tokenizer.
95    pub fn is_fragment(&self) -> bool {
96        error!("Can't determine whether the url is a fragment.");
97        false
98    }
99
100    /// Returns the resolved url if it was valid.
101    pub fn url(&self) -> Option<&Arc<Url>> {
102        self.resolved.as_ref()
103    }
104
105    /// Return the resolved url as string, or the empty string if it's invalid.
106    ///
107    /// TODO(emilio): Should we return the original one if needed?
108    pub fn as_str(&self) -> &str {
109        match self.resolved {
110            Some(ref url) => url.as_str(),
111            None => "",
112        }
113    }
114
115    /// Creates an already specified url value from an already resolved URL
116    /// for insertion in the cascade.
117    pub fn for_cascade(url: Arc<::url::Url>) -> Self {
118        CssUrl(Arc::new(CssUrlData {
119            original: None,
120            resolved: Some(url),
121        }))
122    }
123
124    /// Gets a new url from a string for unit tests.
125    pub fn new_for_testing(url: &str) -> Self {
126        CssUrl(Arc::new(CssUrlData {
127            original: Some(Arc::new(url.into())),
128            resolved: (!url.is_empty())
129                .then(|| ::url::Url::parse(url))
130                .and_then(Result::ok)
131                .map(Arc::new),
132        }))
133    }
134
135    /// Parses a URL request and records that the corresponding request needs to
136    /// be CORS-enabled.
137    ///
138    /// This is only for shape images and masks in Gecko, thus unimplemented for
139    /// now so somebody notices when trying to do so.
140    pub fn parse_with_cors_mode<'i, 't>(
141        context: &ParserContext,
142        input: &mut Parser<'i, 't>,
143        cors_mode: CorsMode,
144    ) -> Result<Self, ParseError<'i>> {
145        let url = input.expect_url()?;
146        Ok(Self::parse_from_string(
147            url.as_ref().to_owned(),
148            context,
149            cors_mode,
150        ))
151    }
152}
153
154impl Parse for CssUrl {
155    fn parse<'i, 't>(
156        context: &ParserContext,
157        input: &mut Parser<'i, 't>,
158    ) -> Result<Self, ParseError<'i>> {
159        Self::parse_with_cors_mode(context, input, CorsMode::None)
160    }
161}
162
163impl PartialEq for CssUrl {
164    fn eq(&self, other: &Self) -> bool {
165        // TODO(emilio): maybe we care about equality of the specified values if
166        // present? Seems not.
167        self.resolved == other.resolved
168    }
169}
170
171impl Eq for CssUrl {}
172
173impl ToCss for CssUrl {
174    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
175    where
176        W: Write,
177    {
178        let string = match self.0.original {
179            Some(ref original) => &**original,
180            None => match self.resolved {
181                Some(ref url) => url.as_str(),
182                // This can only happen if the url wasn't specified by the
183                // user *and* it's an invalid url that has been transformed
184                // back to specified value via the "uncompute" functionality.
185                None => "about:invalid",
186            },
187        };
188
189        dest.write_str("url(")?;
190        string.to_css(dest)?;
191        dest.write_char(')')
192    }
193}
194
195/// A specified url() value for servo.
196pub type SpecifiedUrl = CssUrl;
197
198impl ToComputedValue for SpecifiedUrl {
199    type ComputedValue = ComputedUrl;
200
201    // If we can't resolve the URL from the specified one, we fall back to the original
202    // but still return it as a ComputedUrl::Invalid
203    fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
204        match self.resolved {
205            Some(ref url) => ComputedUrl::Valid(url.clone()),
206            None => match self.original {
207                Some(ref url) => ComputedUrl::Invalid(url.clone()),
208                None => {
209                    unreachable!("Found specified url with neither resolved or original URI!");
210                },
211            },
212        }
213    }
214
215    fn from_computed_value(computed: &ComputedUrl) -> Self {
216        let data = match *computed {
217            ComputedUrl::Valid(ref url) => CssUrlData {
218                original: None,
219                resolved: Some(url.clone()),
220            },
221            ComputedUrl::Invalid(ref url) => CssUrlData {
222                original: Some(url.clone()),
223                resolved: None,
224            },
225        };
226        CssUrl(Arc::new(data))
227    }
228}
229
230/// The computed value of a CSS `url()`, resolved relative to the stylesheet URL.
231#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
232pub enum ComputedUrl {
233    /// The `url()` was invalid or it wasn't specified by the user.
234    Invalid(#[ignore_malloc_size_of = "Arc"] Arc<String>),
235    /// The resolved `url()` relative to the stylesheet URL.
236    Valid(#[ignore_malloc_size_of = "Arc"] Arc<Url>),
237}
238
239impl ComputedUrl {
240    /// Returns the resolved url if it was valid.
241    pub fn url(&self) -> Option<&Arc<Url>> {
242        match *self {
243            ComputedUrl::Valid(ref url) => Some(url),
244            _ => None,
245        }
246    }
247}
248
249impl ToCss for ComputedUrl {
250    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
251    where
252        W: Write,
253    {
254        let string = match *self {
255            ComputedUrl::Valid(ref url) => url.as_str(),
256            ComputedUrl::Invalid(ref invalid_string) => invalid_string,
257        };
258
259        dest.write_str("url(")?;
260        string.to_css(dest)?;
261        dest.write_char(')')
262    }
263}