servo: Merge #17130 - stylo: Handle attr() in `content` (from Manishearth:stylo-attr); r=heycam,emilio
authorManish Goregaokar <manishearth@gmail.com>
Thu, 01 Jun 2017 15:34:55 -0700
changeset 361972 e589f4df590bf79eef4417e3ecd89354ef717dbd
parent 361971 8c962337cfa65cad59f228f062d52bc6271e868b
child 361973 e06f6065c2c306e350f3bc5a4bb4b3e4d761e052
push id90977
push usercbook@mozilla.com
push dateFri, 02 Jun 2017 12:33:01 +0000
treeherdermozilla-inbound@d5cd9b6d1a87 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersheycam, emilio
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
servo: Merge #17130 - stylo: Handle attr() in `content` (from Manishearth:stylo-attr); r=heycam,emilio r=heycam https://bugzilla.mozilla.org/show_bug.cgi?id=1346693 Source-Repo: https://github.com/servo/servo Source-Revision: 373d3b91ddc3560a7de131bd5ca0f5f8030f1d7e
servo/components/style/gecko/generated/bindings.rs
servo/components/style/gecko/selector_parser.rs
servo/components/style/parser.rs
servo/components/style/properties/gecko.mako.rs
servo/components/style/properties/longhand/counters.mako.rs
servo/components/style/servo/selector_parser.rs
servo/components/style/stylesheets.rs
servo/components/style/values/specified/mod.rs
servo/tests/unit/style/parsing/selectors.rs
servo/tests/unit/style/stylesheets.rs
--- a/servo/components/style/gecko/generated/bindings.rs
+++ b/servo/components/style/gecko/generated/bindings.rs
@@ -1395,16 +1395,19 @@ extern "C" {
                                       ident: *const u16,
                                       set_slow_selector: *mut bool) -> bool;
 }
 extern "C" {
     pub fn Gecko_AddPropertyToSet(arg1: nsCSSPropertyIDSetBorrowedMut,
                                   arg2: nsCSSPropertyID);
 }
 extern "C" {
+    pub fn Gecko_RegisterNamespace(ns: *mut nsIAtom) -> i32;
+}
+extern "C" {
     pub fn Gecko_Construct_Default_nsStyleFont(ptr: *mut nsStyleFont,
                                                pres_context:
                                                    RawGeckoPresContextBorrowed);
 }
 extern "C" {
     pub fn Gecko_CopyConstruct_nsStyleFont(ptr: *mut nsStyleFont,
                                            other: *const nsStyleFont);
 }
--- a/servo/components/style/gecko/selector_parser.rs
+++ b/servo/components/style/gecko/selector_parser.rs
@@ -268,21 +268,21 @@ impl<'a> ::selectors::Parser for Selecto
     }
 
     fn parse_pseudo_element(&self, name: Cow<str>) -> Result<PseudoElement, ()> {
         PseudoElement::from_slice(&name, self.in_user_agent_stylesheet())
             .ok_or(())
     }
 
     fn default_namespace(&self) -> Option<Namespace> {
-        self.namespaces.default.clone()
+        self.namespaces.default.clone().as_ref().map(|&(ref ns, _)| ns.clone())
     }
 
     fn namespace_for_prefix(&self, prefix: &Atom) -> Option<Namespace> {
-        self.namespaces.prefixes.get(prefix).cloned()
+        self.namespaces.prefixes.get(prefix).map(|&(ref ns, _)| ns.clone())
     }
 }
 
 impl SelectorImpl {
     #[inline]
     /// Legacy alias for PseudoElement::cascade_type.
     pub fn pseudo_element_cascade_type(pseudo: &PseudoElement) -> PseudoElementCascadeType {
         pseudo.cascade_type()
--- a/servo/components/style/parser.rs
+++ b/servo/components/style/parser.rs
@@ -2,18 +2,19 @@
  * 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/. */
 
 //! The context within which CSS code is parsed.
 
 use context::QuirksMode;
 use cssparser::{Parser, SourcePosition, UnicodeRange};
 use error_reporting::ParseErrorReporter;
+use parking_lot::RwLock;
 use style_traits::OneOrMoreCommaSeparated;
-use stylesheets::{CssRuleType, Origin, UrlExtraData};
+use stylesheets::{CssRuleType, Origin, UrlExtraData, Namespaces};
 
 bitflags! {
     /// The mode to use when parsing values.
     pub flags ParsingMode: u8 {
         /// In CSS, lengths must have units, except for zero values, where the unit can be omitted.
         /// https://www.w3.org/TR/css3-values/#lengths
         const PARSING_MODE_DEFAULT = 0x00,
         /// In SVG, a coordinate or length value without a unit identifier (e.g., "25") is assumed
@@ -76,16 +77,18 @@ pub struct ParserContext<'a> {
     /// The current rule type, if any.
     pub rule_type: Option<CssRuleType>,
     /// Line number offsets for inline stylesheets
     pub line_number_offset: u64,
     /// The mode to use when parsing.
     pub parsing_mode: ParsingMode,
     /// The quirks mode of this stylesheet.
     pub quirks_mode: QuirksMode,
+    /// The list of all namespaces active in the current stylesheet
+    pub namespaces: Option<&'a RwLock<Namespaces>>,
 }
 
 impl<'a> ParserContext<'a> {
     /// Create a parser context.
     pub fn new(stylesheet_origin: Origin,
                url_data: &'a UrlExtraData,
                error_reporter: &'a ParseErrorReporter,
                rule_type: Option<CssRuleType>,
@@ -95,16 +98,17 @@ impl<'a> ParserContext<'a> {
         ParserContext {
             stylesheet_origin: stylesheet_origin,
             url_data: url_data,
             error_reporter: error_reporter,
             rule_type: rule_type,
             line_number_offset: 0u64,
             parsing_mode: parsing_mode,
             quirks_mode: quirks_mode,
+            namespaces: None,
         }
     }
 
     /// Create a parser context for on-the-fly parsing in CSSOM
     pub fn new_for_cssom(url_data: &'a UrlExtraData,
                          error_reporter: &'a ParseErrorReporter,
                          rule_type: Option<CssRuleType>,
                          parsing_mode: ParsingMode,
@@ -120,16 +124,17 @@ impl<'a> ParserContext<'a> {
         ParserContext {
             stylesheet_origin: context.stylesheet_origin,
             url_data: context.url_data,
             error_reporter: context.error_reporter,
             rule_type: rule_type,
             line_number_offset: context.line_number_offset,
             parsing_mode: context.parsing_mode,
             quirks_mode: context.quirks_mode,
+            namespaces: context.namespaces,
         }
     }
 
     /// Create a parser context for inline CSS which accepts additional line offset argument.
     pub fn new_with_line_number_offset(stylesheet_origin: Origin,
                                        url_data: &'a UrlExtraData,
                                        error_reporter: &'a ParseErrorReporter,
                                        line_number_offset: u64,
@@ -139,16 +144,17 @@ impl<'a> ParserContext<'a> {
         ParserContext {
             stylesheet_origin: stylesheet_origin,
             url_data: url_data,
             error_reporter: error_reporter,
             rule_type: None,
             line_number_offset: line_number_offset,
             parsing_mode: parsing_mode,
             quirks_mode: quirks_mode,
+            namespaces: None,
         }
     }
 
     /// Get the rule type, which assumes that one is available.
     pub fn rule_type(&self) -> CssRuleType {
         self.rule_type.expect("Rule type expected, but none was found.")
     }
 }
--- a/servo/components/style/properties/gecko.mako.rs
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -4396,22 +4396,22 @@ clip-path
                         ContentItem::String(value) => {
                             self.gecko.mContents[i].mType = eStyleContentType_String;
                             unsafe {
                                 // NB: we share allocators, so doing this is fine.
                                 *self.gecko.mContents[i].mContent.mString.as_mut() =
                                     as_utf16_and_forget(&value);
                             }
                         }
-                        ContentItem::Attr(ns, val) => {
+                        ContentItem::Attr(attr) => {
                             self.gecko.mContents[i].mType = eStyleContentType_Attr;
-                            let s = if let Some(ns) = ns {
-                                format!("{}|{}", ns, val)
+                            let s = if let Some((_, ns)) = attr.namespace {
+                                format!("{}|{}", ns, attr.attribute)
                             } else {
-                                val
+                                attr.attribute.into()
                             };
                             unsafe {
                                 // NB: we share allocators, so doing this is fine.
                                 *self.gecko.mContents[i].mContent.mString.as_mut() =
                                     as_utf16_and_forget(&s);
                             }
                         }
                         ContentItem::OpenQuote
--- a/servo/components/style/properties/longhand/counters.mako.rs
+++ b/servo/components/style/properties/longhand/counters.mako.rs
@@ -9,16 +9,18 @@
 <%helpers:longhand name="content" boxed="True" animation_value_type="none"
                    spec="https://drafts.csswg.org/css-content/#propdef-content">
     use cssparser::Token;
     use values::computed::ComputedValueAsSpecified;
     #[cfg(feature = "gecko")]
     use values::generics::CounterStyleOrNone;
     #[cfg(feature = "gecko")]
     use values::specified::url::SpecifiedUrl;
+    #[cfg(feature = "gecko")]
+    use values::specified::Attr;
 
     #[cfg(feature = "servo")]
     use super::list_style_type;
 
     pub use self::computed_value::T as SpecifiedValue;
     pub use self::computed_value::ContentItem;
 
     impl ComputedValueAsSpecified for SpecifiedValue {}
@@ -31,16 +33,19 @@
         #[cfg(feature = "gecko")]
         use values::specified::url::SpecifiedUrl;
 
         #[cfg(feature = "servo")]
         type CounterStyleType = super::super::list_style_type::computed_value::T;
         #[cfg(feature = "gecko")]
         type CounterStyleType = ::values::generics::CounterStyleOrNone;
 
+        #[cfg(feature = "gecko")]
+        use values::specified::Attr;
+
         #[derive(Debug, PartialEq, Eq, Clone)]
         #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
         pub enum ContentItem {
             /// Literal string content.
             String(String),
             /// `counter(name, style)`.
             Counter(String, CounterStyleType),
             /// `counters(name, separator, style)`.
@@ -51,17 +56,17 @@
             CloseQuote,
             /// `no-open-quote`.
             NoOpenQuote,
             /// `no-close-quote`.
             NoCloseQuote,
 
             % if product == "gecko":
                 /// `attr([namespace? `|`]? ident)`
-                Attr(Option<String>, String),
+                Attr(Attr),
                 /// `url(url)`
                 Url(SpecifiedUrl),
             % endif
         }
 
         impl ToCss for ContentItem {
             fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
                 match *self {
@@ -85,24 +90,18 @@
                         dest.write_str(")")
                     }
                     ContentItem::OpenQuote => dest.write_str("open-quote"),
                     ContentItem::CloseQuote => dest.write_str("close-quote"),
                     ContentItem::NoOpenQuote => dest.write_str("no-open-quote"),
                     ContentItem::NoCloseQuote => dest.write_str("no-close-quote"),
 
                     % if product == "gecko":
-                        ContentItem::Attr(ref ns, ref attr) => {
-                            dest.write_str("attr(")?;
-                            if let Some(ref ns) = *ns {
-                                cssparser::Token::Ident((&**ns).into()).to_css(dest)?;
-                                dest.write_str("|")?;
-                            }
-                            cssparser::Token::Ident((&**attr).into()).to_css(dest)?;
-                            dest.write_str(")")
+                        ContentItem::Attr(ref attr) => {
+                            attr.to_css(dest)
                         }
                         ContentItem::Url(ref url) => url.to_css(dest),
                     % endif
                 }
             }
         }
 
         #[derive(Debug, PartialEq, Eq, Clone)]
@@ -198,41 +197,17 @@
                             let name = try!(input.expect_ident()).into_owned();
                             try!(input.expect_comma());
                             let separator = try!(input.expect_string()).into_owned();
                             let style = parse_counter_style(context, input);
                             Ok(ContentItem::Counters(name, separator, style))
                         }),
                         % if product == "gecko":
                             "attr" => input.parse_nested_block(|input| {
-                                // Syntax is `[namespace? `|`]? ident`
-                                // no spaces allowed
-                                // FIXME (bug 1346693) we should be checking that
-                                // this is a valid namespace and encoding it as a namespace
-                                // number from the map
-                                let first = input.try(|i| i.expect_ident()).ok().map(|i| i.into_owned());
-                                if let Ok(token) = input.try(|i| i.next_including_whitespace()) {
-                                    match token {
-                                        Token::Delim('|') => {
-                                            // must be followed by an ident
-                                            let tok2 = input.next_including_whitespace()?;
-                                            if let Token::Ident(second) = tok2 {
-                                                return Ok(ContentItem::Attr(first, second.into_owned()))
-                                            } else {
-                                                return Err(())
-                                            }
-                                        }
-                                        _ => return Err(())
-                                    }
-                                }
-                                if let Some(first) = first {
-                                    Ok(ContentItem::Attr(None, first))
-                                } else {
-                                    Err(())
-                                }
+                                Ok(ContentItem::Attr(Attr::parse_function(context, input)?))
                             }),
                         % endif
                         _ => return Err(())
                     }));
                 }
                 Ok(Token::Ident(ident)) => {
                     match_ignore_ascii_case! { &ident,
                         "open-quote" => content.push(ContentItem::OpenQuote),
--- a/servo/components/style/servo/selector_parser.rs
+++ b/servo/components/style/servo/selector_parser.rs
@@ -425,21 +425,21 @@ impl<'a> ::selectors::Parser for Selecto
             },
             _ => return Err(())
         };
 
         Ok(pseudo_element)
     }
 
     fn default_namespace(&self) -> Option<Namespace> {
-        self.namespaces.default.clone()
+        self.namespaces.default.as_ref().map(|&(ref ns, _)| ns.clone())
     }
 
     fn namespace_for_prefix(&self, prefix: &Prefix) -> Option<Namespace> {
-        self.namespaces.prefixes.get(prefix).cloned()
+        self.namespaces.prefixes.get(prefix).map(|&(ref ns, _)| ns.clone())
     }
 }
 
 impl SelectorImpl {
     /// Returns the pseudo-element cascade type of the given `pseudo`.
     #[inline]
     pub fn pseudo_element_cascade_type(pseudo: &PseudoElement) -> PseudoElementCascadeType {
         pseudo.cascade_type()
--- a/servo/components/style/stylesheets.rs
+++ b/servo/components/style/stylesheets.rs
@@ -33,29 +33,30 @@ use properties::{PropertyDeclarationBloc
 use selector_parser::{SelectorImpl, SelectorParser};
 use selectors::parser::SelectorList;
 #[cfg(feature = "servo")]
 use servo_config::prefs::PREFS;
 #[cfg(not(feature = "gecko"))]
 use servo_url::ServoUrl;
 use shared_lock::{SharedRwLock, Locked, ToCssWithGuard, SharedRwLockReadGuard};
 use smallvec::SmallVec;
+use std::{fmt, mem};
 use std::borrow::Borrow;
 use std::cell::Cell;
-use std::fmt;
 use std::mem::align_of;
 use std::os::raw::c_void;
 use std::slice;
 use std::sync::atomic::{AtomicBool, Ordering};
 use str::starts_with_ignore_ascii_case;
 use style_traits::ToCss;
 use stylearc::Arc;
 use stylist::FnvHashMap;
 use supports::SupportsCondition;
 use values::{CustomIdent, KeyframesName};
+use values::specified::NamespaceId;
 use values::specified::url::SpecifiedUrl;
 use viewport::ViewportRule;
 
 
 /// Extra data that the backend may need to resolve url values.
 #[cfg(not(feature = "gecko"))]
 pub type UrlExtraData = ServoUrl;
 
@@ -91,21 +92,23 @@ pub enum Origin {
     /// http://dev.w3.org/csswg/css-cascade/#cascade-origin-author
     Author,
 
     /// http://dev.w3.org/csswg/css-cascade/#cascade-origin-user
     User,
 }
 
 /// A set of namespaces applying to a given stylesheet.
+///
+/// The namespace id is used in gecko
 #[derive(Clone, Default, Debug)]
 #[allow(missing_docs)]
 pub struct Namespaces {
-    pub default: Option<Namespace>,
-    pub prefixes: FnvHashMap<Prefix , Namespace>,
+    pub default: Option<(Namespace, NamespaceId)>,
+    pub prefixes: FnvHashMap<Prefix, (Namespace, NamespaceId)>,
 }
 
 /// Like gecko_bindings::structs::MallocSizeOf, but without the Option<> wrapper. Note that
 /// functions of this type should not be called via do_malloc_size_of(), rather than directly.
 pub type MallocSizeOfFn = unsafe extern "C" fn(ptr: *const c_void) -> usize;
 
 /// Call malloc_size_of on ptr, first checking that the allocation isn't empty.
 pub unsafe fn do_malloc_size_of<T>(malloc_size_of: MallocSizeOfFn, ptr: *const T) -> usize {
@@ -471,34 +474,33 @@ impl CssRule {
     // Returns a parsed CSS rule and the final state of the parser
     #[allow(missing_docs)]
     pub fn parse(css: &str,
                  parent_stylesheet: &Stylesheet,
                  state: Option<State>,
                  loader: Option<&StylesheetLoader>)
                  -> Result<(Self, State), SingleRuleParseError> {
         let error_reporter = NullReporter;
-        let mut namespaces = parent_stylesheet.namespaces.write();
-        let context = ParserContext::new(parent_stylesheet.origin,
-                                         &parent_stylesheet.url_data,
-                                         &error_reporter,
-                                         None,
-                                         PARSING_MODE_DEFAULT,
-                                         parent_stylesheet.quirks_mode);
+        let mut context = ParserContext::new(parent_stylesheet.origin,
+                                             &parent_stylesheet.url_data,
+                                             &error_reporter,
+                                             None,
+                                             PARSING_MODE_DEFAULT,
+                                             parent_stylesheet.quirks_mode);
+        context.namespaces = Some(&parent_stylesheet.namespaces);
         let mut input = Parser::new(css);
 
         // nested rules are in the body state
         let state = state.unwrap_or(State::Body);
         let mut rule_parser = TopLevelRuleParser {
             stylesheet_origin: parent_stylesheet.origin,
             context: context,
             shared_lock: &parent_stylesheet.shared_lock,
             loader: loader,
             state: Cell::new(state),
-            namespaces: &mut namespaces,
         };
         match parse_one_rule(&mut input, &mut rule_parser) {
             Ok(result) => Ok((result, rule_parser.state.get())),
             Err(_) => {
                 if let State::Invalid = rule_parser.state.get() {
                     Err(SingleRuleParseError::Hierarchy)
                 } else {
                     Err(SingleRuleParseError::Syntax)
@@ -1205,52 +1207,54 @@ impl<'a, 'b, C> Iterator for RulesIterat
 impl Stylesheet {
     /// Updates an empty stylesheet from a given string of text.
     pub fn update_from_str(existing: &Stylesheet,
                            css: &str,
                            url_data: &UrlExtraData,
                            stylesheet_loader: Option<&StylesheetLoader>,
                            error_reporter: &ParseErrorReporter,
                            line_number_offset: u64) {
-        let mut namespaces = Namespaces::default();
+        let namespaces = RwLock::new(Namespaces::default());
         // FIXME: we really should update existing.url_data with the given url_data,
         // otherwise newly inserted rule may not have the right base url.
         let (rules, dirty_on_viewport_size_change) = Stylesheet::parse_rules(
-            css, url_data, existing.origin, &mut namespaces,
+            css, url_data, existing.origin, &namespaces,
             &existing.shared_lock, stylesheet_loader, error_reporter,
             existing.quirks_mode, line_number_offset);
-        *existing.namespaces.write() = namespaces;
+        mem::swap(&mut *existing.namespaces.write(), &mut *namespaces.write());
         existing.dirty_on_viewport_size_change
             .store(dirty_on_viewport_size_change, Ordering::Release);
 
         // Acquire the lock *after* parsing, to minimize the exclusive section.
         let mut guard = existing.shared_lock.write();
         *existing.rules.write_with(&mut guard) = CssRules(rules);
     }
 
     fn parse_rules(css: &str,
                    url_data: &UrlExtraData,
                    origin: Origin,
-                   namespaces: &mut Namespaces,
+                   namespaces: &RwLock<Namespaces>,
                    shared_lock: &SharedRwLock,
                    stylesheet_loader: Option<&StylesheetLoader>,
                    error_reporter: &ParseErrorReporter,
                    quirks_mode: QuirksMode,
                    line_number_offset: u64)
                    -> (Vec<CssRule>, bool) {
         let mut rules = Vec::new();
         let mut input = Parser::new(css);
+        let mut context = ParserContext::new_with_line_number_offset(origin, url_data, error_reporter,
+                                                                     line_number_offset,
+                                                                     PARSING_MODE_DEFAULT,
+                                                                     quirks_mode);
+        context.namespaces = Some(namespaces);
         let rule_parser = TopLevelRuleParser {
             stylesheet_origin: origin,
-            namespaces: namespaces,
             shared_lock: shared_lock,
             loader: stylesheet_loader,
-            context: ParserContext::new_with_line_number_offset(origin, url_data, error_reporter,
-                                                                line_number_offset, PARSING_MODE_DEFAULT,
-                                                                quirks_mode),
+            context: context,
             state: Cell::new(State::Start),
         };
 
         input.look_for_viewport_percentages();
 
         {
             let mut iter = RuleListParser::new_for_stylesheet(&mut input, rule_parser);
             while let Some(result) = iter.next() {
@@ -1278,25 +1282,25 @@ impl Stylesheet {
                     origin: Origin,
                     media: Arc<Locked<MediaList>>,
                     shared_lock: SharedRwLock,
                     stylesheet_loader: Option<&StylesheetLoader>,
                     error_reporter: &ParseErrorReporter,
                     quirks_mode: QuirksMode,
                     line_number_offset: u64)
                     -> Stylesheet {
-        let mut namespaces = Namespaces::default();
+        let namespaces = RwLock::new(Namespaces::default());
         let (rules, dirty_on_viewport_size_change) = Stylesheet::parse_rules(
-            css, &url_data, origin, &mut namespaces,
+            css, &url_data, origin, &namespaces,
             &shared_lock, stylesheet_loader, error_reporter, quirks_mode, line_number_offset,
         );
         Stylesheet {
             origin: origin,
             url_data: url_data,
-            namespaces: RwLock::new(namespaces),
+            namespaces: namespaces,
             rules: CssRules::new(rules, &shared_lock),
             media: media,
             shared_lock: shared_lock,
             dirty_on_viewport_size_change: AtomicBool::new(dirty_on_viewport_size_change),
             disabled: AtomicBool::new(false),
             quirks_mode: quirks_mode,
         }
     }
@@ -1465,30 +1469,28 @@ impl StylesheetLoader for NoOpLoader {
     ) -> Arc<Locked<ImportRule>> {
         make_arc(make_import(media))
     }
 }
 
 
 struct TopLevelRuleParser<'a> {
     stylesheet_origin: Origin,
-    namespaces: &'a mut Namespaces,
     shared_lock: &'a SharedRwLock,
     loader: Option<&'a StylesheetLoader>,
     context: ParserContext<'a>,
     state: Cell<State>,
 }
 
 impl<'b> TopLevelRuleParser<'b> {
     fn nested<'a: 'b>(&'a self) -> NestedRuleParser<'a, 'b> {
         NestedRuleParser {
             stylesheet_origin: self.stylesheet_origin,
             shared_lock: self.shared_lock,
             context: &self.context,
-            namespaces: self.namespaces,
         }
     }
 }
 
 #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
 #[allow(missing_docs)]
 pub enum State {
     Start = 1,
@@ -1523,16 +1525,31 @@ enum AtRulePrelude {
     Keyframes(KeyframesName, Option<VendorPrefix>, SourceLocation),
     /// A @page rule prelude.
     Page(SourceLocation),
     /// A @document rule, with its conditional.
     Document(DocumentCondition, SourceLocation),
 }
 
 
+#[cfg(feature = "gecko")]
+fn register_namespace(ns: &Namespace) -> Result<i32, ()> {
+    let id = unsafe { ::gecko_bindings::bindings::Gecko_RegisterNamespace(ns.0.as_ptr()) };
+    if id == -1 {
+        Err(())
+    } else {
+        Ok(id)
+    }
+}
+
+#[cfg(feature = "servo")]
+fn register_namespace(_: &Namespace) -> Result<(), ()> {
+    Ok(()) // servo doesn't use namespace ids
+}
+
 impl<'a> AtRuleParser for TopLevelRuleParser<'a> {
     type Prelude = AtRulePrelude;
     type AtRule = CssRule;
 
     fn parse_prelude(&mut self, name: &str, input: &mut Parser)
                      -> Result<AtRuleType<AtRulePrelude, CssRule>, ()> {
         let location = get_location_with_offset(input.current_source_location(),
                                                 self.context.line_number_offset);
@@ -1581,22 +1598,26 @@ impl<'a> AtRuleParser for TopLevelRulePa
             },
             "namespace" => {
                 if self.state.get() <= State::Namespaces {
                     self.state.set(State::Namespaces);
 
                     let prefix_result = input.try(|input| input.expect_ident());
                     let url = Namespace::from(try!(input.expect_url_or_string()));
 
+                    let id = register_namespace(&url)?;
+
                     let opt_prefix = if let Ok(prefix) = prefix_result {
                         let prefix = Prefix::from(prefix);
-                        self.namespaces.prefixes.insert(prefix.clone(), url.clone());
+                        self.context.namespaces.expect("namespaces must be set whilst parsing rules")
+                                               .write().prefixes.insert(prefix.clone(), (url.clone(), id));
                         Some(prefix)
                     } else {
-                        self.namespaces.default = Some(url.clone());
+                        self.context.namespaces.expect("namespaces must be set whilst parsing rules")
+                                               .write().default = Some((url.clone(), id));
                         None
                     };
 
                     return Ok(AtRuleType::WithoutBlock(CssRule::Namespace(Arc::new(
                         self.shared_lock.wrap(NamespaceRule {
                             prefix: opt_prefix,
                             url: url,
                             source_location: location,
@@ -1645,27 +1666,25 @@ impl<'a> QualifiedRuleParser for TopLeve
     }
 }
 
 #[derive(Clone)]  // shallow, relatively cheap .clone
 struct NestedRuleParser<'a, 'b: 'a> {
     stylesheet_origin: Origin,
     shared_lock: &'a SharedRwLock,
     context: &'a ParserContext<'b>,
-    namespaces: &'b Namespaces,
 }
 
 impl<'a, 'b> NestedRuleParser<'a, 'b> {
     fn parse_nested_rules(&self, input: &mut Parser, rule_type: CssRuleType) -> Arc<Locked<CssRules>> {
         let context = ParserContext::new_with_rule_type(self.context, Some(rule_type));
         let nested_parser = NestedRuleParser {
             stylesheet_origin: self.stylesheet_origin,
             shared_lock: self.shared_lock,
             context: &context,
-            namespaces: self.namespaces,
         };
         let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser);
         let mut rules = Vec::new();
         while let Some(result) = iter.next() {
             match result {
                 Ok(rule) => rules.push(rule),
                 Err(range) => {
                     let pos = range.start;
@@ -1831,19 +1850,20 @@ impl<'a, 'b> AtRuleParser for NestedRule
     }
 }
 
 impl<'a, 'b> QualifiedRuleParser for NestedRuleParser<'a, 'b> {
     type Prelude = SelectorList<SelectorImpl>;
     type QualifiedRule = CssRule;
 
     fn parse_prelude(&mut self, input: &mut Parser) -> Result<SelectorList<SelectorImpl>, ()> {
+        let ns = self.context.namespaces.expect("namespaces must be set when parsing rules").read();
         let selector_parser = SelectorParser {
             stylesheet_origin: self.stylesheet_origin,
-            namespaces: self.namespaces,
+            namespaces: &*ns,
         };
         SelectorList::parse(&selector_parser, input)
     }
 
     fn parse_block(&mut self, prelude: SelectorList<SelectorImpl>, input: &mut Parser)
                    -> Result<CssRule, ()> {
         let location = get_location_with_offset(input.current_source_location(),
                                                 self.context.line_number_offset);
--- a/servo/components/style/values/specified/mod.rs
+++ b/servo/components/style/values/specified/mod.rs
@@ -1,34 +1,36 @@
 /* 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/. */
 
 //! Specified values.
 //!
 //! TODO(emilio): Enhance docs.
 
+use Namespace;
 use app_units::Au;
 use context::QuirksMode;
-use cssparser::{self, Parser, Token};
+use cssparser::{self, Parser, Token, serialize_identifier};
 use itoa;
 use parser::{ParserContext, Parse};
 use self::grid::TrackSizeOrRepeat;
 use self::url::SpecifiedUrl;
 use std::ascii::AsciiExt;
 use std::f32;
 use std::fmt;
 use std::io::Write;
 use style_traits::ToCss;
 use style_traits::values::specified::AllowedNumericType;
 use super::{Auto, CSSFloat, CSSInteger, Either, None_};
 use super::computed::{self, Context};
 use super::computed::{Shadow as ComputedShadow, ToComputedValue};
 use super::generics::grid::{TrackBreadth as GenericTrackBreadth, TrackSize as GenericTrackSize};
 use super::generics::grid::TrackList as GenericTrackList;
+use values::computed::ComputedValueAsSpecified;
 use values::specified::calc::CalcNode;
 
 #[cfg(feature = "gecko")]
 pub use self::align::{AlignItems, AlignJustifyContent, AlignJustifySelf, JustifyItems};
 pub use self::background::BackgroundSize;
 pub use self::border::{BorderCornerRadius, BorderImageSlice, BorderImageWidth};
 pub use self::border::{BorderImageWidthSide, BorderRadius};
 pub use self::color::Color;
@@ -1358,8 +1360,113 @@ pub enum AllowQuirks {
 }
 
 impl AllowQuirks {
     /// Returns `true` if quirks are allowed in this context.
     pub fn allowed(self, quirks_mode: QuirksMode) -> bool {
         self == AllowQuirks::Yes && quirks_mode == QuirksMode::Quirks
     }
 }
+
+#[cfg(feature = "gecko")]
+/// A namespace ID
+pub type NamespaceId = i32;
+
+
+#[cfg(feature = "servo")]
+/// A namespace ID (used by gecko only)
+pub type NamespaceId = ();
+
+/// An attr(...) rule
+///
+/// `[namespace? `|`]? ident`
+#[derive(Clone, PartialEq, Eq, Debug)]
+#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
+pub struct Attr {
+    /// Optional namespace
+    pub namespace: Option<(Namespace, NamespaceId)>,
+    /// Attribute name
+    pub attribute: String,
+}
+
+impl Parse for Attr {
+    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Attr, ()> {
+        input.expect_function_matching("attr")?;
+        input.parse_nested_block(|i| Attr::parse_function(context, i))
+    }
+}
+
+#[cfg(feature = "gecko")]
+/// Get the namespace id from the namespace map
+pub fn get_id_for_namespace(namespace: &Namespace, context: &ParserContext) -> Result<NamespaceId, ()> {
+    if let Some(map) = context.namespaces {
+        if let Some(ref entry) = map.read().prefixes.get(&namespace.0) {
+            Ok(entry.1)
+        } else {
+            Err(())
+        }
+    } else {
+        // if we don't have a namespace map (e.g. in inline styles)
+        // we can't parse namespaces
+        Err(())
+    }
+}
+
+#[cfg(feature = "servo")]
+/// Get the namespace id from the namespace map
+pub fn get_id_for_namespace(_: &Namespace, _: &ParserContext) -> Result<NamespaceId, ()> {
+    Ok(())
+}
+
+impl Attr {
+    /// Parse contents of attr() assuming we have already parsed `attr` and are
+    /// within a parse_nested_block()
+    pub fn parse_function(context: &ParserContext, input: &mut Parser) -> Result<Attr, ()> {
+        // Syntax is `[namespace? `|`]? ident`
+        // no spaces allowed
+        let first = input.try(|i| i.expect_ident()).ok();
+        if let Ok(token) = input.try(|i| i.next_including_whitespace()) {
+            match token {
+                Token::Delim('|') => {
+                    // must be followed by an ident
+                    let second_token = match input.next_including_whitespace()? {
+                        Token::Ident(second) => second,
+                        _ => return Err(()),
+                    };
+                    let ns_with_id = if let Some(ns) = first {
+                        let ns: Namespace = ns.into();
+                        let id = get_id_for_namespace(&ns, context)?;
+                        Some((ns, id))
+                    } else {
+                        None
+                    };
+                    return Ok(Attr {
+                        namespace: ns_with_id,
+                        attribute: second_token.into_owned(),
+                    })
+                }
+                _ => return Err(())
+            }
+        }
+        if let Some(first) = first {
+            Ok(Attr {
+                namespace: None,
+                attribute: first.into_owned(),
+            })
+        } else {
+            Err(())
+        }
+    }
+}
+
+impl ToCss for Attr {
+    fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
+        dest.write_str("attr(")?;
+        if let Some(ref ns) = self.namespace {
+            serialize_identifier(&ns.0.to_string(), dest)?;
+            dest.write_str("|")?;
+        }
+        serialize_identifier(&self.attribute, dest)?;
+        dest.write_str(")")
+    }
+}
+
+impl ComputedValueAsSpecified for Attr {}
--- a/servo/tests/unit/style/parsing/selectors.rs
+++ b/servo/tests/unit/style/parsing/selectors.rs
@@ -4,17 +4,17 @@
 
 use cssparser::{Parser, ToCss};
 use selectors::parser::SelectorList;
 use style::selector_parser::{SelectorImpl, SelectorParser};
 use style::stylesheets::{Origin, Namespaces};
 
 fn parse_selector(input: &mut Parser) -> Result<SelectorList<SelectorImpl>, ()> {
     let mut ns = Namespaces::default();
-    ns.prefixes.insert("svg".into(), ns!(svg));
+    ns.prefixes.insert("svg".into(), (ns!(svg), ()));
     let parser = SelectorParser {
         stylesheet_origin: Origin::UserAgent,
         namespaces: &ns,
     };
     SelectorList::parse(&parser, input)
 }
 
 #[test]
--- a/servo/tests/unit/style/stylesheets.rs
+++ b/servo/tests/unit/style/stylesheets.rs
@@ -65,17 +65,17 @@ fn test_parse_stylesheet() {
             }
         }";
     let url = ServoUrl::parse("about::test").unwrap();
     let lock = SharedRwLock::new();
     let media = Arc::new(lock.wrap(MediaList::empty()));
     let stylesheet = Stylesheet::from_str(css, url.clone(), Origin::UserAgent, media, lock,
                                           None, &CSSErrorReporterTest, QuirksMode::NoQuirks, 0u64);
     let mut namespaces = Namespaces::default();
-    namespaces.default = Some(ns!(html));
+    namespaces.default = Some((ns!(html), ()));
     let expected = Stylesheet {
         origin: Origin::UserAgent,
         media: Arc::new(stylesheet.shared_lock.wrap(MediaList::empty())),
         shared_lock: stylesheet.shared_lock.clone(),
         namespaces: RwLock::new(namespaces),
         url_data: url,
         dirty_on_viewport_size_change: AtomicBool::new(false),
         disabled: AtomicBool::new(false),