Bug 1461288: Distinguish between specified and computed URLs. r?xidorn draft
authorEmilio Cobos Álvarez <emilio@crisal.io>
Mon, 14 May 2018 12:29:40 +0200
changeset 795181 f656ce369665988eb2c200c69319cea22f275ef3
parent 795180 620b240083368f7ac4a74d48b7ed25d564729d74
push id109889
push userbmo:emilio@crisal.io
push dateTue, 15 May 2018 08:27:16 +0000
reviewersxidorn
bugs1461288
milestone62.0a1
Bug 1461288: Distinguish between specified and computed URLs. r?xidorn This is needed to serialize computed URLs correctly from getComputedStyle. MozReview-Commit-ID: 9wakhqNrszb
layout/style/ServoBindings.cpp
layout/style/ServoBindings.h
layout/style/ServoBindings.toml
servo/components/style/gecko/conversions.rs
servo/components/style/gecko/url.rs
servo/components/style/properties/gecko.mako.rs
servo/components/style/values/animated/effects.rs
servo/components/style/values/animated/mod.rs
servo/components/style/values/computed/counters.rs
servo/components/style/values/computed/effects.rs
servo/components/style/values/generics/counters.rs
servo/components/style/values/generics/effects.rs
servo/components/style/values/specified/counters.rs
servo/components/style/values/specified/effects.rs
--- a/layout/style/ServoBindings.cpp
+++ b/layout/style/ServoBindings.cpp
@@ -2129,16 +2129,35 @@ MOZ_DEFINE_MALLOC_SIZE_OF(GeckoURLValueM
 
 size_t
 Gecko_URLValue_SizeOfIncludingThis(URLValue* aURL)
 {
   MOZ_ASSERT(NS_IsMainThread());
   return aURL->SizeOfIncludingThis(GeckoURLValueMallocSizeOf);
 }
 
+void
+Gecko_GetComputedURLSpec(const URLValueData* aURL, nsCString* aOut)
+{
+  MOZ_ASSERT(aURL);
+  MOZ_ASSERT(aOut);
+  if (aURL->IsLocalRef()) {
+    aOut->Assign(aURL->GetString());
+    return;
+  }
+  if (nsIURI* uri = aURL->GetURI()) {
+    nsresult rv = uri->GetSpec(*aOut);
+    if (NS_SUCCEEDED(rv)) {
+      return;
+    }
+  }
+
+  aOut->AssignLiteral("about:invalid");
+}
+
 NS_IMPL_THREADSAFE_FFI_REFCOUNTING(css::URLValue, CSSURLValue);
 
 NS_IMPL_THREADSAFE_FFI_REFCOUNTING(URLExtraData, URLExtraData);
 
 NS_IMPL_THREADSAFE_FFI_REFCOUNTING(nsStyleCoord::Calc, Calc);
 
 nsCSSShadowArray*
 Gecko_NewCSSShadowArray(uint32_t aLen)
--- a/layout/style/ServoBindings.h
+++ b/layout/style/ServoBindings.h
@@ -540,16 +540,17 @@ void Gecko_nsStyleSVGPaint_Reset(nsStyle
 
 void Gecko_nsStyleSVG_SetDashArrayLength(nsStyleSVG* svg, uint32_t len);
 void Gecko_nsStyleSVG_CopyDashArray(nsStyleSVG* dst, const nsStyleSVG* src);
 void Gecko_nsStyleSVG_SetContextPropertiesLength(nsStyleSVG* svg, uint32_t len);
 void Gecko_nsStyleSVG_CopyContextProperties(nsStyleSVG* dst, const nsStyleSVG* src);
 
 mozilla::css::URLValue* Gecko_NewURLValue(ServoBundledURI uri);
 size_t Gecko_URLValue_SizeOfIncludingThis(mozilla::css::URLValue* url);
+void Gecko_GetComputedURLSpec(const mozilla::css::URLValueData* url, nsCString* spec);
 NS_DECL_THREADSAFE_FFI_REFCOUNTING(mozilla::css::URLValue, CSSURLValue);
 NS_DECL_THREADSAFE_FFI_REFCOUNTING(RawGeckoURLExtraData, URLExtraData);
 
 void Gecko_FillAllImageLayers(nsStyleImageLayers* layers, uint32_t max_len);
 NS_DECL_THREADSAFE_FFI_REFCOUNTING(nsStyleCoord::Calc, Calc);
 
 nsCSSShadowArray* Gecko_NewCSSShadowArray(uint32_t len);
 NS_DECL_THREADSAFE_FFI_REFCOUNTING(nsCSSShadowArray, CSSShadowArray);
--- a/layout/style/ServoBindings.toml
+++ b/layout/style/ServoBindings.toml
@@ -435,17 +435,17 @@ fixups = [
 headers = ["mozilla/ServoBindings.h"]
 hide-types = [
     "nsACString_internal",
     "nsAString_internal",
     "ComputedStyleBorrowed",
     "ComputedStyleBorrowedOrNull",
 ]
 raw-lines = [
-    "pub use nsstring::{nsACString, nsAString, nsString, nsStringRepr};",
+    "pub use nsstring::{nsACString, nsAString, nsCString, nsString, nsStringRepr};",
     "use gecko_bindings::structs::nsStyleTransformMatrix;",
     "use gecko_bindings::structs::nsTArray;",
     "type nsACString_internal = nsACString;",
     "type nsAString_internal = nsAString;",
     "pub type ComputedStyleBorrowed<'a> = &'a ::properties::ComputedValues;",
     "pub type ComputedStyleBorrowedOrNull<'a> = Option<&'a ::properties::ComputedValues>;",
     "pub type ServoComputedDataBorrowed<'a> = &'a ServoComputedData;",
     "pub type RawServoAnimationValueTableBorrowed<'a> = &'a ();"
--- a/servo/components/style/gecko/conversions.rs
+++ b/servo/components/style/gecko/conversions.rs
@@ -140,21 +140,21 @@ impl Angle {
 }
 
 impl nsStyleImage {
     /// Set a given Servo `Image` value into this `nsStyleImage`.
     pub fn set(&mut self, image: Image) {
         match image {
             GenericImage::Gradient(boxed_gradient) => self.set_gradient(*boxed_gradient),
             GenericImage::Url(ref url) => unsafe {
-                bindings::Gecko_SetLayerImageImageValue(self, url.image_value.get());
+                bindings::Gecko_SetLayerImageImageValue(self, url.0.image_value.get());
             },
             GenericImage::Rect(ref image_rect) => {
                 unsafe {
-                    bindings::Gecko_SetLayerImageImageValue(self, image_rect.url.image_value.get());
+                    bindings::Gecko_SetLayerImageImageValue(self, image_rect.url.0.image_value.get());
                     bindings::Gecko_InitializeImageCropRect(self);
 
                     // Set CropRect
                     let ref mut rect = *self.mCropRect.mPtr;
                     image_rect
                         .top
                         .to_gecko_style_coord(&mut rect.data_at_mut(0));
                     image_rect
--- a/servo/components/style/gecko/url.rs
+++ b/servo/components/style/gecko/url.rs
@@ -7,20 +7,23 @@
 use cssparser::Parser;
 use gecko_bindings::bindings;
 use gecko_bindings::structs::{ServoBundledURI, URLExtraData};
 use gecko_bindings::structs::mozilla::css::URLValueData;
 use gecko_bindings::structs::root::{RustString, nsStyleImageRequest};
 use gecko_bindings::structs::root::mozilla::css::{ImageValue, URLValue};
 use gecko_bindings::sugar::refptr::RefPtr;
 use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use nsstring::nsCString;
 use parser::{Parse, ParserContext};
 use servo_arc::{Arc, RawOffsetArc};
 use std::mem;
-use style_traits::ParseError;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+use values::computed::{Context, ToComputedValue};
 
 /// A CSS url() value for gecko.
 #[css(function = "url")]
 #[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss)]
 pub struct CssUrl {
     /// The URL in unresolved string form.
     ///
     /// Refcounted since cloning this should be cheap and data: uris can be
@@ -65,20 +68,18 @@ impl CssUrl {
     }
 
     /// Returns true if this URL looks like a fragment.
     /// See https://drafts.csswg.org/css-values/#local-urls
     pub fn is_fragment(&self) -> bool {
         self.as_str().chars().next().map_or(false, |c| c == '#')
     }
 
-    /// Return the resolved url as string, or the empty string if it's invalid.
-    ///
-    /// FIXME(bholley): This returns the unresolved URL while the servo version
-    /// returns the resolved URL.
+    /// Return the unresolved url as string, or the empty string if it's
+    /// invalid.
     pub fn as_str(&self) -> &str {
         &*self.serialization
     }
 
     /// Little helper for Gecko's ffi.
     pub fn as_slice_components(&self) -> (*const u8, usize) {
         (
             self.serialization.as_str().as_ptr(),
@@ -116,17 +117,17 @@ impl MallocSizeOf for CssUrl {
         // We ignore `extra_data`, because RefPtr is tricky, and there aren't
         // many of them in practise (sharing is common).
 
         0
     }
 }
 
 /// A specified url() value for general usage.
-#[derive(Clone, Debug, SpecifiedValueInfo, ToComputedValue, ToCss)]
+#[derive(Clone, Debug, SpecifiedValueInfo, ToCss)]
 pub struct SpecifiedUrl {
     /// The specified url value.
     pub url: CssUrl,
     /// Gecko's URLValue so that we can reuse it while rematching a
     /// property with this specified value.
     #[css(skip)]
     pub url_value: RefPtr<URLValue>,
 }
@@ -134,25 +135,21 @@ pub struct SpecifiedUrl {
 impl SpecifiedUrl {
     fn from_css_url(url: CssUrl) -> Self {
         let url_value = unsafe {
             let ptr = bindings::Gecko_NewURLValue(url.for_ffi());
             // We do not expect Gecko_NewURLValue returns null.
             debug_assert!(!ptr.is_null());
             RefPtr::from_addrefed(ptr)
         };
-        SpecifiedUrl { url, url_value }
-    }
-
-    /// Convert from URLValueData to SpecifiedUrl.
-    pub unsafe fn from_url_value_data(url: &URLValueData) -> Result<Self, ()> {
-        CssUrl::from_url_value_data(url).map(Self::from_css_url)
+        Self { url, url_value }
     }
 }
 
+
 impl PartialEq for SpecifiedUrl {
     fn eq(&self, other: &Self) -> bool {
         self.url.eq(&other.url)
     }
 }
 
 impl Eq for SpecifiedUrl {}
 
@@ -174,59 +171,43 @@ impl MallocSizeOf for SpecifiedUrl {
         n += unsafe { bindings::Gecko_URLValue_SizeOfIncludingThis(self.url_value.get()) };
         n
     }
 }
 
 /// A specified url() value for image.
 ///
 /// This exists so that we can construct `ImageValue` and reuse it.
-#[derive(Clone, Debug, SpecifiedValueInfo, ToComputedValue, ToCss)]
+#[derive(Clone, Debug, SpecifiedValueInfo, ToCss)]
 pub struct SpecifiedImageUrl {
     /// The specified url value.
     pub url: CssUrl,
     /// Gecko's ImageValue so that we can reuse it while rematching a
     /// property with this specified value.
     #[css(skip)]
     pub image_value: RefPtr<ImageValue>,
 }
 
 impl SpecifiedImageUrl {
-    fn from_css_url(url: CssUrl) -> Self {
-        let image_value = unsafe {
-            let ptr = bindings::Gecko_ImageValue_Create(url.for_ffi());
-            // We do not expect Gecko_ImageValue_Create returns null.
-            debug_assert!(!ptr.is_null());
-            RefPtr::from_addrefed(ptr)
-        };
-        SpecifiedImageUrl { url, image_value }
-    }
-
     /// Parse a URL from a string value. See SpecifiedUrl::parse_from_string.
     pub fn parse_from_string<'a>(
         url: String,
         context: &ParserContext,
     ) -> Result<Self, ParseError<'a>> {
         CssUrl::parse_from_string(url, context).map(Self::from_css_url)
     }
 
-    /// Convert from URLValueData to SpecifiedUrl.
-    pub unsafe fn from_url_value_data(url: &URLValueData) -> Result<Self, ()> {
-        CssUrl::from_url_value_data(url).map(Self::from_css_url)
-    }
-
-    /// Convert from nsStyleImageRequest to SpecifiedUrl.
-    pub unsafe fn from_image_request(image_request: &nsStyleImageRequest) -> Result<Self, ()> {
-        if image_request.mImageValue.mRawPtr.is_null() {
-            return Err(());
-        }
-
-        let image_value = image_request.mImageValue.mRawPtr.as_ref().unwrap();
-        let url_value_data = &image_value._base;
-        Self::from_url_value_data(url_value_data)
+    fn from_css_url(url: CssUrl) -> Self {
+        let image_value = unsafe {
+            let ptr = bindings::Gecko_ImageValue_Create(url.for_ffi());
+            // We do not expect Gecko_ImageValue_Create returns null.
+            debug_assert!(!ptr.is_null());
+            RefPtr::from_addrefed(ptr)
+        };
+        Self { url, image_value }
     }
 }
 
 impl Parse for SpecifiedImageUrl {
     fn parse<'i, 't>(
         context: &ParserContext,
         input: &mut Parser<'i, 't>,
     ) -> Result<Self, ParseError<'i>> {
@@ -248,12 +229,109 @@ impl MallocSizeOf for SpecifiedImageUrl 
         // Although this is a RefPtr, this is the primary reference because
         // SpecifiedUrl is responsible for creating the image_value. So we
         // measure unconditionally here.
         n += unsafe { bindings::Gecko_ImageValue_SizeOfIncludingThis(self.image_value.get()) };
         n
     }
 }
 
+impl ToComputedValue for SpecifiedUrl {
+    type ComputedValue = ComputedUrl;
+
+    #[inline]
+    fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+        ComputedUrl(self.clone())
+    }
+
+    #[inline]
+    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+        computed.0.clone()
+    }
+}
+
+impl ToComputedValue for SpecifiedImageUrl {
+    type ComputedValue = ComputedImageUrl;
+
+    #[inline]
+    fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+        ComputedImageUrl(self.clone())
+    }
+
+    #[inline]
+    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+        computed.0.clone()
+    }
+}
+
+fn serialize_computed_url<W>(
+    url_value_data: &URLValueData,
+    dest: &mut CssWriter<W>,
+) -> fmt::Result
+where
+    W: Write,
+{
+    dest.write_str("url(")?;
+    unsafe {
+        let mut string = nsCString::new();
+        bindings::Gecko_GetComputedURLSpec(url_value_data, &mut string);
+        string.as_str_unchecked().to_css(dest)?;
+    }
+    dest.write_char(')')
+}
+
 /// The computed value of a CSS `url()`.
-pub type ComputedUrl = SpecifiedUrl;
+///
+/// The only difference between specified and computed URLs is the
+/// serialization.
+#[derive(Clone, Debug, Eq, PartialEq, MallocSizeOf)]
+pub struct ComputedUrl(pub SpecifiedUrl);
+
+impl ToCss for ComputedUrl {
+    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+    where
+        W: Write
+    {
+        serialize_computed_url(&self.0.url_value._base, dest)
+    }
+}
+
+impl ComputedUrl {
+    /// Convert from URLValueData to ComputedUrl.
+    pub unsafe fn from_url_value_data(url: &URLValueData) -> Result<Self, ()> {
+        Ok(ComputedUrl(
+            SpecifiedUrl::from_css_url(CssUrl::from_url_value_data(url)?)
+        ))
+    }
+}
+
 /// The computed value of a CSS `url()` for image.
-pub type ComputedImageUrl = SpecifiedImageUrl;
+#[derive(Clone, Debug, Eq, PartialEq, MallocSizeOf)]
+pub struct ComputedImageUrl(pub SpecifiedImageUrl);
+
+impl ToCss for ComputedImageUrl {
+    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+    where
+        W: Write
+    {
+        serialize_computed_url(&self.0.image_value._base, dest)
+    }
+}
+
+impl ComputedImageUrl {
+    /// Convert from URLValueData to SpecifiedUrl.
+    pub unsafe fn from_url_value_data(url: &URLValueData) -> Result<Self, ()> {
+        Ok(ComputedImageUrl(
+            SpecifiedImageUrl::from_css_url(CssUrl::from_url_value_data(url)?)
+        ))
+    }
+
+    /// Convert from nsStyleImageReques to ComputedImageUrl.
+    pub unsafe fn from_image_request(image_request: &nsStyleImageRequest) -> Result<Self, ()> {
+        if image_request.mImageValue.mRawPtr.is_null() {
+            return Err(());
+        }
+
+        let image_value = image_request.mImageValue.mRawPtr.as_ref().unwrap();
+        let url_value_data = &image_value._base;
+        Self::from_url_value_data(url_value_data)
+    }
+}
--- a/servo/components/style/properties/gecko.mako.rs
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -692,17 +692,17 @@ def set_gecko_property(ffi_name, expr):
             SVGPaintKind::ContextFill => {
                 paint.mType = nsStyleSVGPaintType::eStyleSVGPaintType_ContextFill;
             }
             SVGPaintKind::ContextStroke => {
                 paint.mType = nsStyleSVGPaintType::eStyleSVGPaintType_ContextStroke;
             }
             SVGPaintKind::PaintServer(url) => {
                 unsafe {
-                    bindings::Gecko_nsStyleSVGPaint_SetURLValue(paint, url.url_value.get());
+                    bindings::Gecko_nsStyleSVGPaint_SetURLValue(paint, url.0.url_value.get());
                 }
             }
             SVGPaintKind::Color(color) => {
                 paint.mType = nsStyleSVGPaintType::eStyleSVGPaintType_Color;
                 unsafe {
                     *paint.mPaint.mColor.as_mut() = convert_rgba_to_nscolor(&color);
                 }
             }
@@ -732,18 +732,18 @@ def set_gecko_property(ffi_name, expr):
 
     #[allow(non_snake_case)]
     pub fn reset_${ident}(&mut self, other: &Self) {
         self.copy_${ident}_from(other)
     }
 
     #[allow(non_snake_case)]
     pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
+        use values::computed::url::ComputedUrl;
         use values::generics::svg::{SVGPaint, SVGPaintKind};
-        use values::specified::url::SpecifiedUrl;
         use self::structs::nsStyleSVGPaintType;
         use self::structs::nsStyleSVGFallbackType;
         let ref paint = ${get_gecko_property(gecko_ffi_name)};
 
         let fallback = match paint.mFallbackType {
             nsStyleSVGFallbackType::eStyleSVGFallbackType_Color => {
                 Some(Either::First(convert_nscolor_to_rgba(paint.mFallbackColor)))
             },
@@ -755,17 +755,17 @@ def set_gecko_property(ffi_name, expr):
 
         let kind = match paint.mType {
             nsStyleSVGPaintType::eStyleSVGPaintType_None => SVGPaintKind::None,
             nsStyleSVGPaintType::eStyleSVGPaintType_ContextFill => SVGPaintKind::ContextFill,
             nsStyleSVGPaintType::eStyleSVGPaintType_ContextStroke => SVGPaintKind::ContextStroke,
             nsStyleSVGPaintType::eStyleSVGPaintType_Server => {
                 unsafe {
                     SVGPaintKind::PaintServer(
-                        SpecifiedUrl::from_url_value_data(
+                        ComputedUrl::from_url_value_data(
                             &(**paint.mPaint.mPaintServer.as_ref())._base
                         ).unwrap()
                     )
                 }
             }
             nsStyleSVGPaintType::eStyleSVGPaintType_Color => {
                 unsafe { SVGPaintKind::Color(convert_nscolor_to_rgba(*paint.mPaint.mColor.as_ref())) }
             }
@@ -934,17 +934,17 @@ def set_gecko_property(ffi_name, expr):
     }
 </%def>
 
 <%def name="impl_css_url(ident, gecko_ffi_name)">
     #[allow(non_snake_case)]
     pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
         match v {
             UrlOrNone::Url(ref url) => {
-                self.gecko.${gecko_ffi_name}.set_move(url.url_value.clone())
+                self.gecko.${gecko_ffi_name}.set_move(url.0.url_value.clone())
             }
             UrlOrNone::None => {
                 unsafe {
                     self.gecko.${gecko_ffi_name}.clear();
                 }
             }
         }
     }
@@ -956,26 +956,28 @@ def set_gecko_property(ffi_name, expr):
     }
     #[allow(non_snake_case)]
     pub fn reset_${ident}(&mut self, other: &Self) {
         self.copy_${ident}_from(other)
     }
 
     #[allow(non_snake_case)]
     pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
-        use values::specified::url::SpecifiedUrl;
+        use values::computed::url::ComputedUrl;
 
         if self.gecko.${gecko_ffi_name}.mRawPtr.is_null() {
-            UrlOrNone::none()
-        } else {
-            unsafe {
-                let ref gecko_url_value = *self.gecko.${gecko_ffi_name}.mRawPtr;
-                UrlOrNone::Url(SpecifiedUrl::from_url_value_data(&gecko_url_value._base)
-                               .expect("${gecko_ffi_name} could not convert to SpecifiedUrl"))
-            }
+            return UrlOrNone::none()
+        }
+
+        unsafe {
+            let gecko_url_value = &*self.gecko.${gecko_ffi_name}.mRawPtr;
+            UrlOrNone::Url(
+                ComputedUrl::from_url_value_data(&gecko_url_value._base)
+                    .expect("${gecko_ffi_name} could not convert to ComputedUrl")
+            )
         }
     }
 </%def>
 
 <%
 transform_functions = [
     ("Matrix3D", "matrix3d", ["number"] * 16),
     ("Matrix", "matrix", ["number"] * 6),
@@ -4105,41 +4107,42 @@ fn static_assert() {
         match image {
             UrlOrNone::None => {
                 unsafe {
                     Gecko_SetListStyleImageNone(&mut self.gecko);
                 }
             }
             UrlOrNone::Url(ref url) => {
                 unsafe {
-                    Gecko_SetListStyleImageImageValue(&mut self.gecko, url.image_value.get());
+                    Gecko_SetListStyleImageImageValue(&mut self.gecko, url.0.image_value.get());
                 }
             }
         }
     }
 
     pub fn copy_list_style_image_from(&mut self, other: &Self) {
         unsafe { Gecko_CopyListStyleImageFrom(&mut self.gecko, &other.gecko); }
     }
 
     pub fn reset_list_style_image(&mut self, other: &Self) {
         self.copy_list_style_image_from(other)
     }
 
     pub fn clone_list_style_image(&self) -> longhands::list_style_image::computed_value::T {
-        use values::specified::url::SpecifiedImageUrl;
+        use values::computed::url::ComputedImageUrl;
 
         if self.gecko.mListStyleImage.mRawPtr.is_null() {
             return UrlOrNone::None;
         }
 
         unsafe {
             let ref gecko_image_request = *self.gecko.mListStyleImage.mRawPtr;
-            UrlOrNone::Url(SpecifiedImageUrl::from_image_request(gecko_image_request)
-                           .expect("mListStyleImage could not convert to SpecifiedImageUrl"))
+            UrlOrNone::Url(ComputedImageUrl::from_image_request(
+                gecko_image_request
+            ).expect("mListStyleImage could not convert to ComputedImageUrl"))
         }
     }
 
     pub fn set_list_style_type(&mut self, v: longhands::list_style_type::computed_value::T, device: &Device) {
         use gecko_bindings::bindings::Gecko_SetCounterStyleToString;
         use nsstring::{nsACString, nsCStr};
         use self::longhands::list_style_type::computed_value::T;
         match v {
@@ -4475,17 +4478,17 @@ fn static_assert() {
                         }
                     }
 
                     let gecko_shadow = init_shadow(gecko_filter);
                     gecko_shadow.mArray[0].set_from_simple_shadow(shadow);
                 },
                 Url(ref url) => {
                     unsafe {
-                        bindings::Gecko_nsStyleFilter_SetURLValue(gecko_filter, url.url_value.get());
+                        bindings::Gecko_nsStyleFilter_SetURLValue(gecko_filter, url.0.url_value.get());
                     }
                 },
             }
         }
     }
 
     pub fn copy_filter_from(&mut self, other: &Self) {
         unsafe {
@@ -4494,17 +4497,17 @@ fn static_assert() {
     }
 
     pub fn reset_filter(&mut self, other: &Self) {
         self.copy_filter_from(other)
     }
 
     pub fn clone_filter(&self) -> longhands::filter::computed_value::T {
         use values::generics::effects::Filter;
-        use values::specified::url::SpecifiedUrl;
+        use values::computed::url::ComputedUrl;
         use gecko_bindings::structs::NS_STYLE_FILTER_BLUR;
         use gecko_bindings::structs::NS_STYLE_FILTER_BRIGHTNESS;
         use gecko_bindings::structs::NS_STYLE_FILTER_CONTRAST;
         use gecko_bindings::structs::NS_STYLE_FILTER_GRAYSCALE;
         use gecko_bindings::structs::NS_STYLE_FILTER_INVERT;
         use gecko_bindings::structs::NS_STYLE_FILTER_OPACITY;
         use gecko_bindings::structs::NS_STYLE_FILTER_SATURATE;
         use gecko_bindings::structs::NS_STYLE_FILTER_SEPIA;
@@ -4536,17 +4539,17 @@ fn static_assert() {
                         Filter::DropShadow(
                             (**filter.__bindgen_anon_1.mDropShadow.as_ref()).mArray[0].to_simple_shadow(),
                         )
                     });
                 },
                 NS_STYLE_FILTER_URL => {
                     filters.push(unsafe {
                         Filter::Url(
-                            SpecifiedUrl::from_url_value_data(&(**filter.__bindgen_anon_1.mURL.as_ref())._base).unwrap()
+                            ComputedUrl::from_url_value_data(&(**filter.__bindgen_anon_1.mURL.as_ref())._base).unwrap()
                         )
                     });
                 }
                 _ => {},
             }
         }
         longhands::filter::computed_value::T(filters)
     }
@@ -5002,17 +5005,17 @@ fn static_assert() {
         // clean up existing struct
         unsafe { Gecko_DestroyShapeSource(${ident}) };
         ${ident}.mType = StyleShapeSourceType::None;
 
         match v {
             % if ident == "clip_path":
             ShapeSource::ImageOrUrl(ref url) => {
                 unsafe {
-                    bindings::Gecko_StyleShapeSource_SetURLValue(${ident}, url.url_value.get())
+                    bindings::Gecko_StyleShapeSource_SetURLValue(${ident}, url.0.url_value.get())
                 }
             }
             % elif ident == "shape_outside":
             ShapeSource::ImageOrUrl(image) => {
                 unsafe {
                     bindings::Gecko_NewShapeImage(${ident});
                     let style_image = &mut *${ident}.mShapeImage.mPtr;
                     style_image.set(image);
@@ -5342,17 +5345,17 @@ clip-path
 
         unsafe {
             Gecko_SetCursorArrayLength(&mut self.gecko, v.images.len());
         }
         for i in 0..v.images.len() {
             unsafe {
                 Gecko_SetCursorImageValue(
                     &mut self.gecko.mCursorImages[i],
-                    v.images[i].url.image_value.get(),
+                    v.images[i].url.0.image_value.get(),
                 );
             }
 
             match v.images[i].hotspot {
                 Some((x, y)) => {
                     self.gecko.mCursorImages[i].mHaveHotspot = true;
                     self.gecko.mCursorImages[i].mHotspotX = x;
                     self.gecko.mCursorImages[i].mHotspotY = y;
@@ -5372,18 +5375,18 @@ clip-path
     }
 
     pub fn reset_cursor(&mut self, other: &Self) {
         self.copy_cursor_from(other)
     }
 
     pub fn clone_cursor(&self) -> longhands::cursor::computed_value::T {
         use values::computed::ui::CursorImage;
+        use values::computed::url::ComputedImageUrl;
         use style_traits::cursor::CursorKind;
-        use values::specified::url::SpecifiedImageUrl;
 
         let keyword = match self.gecko.mCursor as u32 {
             structs::NS_STYLE_CURSOR_AUTO => CursorKind::Auto,
             structs::NS_STYLE_CURSOR_NONE => CursorKind::None,
             structs::NS_STYLE_CURSOR_DEFAULT => CursorKind::Default,
             structs::NS_STYLE_CURSOR_POINTER => CursorKind::Pointer,
             structs::NS_STYLE_CURSOR_CONTEXT_MENU => CursorKind::ContextMenu,
             structs::NS_STYLE_CURSOR_HELP => CursorKind::Help,
@@ -5418,18 +5421,18 @@ clip-path
             structs::NS_STYLE_CURSOR_ZOOM_IN => CursorKind::ZoomIn,
             structs::NS_STYLE_CURSOR_ZOOM_OUT => CursorKind::ZoomOut,
             _ => panic!("Found unexpected value in style struct for cursor property"),
         };
 
         let images = self.gecko.mCursorImages.iter().map(|gecko_cursor_image| {
             let url = unsafe {
                 let gecko_image_request = gecko_cursor_image.mImage.mRawPtr.as_ref().unwrap();
-                SpecifiedImageUrl::from_image_request(&gecko_image_request)
-                    .expect("mCursorImages.mImage could not convert to SpecifiedImageUrl")
+                ComputedImageUrl::from_image_request(&gecko_image_request)
+                    .expect("mCursorImages.mImage could not convert to ComputedImageUrl")
             };
 
             let hotspot =
                 if gecko_cursor_image.mHaveHotspot {
                     Some((gecko_cursor_image.mHotspotX, gecko_cursor_image.mHotspotY))
                 } else {
                     None
                 };
@@ -5478,17 +5481,17 @@ clip-path
 <%self:impl_trait style_struct_name="Counters"
                   skip_longhands="content counter-increment counter-reset">
     pub fn ineffective_content_property(&self) -> bool {
         self.gecko.mContents.is_empty()
     }
 
     pub fn set_content(&mut self, v: longhands::content::computed_value::T, device: &Device) {
         use values::CustomIdent;
-        use values::computed::counters::{Content, ContentItem};
+        use values::generics::counters::{Content, ContentItem};
         use values::generics::CounterStyleOrNone;
         use gecko_bindings::structs::nsStyleContentData;
         use gecko_bindings::structs::nsStyleContentAttr;
         use gecko_bindings::structs::nsStyleContentType;
         use gecko_bindings::structs::nsStyleContentType::*;
         use gecko_bindings::bindings::Gecko_ClearAndResizeStyleContents;
 
         // Converts a string as utf16, and returns an owned, zero-terminated raw buffer.
@@ -5605,17 +5608,17 @@ clip-path
                                 style.clone(),
                                 device,
                             );
                         }
                         ContentItem::Url(ref url) => {
                             unsafe {
                                 bindings::Gecko_SetContentDataImageValue(
                                     &mut self.gecko.mContents[i],
-                                    url.image_value.get(),
+                                    url.0.image_value.get(),
                                 )
                             }
                         }
                     }
                 }
             }
         }
     }
@@ -5630,20 +5633,20 @@ clip-path
     pub fn reset_content(&mut self, other: &Self) {
         self.copy_content_from(other)
     }
 
     pub fn clone_content(&self) -> longhands::content::computed_value::T {
         use {Atom, Namespace};
         use gecko::conversions::string_from_chars_pointer;
         use gecko_bindings::structs::nsStyleContentType::*;
-        use values::computed::counters::{Content, ContentItem};
+        use values::generics::counters::{Content, ContentItem};
+        use values::computed::url::ComputedImageUrl;
         use values::{CustomIdent, Either};
         use values::generics::CounterStyleOrNone;
-        use values::specified::url::SpecifiedImageUrl;
         use values::specified::Attr;
 
         if self.gecko.mContents.is_empty() {
             return Content::Normal;
         }
 
         if self.gecko.mContents.len() == 1 &&
            self.gecko.mContents[0].mType == eStyleContentType_AltContent {
@@ -5694,18 +5697,18 @@ clip-path
                             ContentItem::Counters(ident, separator.into_boxed_str(), style)
                         }
                     },
                     eStyleContentType_Image => {
                         unsafe {
                             let gecko_image_request =
                                 &**gecko_content.mContent.mImage.as_ref();
                             ContentItem::Url(
-                                SpecifiedImageUrl::from_image_request(gecko_image_request)
-                                    .expect("mContent could not convert to SpecifiedImageUrl")
+                                ComputedImageUrl::from_image_request(gecko_image_request)
+                                    .expect("mContent could not convert to ComputedImageUrl")
                             )
                         }
                     },
                     _ => panic!("Found unexpected value in style struct for content property"),
                 }
             }).collect::<Vec<_>>().into_boxed_slice()
         )
     }
--- a/servo/components/style/values/animated/effects.rs
+++ b/servo/components/style/values/animated/effects.rs
@@ -9,16 +9,17 @@ use properties::longhands::filter::compu
 use properties::longhands::text_shadow::computed_value::T as ComputedTextShadowList;
 use std::cmp;
 #[cfg(not(feature = "gecko"))]
 use values::Impossible;
 use values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero};
 use values::animated::color::RGBA;
 use values::computed::{Angle, Number};
 use values::computed::length::Length;
+use values::computed::url::ComputedUrl;
 use values::distance::{ComputeSquaredDistance, SquaredDistance};
 use values::generics::effects::BoxShadow as GenericBoxShadow;
 use values::generics::effects::Filter as GenericFilter;
 use values::generics::effects::SimpleShadow as GenericSimpleShadow;
 
 /// An animated value for the `box-shadow` property.
 pub type BoxShadowList = ShadowList<BoxShadow>;
 
@@ -37,21 +38,21 @@ pub type BoxShadow = GenericBoxShadow<Op
 
 /// An animated value for the `filter` property.
 #[cfg_attr(feature = "servo", derive(MallocSizeOf))]
 #[derive(Clone, Debug, PartialEq)]
 pub struct FilterList(pub Vec<Filter>);
 
 /// An animated value for a single `filter`.
 #[cfg(feature = "gecko")]
-pub type Filter = GenericFilter<Angle, Number, Length, SimpleShadow>;
+pub type Filter = GenericFilter<Angle, Number, Length, SimpleShadow, ComputedUrl>;
 
 /// An animated value for a single `filter`.
 #[cfg(not(feature = "gecko"))]
-pub type Filter = GenericFilter<Angle, Number, Length, Impossible>;
+pub type Filter = GenericFilter<Angle, Number, Length, Impossible, ComputedUrl>;
 
 /// An animated value for the `drop-shadow()` filter.
 pub type SimpleShadow = GenericSimpleShadow<Option<RGBA>, Length, Length>;
 
 impl ToAnimatedValue for ComputedBoxShadowList {
     type AnimatedValue = BoxShadowList;
 
     #[inline]
--- a/servo/components/style/values/animated/mod.rs
+++ b/servo/components/style/values/animated/mod.rs
@@ -10,19 +10,17 @@
 
 use app_units::Au;
 use euclid::{Point2D, Size2D};
 use smallvec::SmallVec;
 use values::computed::Angle as ComputedAngle;
 use values::computed::BorderCornerRadius as ComputedBorderCornerRadius;
 use values::computed::MaxLength as ComputedMaxLength;
 use values::computed::MozLength as ComputedMozLength;
-#[cfg(feature = "servo")]
 use values::computed::url::ComputedUrl;
-use values::specified::url::SpecifiedUrl;
 
 pub mod color;
 pub mod effects;
 
 /// Animate from one value to another.
 ///
 /// This trait is derivable with `#[derive(Animate)]`. The derived
 /// implementation uses a `match` expression with identical patterns for both
@@ -255,18 +253,16 @@ macro_rules! trivial_to_animated_value {
                 animated
             }
         }
     };
 }
 
 trivial_to_animated_value!(Au);
 trivial_to_animated_value!(ComputedAngle);
-trivial_to_animated_value!(SpecifiedUrl);
-#[cfg(feature = "servo")]
 trivial_to_animated_value!(ComputedUrl);
 trivial_to_animated_value!(bool);
 trivial_to_animated_value!(f32);
 
 impl ToAnimatedValue for ComputedBorderCornerRadius {
     type AnimatedValue = Self;
 
     #[inline]
--- a/servo/components/style/values/computed/counters.rs
+++ b/servo/components/style/values/computed/counters.rs
@@ -1,160 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Computed values for counter properties
 
-#[cfg(feature = "servo")]
-use computed_values::list_style_type::T as ListStyleType;
-use cssparser::{Parser, Token};
-use parser::{Parse, ParserContext};
-use selectors::parser::SelectorParseErrorKind;
-use style_traits::{ParseError, StyleParseErrorKind};
-use values::CustomIdent;
-#[cfg(feature = "gecko")]
-use values::generics::CounterStyleOrNone;
+use values::computed::url::ComputedImageUrl;
+use values::generics::counters as generics;
 use values::generics::counters::CounterIncrement as GenericCounterIncrement;
 use values::generics::counters::CounterReset as GenericCounterReset;
-#[cfg(feature = "gecko")]
-use values::specified::Attr;
-#[cfg(feature = "gecko")]
-use values::specified::url::SpecifiedImageUrl;
-pub use values::specified::{Content, ContentItem};
 
 /// A computed value for the `counter-increment` property.
 pub type CounterIncrement = GenericCounterIncrement<i32>;
 
 /// A computed value for the `counter-increment` property.
 pub type CounterReset = GenericCounterReset<i32>;
 
-impl Content {
-    /// Set `content` property to `normal`.
-    #[inline]
-    pub fn normal() -> Self {
-        Content::Normal
-    }
-
-    #[cfg(feature = "servo")]
-    fn parse_counter_style(input: &mut Parser) -> ListStyleType {
-        input
-            .try(|input| {
-                input.expect_comma()?;
-                ListStyleType::parse(input)
-            })
-            .unwrap_or(ListStyleType::Decimal)
-    }
-
-    #[cfg(feature = "gecko")]
-    fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyleOrNone {
-        input
-            .try(|input| {
-                input.expect_comma()?;
-                CounterStyleOrNone::parse(context, input)
-            })
-            .unwrap_or(CounterStyleOrNone::decimal())
-    }
-}
-
-impl Parse for Content {
-    // normal | none | [ <string> | <counter> | open-quote | close-quote | no-open-quote |
-    // no-close-quote ]+
-    // TODO: <uri>, attr(<identifier>)
-    fn parse<'i, 't>(
-        _context: &ParserContext,
-        input: &mut Parser<'i, 't>,
-    ) -> Result<Self, ParseError<'i>> {
-        if input
-            .try(|input| input.expect_ident_matching("normal"))
-            .is_ok()
-        {
-            return Ok(Content::Normal);
-        }
-        if input
-            .try(|input| input.expect_ident_matching("none"))
-            .is_ok()
-        {
-            return Ok(Content::None);
-        }
-        #[cfg(feature = "gecko")]
-        {
-            if input
-                .try(|input| input.expect_ident_matching("-moz-alt-content"))
-                .is_ok()
-            {
-                return Ok(Content::MozAltContent);
-            }
-        }
+/// A computed value for the `content` property.
+pub type Content = generics::Content<ComputedImageUrl>;
 
-        let mut content = vec![];
-        loop {
-            #[cfg(feature = "gecko")]
-            {
-                if let Ok(url) = input.try(|i| SpecifiedImageUrl::parse(_context, i)) {
-                    content.push(ContentItem::Url(url));
-                    continue;
-                }
-            }
-            // FIXME: remove clone() when lifetimes are non-lexical
-            match input.next().map(|t| t.clone()) {
-                Ok(Token::QuotedString(ref value)) => {
-                    content.push(ContentItem::String(
-                        value.as_ref().to_owned().into_boxed_str(),
-                    ));
-                },
-                Ok(Token::Function(ref name)) => {
-                    let result = match_ignore_ascii_case! { &name,
-                        "counter" => Some(input.parse_nested_block(|input| {
-                            let location = input.current_source_location();
-                            let name = CustomIdent::from_ident(location, input.expect_ident()?, &[])?;
-                            #[cfg(feature = "servo")]
-                            let style = Content::parse_counter_style(input);
-                            #[cfg(feature = "gecko")]
-                            let style = Content::parse_counter_style(_context, input);
-                            Ok(ContentItem::Counter(name, style))
-                        })),
-                        "counters" => Some(input.parse_nested_block(|input| {
-                            let location = input.current_source_location();
-                            let name = CustomIdent::from_ident(location, input.expect_ident()?, &[])?;
-                            input.expect_comma()?;
-                            let separator = input.expect_string()?.as_ref().to_owned().into_boxed_str();
-                            #[cfg(feature = "servo")]
-                            let style = Content::parse_counter_style(input);
-                            #[cfg(feature = "gecko")]
-                            let style = Content::parse_counter_style(_context, input);
-                            Ok(ContentItem::Counters(name, separator, style))
-                        })),
-                        #[cfg(feature = "gecko")]
-                        "attr" => Some(input.parse_nested_block(|input| {
-                            Ok(ContentItem::Attr(Attr::parse_function(_context, input)?))
-                        })),
-                        _ => None
-                    };
-                    match result {
-                        Some(result) => content.push(result?),
-                        None => {
-                            return Err(input.new_custom_error(
-                                StyleParseErrorKind::UnexpectedFunction(name.clone()),
-                            ))
-                        },
-                    }
-                },
-                Ok(Token::Ident(ref ident)) => {
-                    content.push(match_ignore_ascii_case! { &ident,
-                        "open-quote" => ContentItem::OpenQuote,
-                        "close-quote" => ContentItem::CloseQuote,
-                        "no-open-quote" => ContentItem::NoOpenQuote,
-                        "no-close-quote" => ContentItem::NoCloseQuote,
-                        _ => return Err(input.new_custom_error(
-                                SelectorParseErrorKind::UnexpectedIdent(ident.clone())))
-                    });
-                },
-                Err(_) => break,
-                Ok(t) => return Err(input.new_unexpected_token_error(t)),
-            }
-        }
-        if content.is_empty() {
-            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
-        }
-        Ok(Content::Items(content.into_boxed_slice()))
-    }
-}
+/// A computed content item.
+pub type ContentItem = generics::ContentItem<ComputedImageUrl>;
+
--- a/servo/components/style/values/computed/effects.rs
+++ b/servo/components/style/values/computed/effects.rs
@@ -4,25 +4,26 @@
 
 //! Computed types for CSS values related to effects.
 
 #[cfg(not(feature = "gecko"))]
 use values::Impossible;
 use values::computed::{Angle, NonNegativeNumber};
 use values::computed::color::RGBAColor;
 use values::computed::length::{Length, NonNegativeLength};
+use values::computed::url::ComputedUrl;
 use values::generics::effects::BoxShadow as GenericBoxShadow;
 use values::generics::effects::Filter as GenericFilter;
 use values::generics::effects::SimpleShadow as GenericSimpleShadow;
 
 /// A computed value for a single shadow of the `box-shadow` property.
 pub type BoxShadow = GenericBoxShadow<Option<RGBAColor>, Length, NonNegativeLength, Length>;
 
 /// A computed value for a single `filter`.
 #[cfg(feature = "gecko")]
-pub type Filter = GenericFilter<Angle, NonNegativeNumber, NonNegativeLength, SimpleShadow>;
+pub type Filter = GenericFilter<Angle, NonNegativeNumber, NonNegativeLength, SimpleShadow, ComputedUrl>;
 
 /// A computed value for a single `filter`.
 #[cfg(not(feature = "gecko"))]
-pub type Filter = GenericFilter<Angle, NonNegativeNumber, NonNegativeLength, Impossible>;
+pub type Filter = GenericFilter<Angle, NonNegativeNumber, NonNegativeLength, Impossible, ComputedUrl>;
 
 /// A computed value for the `drop-shadow()` filter.
 pub type SimpleShadow = GenericSimpleShadow<Option<RGBAColor>, Length, NonNegativeLength>;
--- a/servo/components/style/values/generics/counters.rs
+++ b/servo/components/style/values/generics/counters.rs
@@ -1,16 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Generic types for counters-related CSS values.
 
+#[cfg(feature = "servo")]
+use computed_values::list_style_type::T as ListStyleType;
 use std::ops::Deref;
 use values::CustomIdent;
+#[cfg(feature = "gecko")]
+use values::generics::CounterStyleOrNone;
+#[cfg(feature = "gecko")]
+use values::specified::Attr;
 
 /// A name / value pair for counters.
 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo,
          ToComputedValue, ToCss)]
 pub struct CounterPair<Integer> {
     /// The name of the counter.
     pub name: CustomIdent,
     /// The value of the counter / increment / etc.
@@ -69,8 +75,83 @@ impl<I> Deref for CounterReset<I> {
 pub struct Counters<I>(#[css(iterable, if_empty = "none")] Box<[CounterPair<I>]>);
 
 impl<I> Default for Counters<I> {
     #[inline]
     fn default() -> Self {
         Counters(vec![].into_boxed_slice())
     }
 }
+
+#[cfg(feature = "servo")]
+type CounterStyleType = ListStyleType;
+
+#[cfg(feature = "gecko")]
+type CounterStyleType = CounterStyleOrNone;
+
+#[cfg(feature = "servo")]
+#[inline]
+fn is_decimal(counter_type: &CounterStyleType) -> bool {
+    *counter_type == ListStyleType::Decimal
+}
+
+#[cfg(feature = "gecko")]
+#[inline]
+fn is_decimal(counter_type: &CounterStyleType) -> bool {
+    *counter_type == CounterStyleOrNone::decimal()
+}
+
+/// The specified value for the `content` property.
+///
+/// https://drafts.csswg.org/css-content/#propdef-content
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo,
+         ToComputedValue, ToCss)]
+pub enum Content<ImageUrl> {
+    /// `normal` reserved keyword.
+    Normal,
+    /// `none` reserved keyword.
+    None,
+    /// `-moz-alt-content`.
+    #[cfg(feature = "gecko")]
+    MozAltContent,
+    /// Content items.
+    Items(#[css(iterable)] Box<[ContentItem<ImageUrl>]>),
+}
+
+impl<ImageUrl> Content<ImageUrl> {
+    /// Set `content` property to `normal`.
+    #[inline]
+    pub fn normal() -> Self {
+        Content::Normal
+    }
+
+}
+
+/// Items for the `content` property.
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo,
+         ToComputedValue, ToCss)]
+pub enum ContentItem<ImageUrl> {
+    /// Literal string content.
+    String(Box<str>),
+    /// `counter(name, style)`.
+    #[css(comma, function)]
+    Counter(CustomIdent, #[css(skip_if = "is_decimal")] CounterStyleType),
+    /// `counters(name, separator, style)`.
+    #[css(comma, function)]
+    Counters(
+        CustomIdent,
+        Box<str>,
+        #[css(skip_if = "is_decimal")] CounterStyleType,
+    ),
+    /// `open-quote`.
+    OpenQuote,
+    /// `close-quote`.
+    CloseQuote,
+    /// `no-open-quote`.
+    NoOpenQuote,
+    /// `no-close-quote`.
+    NoCloseQuote,
+    /// `attr([namespace? `|`]? ident)`
+    #[cfg(feature = "gecko")]
+    Attr(Attr),
+    /// `url(url)`
+    Url(ImageUrl),
+}
--- a/servo/components/style/values/generics/effects.rs
+++ b/servo/components/style/values/generics/effects.rs
@@ -1,36 +1,34 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Generic types for CSS values related to effects.
 
-#[cfg(feature = "gecko")]
-use values::specified::url::SpecifiedUrl;
-
 /// A generic value for a single `box-shadow`.
 #[derive(Animate, Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo,
          ToAnimatedValue, ToAnimatedZero, ToCss)]
 pub struct BoxShadow<Color, SizeLength, BlurShapeLength, ShapeLength> {
     /// The base shadow.
     pub base: SimpleShadow<Color, SizeLength, BlurShapeLength>,
     /// The spread radius.
     pub spread: ShapeLength,
     /// Whether this is an inset box shadow.
     #[animation(constant)]
     #[css(represents_keyword)]
     pub inset: bool,
 }
 
 /// A generic value for a single `filter`.
 #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[animation(no_bound(Url))]
 #[derive(Clone, ComputeSquaredDistance, Debug, MallocSizeOf, PartialEq,
          SpecifiedValueInfo, ToAnimatedValue, ToComputedValue, ToCss)]
-pub enum Filter<Angle, Factor, Length, DropShadow> {
+pub enum Filter<Angle, Factor, Length, DropShadow, Url> {
     /// `blur(<length>)`
     #[css(function)]
     Blur(Length),
     /// `brightness(<factor>)`
     #[css(function)]
     Brightness(Factor),
     /// `contrast(<factor>)`
     #[css(function)]
@@ -53,18 +51,17 @@ pub enum Filter<Angle, Factor, Length, D
     /// `sepia(<factor>)`
     #[css(function)]
     Sepia(Factor),
     /// `drop-shadow(...)`
     #[css(function)]
     DropShadow(DropShadow),
     /// `<url>`
     #[animation(error)]
-    #[cfg(feature = "gecko")]
-    Url(SpecifiedUrl),
+    Url(Url),
 }
 
 /// A generic value for the `drop-shadow()` filter and the `text-shadow` property.
 ///
 /// Contrary to the canonical order from the spec, the color is serialised
 /// first, like in Gecko and Webkit.
 #[derive(Animate, Clone, ComputeSquaredDistance, Debug, MallocSizeOf, PartialEq,
          SpecifiedValueInfo, ToAnimatedValue, ToAnimatedZero, ToCss)]
--- a/servo/components/style/values/specified/counters.rs
+++ b/servo/components/style/values/specified/counters.rs
@@ -3,28 +3,29 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! Specified types for counter properties.
 
 #[cfg(feature = "servo")]
 use computed_values::list_style_type::T as ListStyleType;
 use cssparser::{Parser, Token};
 use parser::{Parse, ParserContext};
+use selectors::parser::SelectorParseErrorKind;
 use style_traits::{ParseError, StyleParseErrorKind};
 use values::CustomIdent;
 #[cfg(feature = "gecko")]
 use values::generics::CounterStyleOrNone;
+use values::generics::counters as generics;
 use values::generics::counters::CounterIncrement as GenericCounterIncrement;
 use values::generics::counters::CounterPair;
 use values::generics::counters::CounterReset as GenericCounterReset;
+use values::specified::Integer;
+use values::specified::url::SpecifiedImageUrl;
 #[cfg(feature = "gecko")]
 use values::specified::Attr;
-use values::specified::Integer;
-#[cfg(feature = "gecko")]
-use values::specified::url::SpecifiedImageUrl;
 
 /// A specified value for the `counter-increment` property.
 pub type CounterIncrement = GenericCounterIncrement<Integer>;
 
 impl Parse for CounterIncrement {
     fn parse<'i, 't>(
         context: &ParserContext,
         input: &mut Parser<'i, 't>,
@@ -74,74 +75,134 @@ fn parse_counters<'i, 't>(
 
     if !counters.is_empty() {
         Ok(counters)
     } else {
         Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
     }
 }
 
-#[cfg(feature = "servo")]
-type CounterStyleType = ListStyleType;
+/// The specified value for the `content` property.
+pub type Content = generics::Content<SpecifiedImageUrl>;
 
-#[cfg(feature = "gecko")]
-type CounterStyleType = CounterStyleOrNone;
+/// The specified value for a content item in the `content` property.
+pub type ContentItem = generics::ContentItem<SpecifiedImageUrl>;
 
-#[cfg(feature = "servo")]
-#[inline]
-fn is_decimal(counter_type: &CounterStyleType) -> bool {
-    *counter_type == ListStyleType::Decimal
-}
+impl Content {
+    #[cfg(feature = "servo")]
+    fn parse_counter_style(_: &ParserContext, input: &mut Parser) -> ListStyleType {
+        input
+            .try(|input| {
+                input.expect_comma()?;
+                ListStyleType::parse(input)
+            })
+            .unwrap_or(ListStyleType::Decimal)
+    }
 
-#[cfg(feature = "gecko")]
-#[inline]
-fn is_decimal(counter_type: &CounterStyleType) -> bool {
-    *counter_type == CounterStyleOrNone::decimal()
+    #[cfg(feature = "gecko")]
+    fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyleOrNone {
+        input
+            .try(|input| {
+                input.expect_comma()?;
+                CounterStyleOrNone::parse(context, input)
+            })
+            .unwrap_or(CounterStyleOrNone::decimal())
+    }
 }
 
-/// The specified value for the `content` property.
-///
-/// https://drafts.csswg.org/css-content/#propdef-content
-#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo,
-         ToComputedValue, ToCss)]
-pub enum Content {
-    /// `normal` reserved keyword.
-    Normal,
-    /// `none` reserved keyword.
-    None,
-    /// `-moz-alt-content`.
-    #[cfg(feature = "gecko")]
-    MozAltContent,
-    /// Content items.
-    Items(#[css(iterable)] Box<[ContentItem]>),
-}
+impl Parse for Content {
+    // normal | none | [ <string> | <counter> | open-quote | close-quote | no-open-quote |
+    // no-close-quote ]+
+    // TODO: <uri>, attr(<identifier>)
+    fn parse<'i, 't>(
+        context: &ParserContext,
+        input: &mut Parser<'i, 't>,
+    ) -> Result<Self, ParseError<'i>> {
+        if input
+            .try(|input| input.expect_ident_matching("normal"))
+            .is_ok()
+        {
+            return Ok(generics::Content::Normal);
+        }
+        if input
+            .try(|input| input.expect_ident_matching("none"))
+            .is_ok()
+        {
+            return Ok(generics::Content::None);
+        }
+        #[cfg(feature = "gecko")]
+        {
+            if input
+                .try(|input| input.expect_ident_matching("-moz-alt-content"))
+                .is_ok()
+            {
+                return Ok(generics::Content::MozAltContent);
+            }
+        }
 
-/// Items for the `content` property.
-#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo,
-         ToComputedValue, ToCss)]
-pub enum ContentItem {
-    /// Literal string content.
-    String(Box<str>),
-    /// `counter(name, style)`.
-    #[css(comma, function)]
-    Counter(CustomIdent, #[css(skip_if = "is_decimal")] CounterStyleType),
-    /// `counters(name, separator, style)`.
-    #[css(comma, function)]
-    Counters(
-        CustomIdent,
-        Box<str>,
-        #[css(skip_if = "is_decimal")] CounterStyleType,
-    ),
-    /// `open-quote`.
-    OpenQuote,
-    /// `close-quote`.
-    CloseQuote,
-    /// `no-open-quote`.
-    NoOpenQuote,
-    /// `no-close-quote`.
-    NoCloseQuote,
-    /// `attr([namespace? `|`]? ident)`
-    #[cfg(feature = "gecko")]
-    Attr(Attr),
-    /// `url(url)`
-    #[cfg(feature = "gecko")]
-    Url(SpecifiedImageUrl),
+        let mut content = vec![];
+        loop {
+            #[cfg(feature = "gecko")]
+            {
+                if let Ok(url) = input.try(|i| SpecifiedImageUrl::parse(context, i)) {
+                    content.push(generics::ContentItem::Url(url));
+                    continue;
+                }
+            }
+            // FIXME: remove clone() when lifetimes are non-lexical
+            match input.next().map(|t| t.clone()) {
+                Ok(Token::QuotedString(ref value)) => {
+                    content.push(generics::ContentItem::String(
+                        value.as_ref().to_owned().into_boxed_str(),
+                    ));
+                },
+                Ok(Token::Function(ref name)) => {
+                    let result = match_ignore_ascii_case! { &name,
+                        "counter" => Some(input.parse_nested_block(|input| {
+                            let location = input.current_source_location();
+                            let name = CustomIdent::from_ident(location, input.expect_ident()?, &[])?;
+                            let style = Content::parse_counter_style(context, input);
+                            Ok(generics::ContentItem::Counter(name, style))
+                        })),
+                        "counters" => Some(input.parse_nested_block(|input| {
+                            let location = input.current_source_location();
+                            let name = CustomIdent::from_ident(location, input.expect_ident()?, &[])?;
+                            input.expect_comma()?;
+                            let separator = input.expect_string()?.as_ref().to_owned().into_boxed_str();
+                            let style = Content::parse_counter_style(context, input);
+                            Ok(generics::ContentItem::Counters(name, separator, style))
+                        })),
+                        #[cfg(feature = "gecko")]
+                        "attr" => Some(input.parse_nested_block(|input| {
+                            Ok(generics::ContentItem::Attr(Attr::parse_function(context, input)?))
+                        })),
+                        _ => None
+                    };
+                    match result {
+                        Some(result) => content.push(result?),
+                        None => {
+                            return Err(input.new_custom_error(
+                                StyleParseErrorKind::UnexpectedFunction(name.clone()),
+                            ))
+                        },
+                    }
+                },
+                Ok(Token::Ident(ref ident)) => {
+                    content.push(match_ignore_ascii_case! { &ident,
+                        "open-quote" => generics::ContentItem::OpenQuote,
+                        "close-quote" => generics::ContentItem::CloseQuote,
+                        "no-open-quote" => generics::ContentItem::NoOpenQuote,
+                        "no-close-quote" => generics::ContentItem::NoCloseQuote,
+                        _ => return Err(input.new_custom_error(
+                            SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+                        ))
+                    });
+                },
+                Err(_) => break,
+                Ok(t) => return Err(input.new_unexpected_token_error(t)),
+            }
+        }
+        if content.is_empty() {
+            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+        }
+        Ok(generics::Content::Items(content.into_boxed_slice()))
+    }
 }
--- a/servo/components/style/values/specified/effects.rs
+++ b/servo/components/style/values/specified/effects.rs
@@ -14,30 +14,29 @@ use values::computed::effects::BoxShadow
 use values::computed::effects::SimpleShadow as ComputedSimpleShadow;
 use values::generics::NonNegative;
 use values::generics::effects::BoxShadow as GenericBoxShadow;
 use values::generics::effects::Filter as GenericFilter;
 use values::generics::effects::SimpleShadow as GenericSimpleShadow;
 use values::specified::{Angle, NumberOrPercentage};
 use values::specified::color::RGBAColor;
 use values::specified::length::{Length, NonNegativeLength};
-#[cfg(feature = "gecko")]
 use values::specified::url::SpecifiedUrl;
 
 /// A specified value for a single shadow of the `box-shadow` property.
 pub type BoxShadow =
     GenericBoxShadow<Option<RGBAColor>, Length, Option<NonNegativeLength>, Option<Length>>;
 
 /// A specified value for a single `filter`.
 #[cfg(feature = "gecko")]
-pub type Filter = GenericFilter<Angle, Factor, NonNegativeLength, SimpleShadow>;
+pub type Filter = GenericFilter<Angle, Factor, NonNegativeLength, SimpleShadow, SpecifiedUrl>;
 
 /// A specified value for a single `filter`.
 #[cfg(not(feature = "gecko"))]
-pub type Filter = GenericFilter<Angle, Factor, NonNegativeLength, Impossible>;
+pub type Filter = GenericFilter<Angle, Factor, NonNegativeLength, Impossible, SpecifiedUrl>;
 
 /// A value for the `<factor>` parts in `Filter`.
 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss)]
 pub struct Factor(NumberOrPercentage);
 
 impl Factor {
     /// Parse this factor but clamp to one if the value is over 100%.
     #[inline]