Bug 549861. Implement support for @font-feature-values rule. r=dbaron
authorJohn Daggett <jdaggett@mozilla.com>
Mon, 13 May 2013 18:45:37 +0900
changeset 131737 2a664a9431799f250a2e6d479d665a5e6a9d9601
parent 131736 931349bb42e3dfd71b5cff70de01a08c6c28d8b9
child 131738 414ebe5595c1364258e6bd8223a5dea69d373065
push id27953
push userjdaggett@mozilla.com
push dateMon, 13 May 2013 09:46:45 +0000
treeherdermozilla-inbound@7ecd4e3863b4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdbaron
bugs549861
milestone23.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
Bug 549861. Implement support for @font-feature-values rule. r=dbaron
dom/base/nsDOMClassInfo.cpp
dom/base/nsDOMClassInfoClasses.h
dom/interfaces/base/domstubs.idl
dom/interfaces/css/moz.build
dom/interfaces/css/nsIDOMCSSFontFeatureValuesRule.idl
dom/interfaces/css/nsIDOMCSSRule.idl
dom/locales/en-US/chrome/layout/css.properties
dom/tests/mochitest/general/test_interfaces.html
gfx/src/nsFont.cpp
gfx/src/nsFont.h
layout/style/Rule.h
layout/style/nsCSSParser.cpp
layout/style/nsCSSRuleProcessor.cpp
layout/style/nsCSSRuleProcessor.h
layout/style/nsCSSRules.cpp
layout/style/nsCSSRules.h
layout/style/nsStyleSet.cpp
layout/style/nsStyleSet.h
layout/style/test/Makefile.in
layout/style/test/test_font_feature_values_parsing.html
--- a/dom/base/nsDOMClassInfo.cpp
+++ b/dom/base/nsDOMClassInfo.cpp
@@ -908,16 +908,18 @@ static nsDOMClassInfoData sClassInfoData
 
   NS_DEFINE_CLASSINFO_DATA(OpenWindowEventDetail, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
   NS_DEFINE_CLASSINFO_DATA(AsyncScrollEventDetail, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
 
   NS_DEFINE_CLASSINFO_DATA(LockedFile, nsEventTargetSH,
                            EVENTTARGET_SCRIPTABLE_FLAGS)
+  NS_DEFINE_CLASSINFO_DATA(CSSFontFeatureValuesRule, nsDOMGenericSH,
+                           DOM_DEFAULT_SCRIPTABLE_FLAGS)
 
 #ifdef MOZ_TIME_MANAGER
   NS_DEFINE_CLASSINFO_DATA(MozTimeManager, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
 #endif
 
 #ifdef MOZ_WEBRTC
   NS_DEFINE_CLASSINFO_DATA(DataChannel, nsEventTargetSH,
@@ -2299,16 +2301,20 @@ nsDOMClassInfo::Init()
   DOM_CLASSINFO_MAP_BEGIN(AsyncScrollEventDetail, nsIAsyncScrollEventDetail)
     DOM_CLASSINFO_MAP_ENTRY(nsIAsyncScrollEventDetail)
   DOM_CLASSINFO_MAP_END
 
   DOM_CLASSINFO_MAP_BEGIN(LockedFile, nsIDOMLockedFile)
     DOM_CLASSINFO_MAP_ENTRY(nsIDOMLockedFile)
   DOM_CLASSINFO_MAP_END
 
+  DOM_CLASSINFO_MAP_BEGIN(CSSFontFeatureValuesRule, nsIDOMCSSFontFeatureValuesRule)
+    DOM_CLASSINFO_MAP_ENTRY(nsIDOMCSSFontFeatureValuesRule)
+  DOM_CLASSINFO_MAP_END
+
 #ifdef MOZ_TIME_MANAGER
   DOM_CLASSINFO_MAP_BEGIN(MozTimeManager, nsIDOMMozTimeManager)
     DOM_CLASSINFO_MAP_ENTRY(nsIDOMMozTimeManager)
   DOM_CLASSINFO_MAP_END
 #endif
 
 #ifdef MOZ_WEBRTC
   DOM_CLASSINFO_MAP_BEGIN(DataChannel, nsIDOMDataChannel)
--- a/dom/base/nsDOMClassInfoClasses.h
+++ b/dom/base/nsDOMClassInfoClasses.h
@@ -232,16 +232,18 @@ DOMCI_CLASS(CameraControl)
 DOMCI_CLASS(CameraCapabilities)
 
 DOMCI_CLASS(DOMError)
 DOMCI_CLASS(OpenWindowEventDetail)
 DOMCI_CLASS(AsyncScrollEventDetail)
 
 DOMCI_CLASS(LockedFile)
 
+DOMCI_CLASS(CSSFontFeatureValuesRule)
+
 #ifdef MOZ_TIME_MANAGER
 DOMCI_CLASS(MozTimeManager)
 #endif
 
 #ifdef MOZ_WEBRTC
 DOMCI_CLASS(DataChannel)
 DOMCI_CLASS(RTCPeerConnection)
 #endif
--- a/dom/interfaces/base/domstubs.idl
+++ b/dom/interfaces/base/domstubs.idl
@@ -64,16 +64,17 @@ interface nsIDOMHTMLCollection;
 interface nsIDOMHTMLHeadElement;
 
 // CSS
 interface nsIDOMCSSValue;
 interface nsIDOMCSSPrimitiveValue;
 interface nsIDOMCSSRule;
 interface nsIDOMCSSRuleList;
 interface nsIDOMMozCSSKeyframeRule;
+interface nsIDOMCSSFontFeatureValuesRule;
 interface nsIDOMCSSStyleSheet;
 interface nsIDOMCSSStyleDeclaration;
 interface nsIDOMCounter;
 interface nsIDOMRect;
 interface nsIDOMCSSStyleRule;
 interface nsIDOMCSSStyleRuleCollection;
 interface nsIDOMHTMLTableCaptionElement;
 interface nsIDOMHTMLTableSectionElement;
--- a/dom/interfaces/css/moz.build
+++ b/dom/interfaces/css/moz.build
@@ -3,16 +3,17 @@
 # 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/.
 
 XPIDL_SOURCES += [
     'nsIDOMCSSCharsetRule.idl',
     'nsIDOMCSSConditionRule.idl',
     'nsIDOMCSSFontFaceRule.idl',
+    'nsIDOMCSSFontFeatureValuesRule.idl',
     'nsIDOMCSSGroupingRule.idl',
     'nsIDOMCSSImportRule.idl',
     'nsIDOMCSSMediaRule.idl',
     'nsIDOMCSSMozDocumentRule.idl',
     'nsIDOMCSSPageRule.idl',
     'nsIDOMCSSPrimitiveValue.idl',
     'nsIDOMCSSRule.idl',
     'nsIDOMCSSRuleList.idl',
new file mode 100644
--- /dev/null
+++ b/dom/interfaces/css/nsIDOMCSSFontFeatureValuesRule.idl
@@ -0,0 +1,48 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2004
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   John Daggett <jdaggett@mozilla.com> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "nsIDOMCSSRule.idl"
+
+[scriptable, uuid(f4cb1776-389d-4f52-a4d8-68bea5bd00c1)]
+interface nsIDOMCSSFontFeatureValuesRule : nsIDOMCSSRule
+{
+  attribute DOMString fontFamily;
+                      // raises(DOMException) on setting
+
+  attribute DOMString valueText;
+                      // raises(DOMException) on setting
+};
--- a/dom/interfaces/css/nsIDOMCSSRule.idl
+++ b/dom/interfaces/css/nsIDOMCSSRule.idl
@@ -27,16 +27,17 @@ interface nsIDOMCSSRule : nsISupports
   const unsigned short      KEYFRAMES_RULE                 = 7;
   const unsigned short      KEYFRAME_RULE                  = 8;
   // When layout.css.prefixes.animations is disabled/removed,
   // we should remove these two MOZ_* constants.
   const unsigned short      MOZ_KEYFRAMES_RULE             = 7;
   const unsigned short      MOZ_KEYFRAME_RULE              = 8;
   const unsigned short      NAMESPACE_RULE                 = 10;
   const unsigned short      SUPPORTS_RULE                  = 12;
+  const unsigned short      FONT_FEATURE_VALUES_RULE       = 14;
 
   readonly attribute unsigned short      type;
            attribute DOMString           cssText;
                                         // raises(DOMException) on setting
 
   readonly attribute nsIDOMCSSStyleSheet parentStyleSheet;
   readonly attribute nsIDOMCSSRule       parentRule;
 };
--- a/dom/locales/en-US/chrome/layout/css.properties
+++ b/dom/locales/en-US/chrome/layout/css.properties
@@ -110,16 +110,27 @@ PEMQExpectedExpressionStart=Expected '('
 PEMQExpressionEOF=contents of media query expression
 PEMQExpectedFeatureName=Expected media feature name but found '%1$S'.
 PEMQExpectedFeatureNameEnd=Expected ':' or ')' after media feature name but found '%1$S'.
 PEMQNoMinMaxWithoutValue=Media features with min- or max- must have a value.
 PEMQExpectedFeatureValue=Found invalid value for media feature.
 PEBadFontBlockStart=Expected '{' to begin @font-face rule but found '%1$S'.
 PEBadFontBlockEnd=Expected '}' to end @font-face rule but found '%1$S'.
 PEAnonBoxNotAlone=Did not expect anonymous box.
+PEFFVUnexpectedEOF=Unexpected end of @font-feature-values rule.
+PEFFVBlockStart=Expected opening { of @font-feature-values rule but found '%1$S'.
+PEFFVValueSetStart=Expected opening { of feature value set but found '%1$S'.
+PEFFVNoFamily=Expected font family list for @font-feature-values rule but found '%1$S'.
+PEFFVUnexpectedBlockEnd=Expected '}' to end @font-feature-values rule but found '%1$S'.
+PEFFVUnknownFontVariantPropValue=Unknown font-variant property value '%1$S'.
+PEFFVExpectedIdent=Expected identifier but found '%1$S'.
+PEFFVExpectedValue=Expected non-negative integer value but found '%1$S'.
+PEFFVTooManyValues=Too many values for feature type '%1$S'.
+PEFFVGenericInFamilyList=Family list cannot contain generic font family name.
+PEFFVValueDefinitionTrailing=Expected end of value definition but found '%1$S'.
 PEBadDirValue=Expected 'ltr' or 'rtl' in direction selector but found '%1$S'.
 PESupportsConditionStartEOF2='not', '(', or function
 PESupportsConditionInParensEOF=')'
 PESupportsConditionNotEOF='not'
 PESupportsWhitespaceRequired=Expected whitespace after 'not', 'and', or 'or'.
 PESupportsConditionExpectedOpenParenOrFunction=Expected '(' or function while parsing supports condition but found '%1$S'.
 PESupportsConditionExpectedCloseParen=Expected ')' while parsing supports condition but found '%1$S'.
 PESupportsConditionExpectedStart2=Expected 'not', '(', or function while parsing supports condition but found '%1$S'.
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -487,16 +487,17 @@ var interfaceNamesInGlobalScope =
     "BeforeUnloadEvent",
     "NSRGBAColor",
     "MozBrowserFrame",
     "SVGPreserveAspectRatio",
     "HTMLMenuElement",
     "CloseEvent",
     "IDBCursorWithValue",
     "CSSFontFaceRule",
+    "CSSFontFeatureValuesRule",
     "XMLHttpRequestEventTarget",
     "CompositionEvent",
     "HTMLOutputElement",
     "HTMLFormElement",
     "SVGLength",
     "SVGFilterElement",
     "HTMLScriptElement",
     "SVGPathSegCurvetoCubicAbs",
--- a/gfx/src/nsFont.cpp
+++ b/gfx/src/nsFont.cpp
@@ -30,17 +30,17 @@ nsFont::nsFont(const char* aName, uint8_
   variantAlternates = 0;
   variantCaps = NS_FONT_VARIANT_CAPS_NORMAL;
   variantEastAsian = 0;
   variantLigatures = 0;
   variantNumeric = 0;
   variantPosition = NS_FONT_VARIANT_POSITION_NORMAL;
 }
 
-nsFont::nsFont(const nsString& aName, uint8_t aStyle, uint8_t aVariant,
+nsFont::nsFont(const nsSubstring& aName, uint8_t aStyle, uint8_t aVariant,
                uint16_t aWeight, int16_t aStretch, uint8_t aDecoration,
                nscoord aSize)
   : name(aName)
 {
   style = aStyle;
   systemFont = false;
   variant = aVariant;
   weight = aWeight;
--- a/gfx/src/nsFont.h
+++ b/gfx/src/nsFont.h
@@ -108,17 +108,17 @@ struct NS_GFX nsFont {
   uint8_t synthesis;
 
   // Initialize the font struct with an ASCII name
   nsFont(const char* aName, uint8_t aStyle, uint8_t aVariant,
          uint16_t aWeight, int16_t aStretch, uint8_t aDecoration,
          nscoord aSize);
 
   // Initialize the font struct with a (potentially) unicode name
-  nsFont(const nsString& aName, uint8_t aStyle, uint8_t aVariant,
+  nsFont(const nsSubstring& aName, uint8_t aStyle, uint8_t aVariant,
          uint16_t aWeight, int16_t aStretch, uint8_t aDecoration,
          nscoord aSize);
 
   // Make a copy of the given font
   nsFont(const nsFont& aFont);
 
   nsFont();
   ~nsFont();
--- a/layout/style/Rule.h
+++ b/layout/style/Rule.h
@@ -60,17 +60,18 @@ public:
     NAMESPACE_RULE,
     STYLE_RULE,
     MEDIA_RULE,
     FONT_FACE_RULE,
     PAGE_RULE,
     KEYFRAME_RULE,
     KEYFRAMES_RULE,
     DOCUMENT_RULE,
-    SUPPORTS_RULE
+    SUPPORTS_RULE,
+    FONT_FEATURE_VALUES_RULE
   };
 
   virtual int32_t GetType() const = 0;
 
   nsCSSStyleSheet* GetStyleSheet() const;
   nsHTMLCSSStyleSheet* GetHTMLCSSStyleSheet() const;
 
   // Return the document the rule lives in, if any
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -370,16 +370,19 @@ protected:
   bool ParseMediaRule(RuleAppendFunc aAppendFunc, void* aProcessData);
   bool ParseMozDocumentRule(RuleAppendFunc aAppendFunc, void* aProcessData);
   bool ParseNameSpaceRule(RuleAppendFunc aAppendFunc, void* aProcessData);
   void ProcessNameSpace(const nsString& aPrefix,
                         const nsString& aURLSpec, RuleAppendFunc aAppendFunc,
                         void* aProcessData);
 
   bool ParseFontFaceRule(RuleAppendFunc aAppendFunc, void* aProcessData);
+  bool ParseFontFeatureValuesRule(RuleAppendFunc aAppendFunc,
+                                  void* aProcessData);
+  bool ParseFontFeatureValueSet(nsCSSFontFeatureValuesRule *aRule);
   bool ParseFontDescriptor(nsCSSFontFaceRule* aRule);
   bool ParseFontDescriptorValue(nsCSSFontDesc aDescID,
                                 nsCSSValue& aValue);
 
   bool ParsePageRule(RuleAppendFunc aAppendFunc, void* aProcessData);
   bool ParseKeyframesRule(RuleAppendFunc aAppendFunc, void* aProcessData);
   already_AddRefed<nsCSSKeyframeRule> ParseKeyframeRule();
   bool ParseKeyframeSelectorList(InfallibleTArray<float>& aSelectorList);
@@ -1630,16 +1633,21 @@ CSSParserImpl::ParseAtRule(RuleAppendFun
   } else if (mToken.mIdent.LowerCaseEqualsLiteral("-moz-document")) {
     parseFunc = &CSSParserImpl::ParseMozDocumentRule;
     newSection = eCSSSection_General;
 
   } else if (mToken.mIdent.LowerCaseEqualsLiteral("font-face")) {
     parseFunc = &CSSParserImpl::ParseFontFaceRule;
     newSection = eCSSSection_General;
 
+  } else if (mToken.mIdent.LowerCaseEqualsLiteral("font-feature-values") &&
+             nsCSSFontFeatureValuesRule::PrefEnabled()) {
+    parseFunc = &CSSParserImpl::ParseFontFeatureValuesRule;
+    newSection = eCSSSection_General;
+
   } else if (mToken.mIdent.LowerCaseEqualsLiteral("page")) {
     parseFunc = &CSSParserImpl::ParsePageRule;
     newSection = eCSSSection_General;
 
   } else if ((nsCSSProps::IsEnabled(eCSSPropertyAlias_MozAnimation) &&
               mToken.mIdent.LowerCaseEqualsLiteral("-moz-keyframes")) ||
              mToken.mIdent.LowerCaseEqualsLiteral("keyframes")) {
     parseFunc = &CSSParserImpl::ParseKeyframesRule;
@@ -2355,16 +2363,243 @@ CSSParserImpl::ParseFontDescriptor(nsCSS
 
   if (!ExpectEndProperty())
     return false;
 
   aRule->SetDesc(descID, value);
   return true;
 }
 
+// @font-feature-values <font-family># {
+//   @<feature-type> {
+//     <feature-ident> : <feature-index>+;
+//     <feature-ident> : <feature-index>+;
+//     ...
+//   }
+//   ...
+// }
+
+bool
+CSSParserImpl::ParseFontFeatureValuesRule(RuleAppendFunc aAppendFunc,
+                                          void* aData)
+{
+  nsRefPtr<nsCSSFontFeatureValuesRule>
+               valuesRule(new nsCSSFontFeatureValuesRule());
+
+  // parse family list
+  nsCSSValue familyValue;
+
+  if (!ParseFamily(familyValue) ||
+      familyValue.GetUnit() != eCSSUnit_Families)
+  {
+    REPORT_UNEXPECTED_TOKEN(PEFFVNoFamily);
+    return false;
+  }
+
+  // add family to rule
+  nsAutoString familyList;
+  bool hasGeneric;
+  familyValue.GetStringValue(familyList);
+  valuesRule->SetFamilyList(familyList, hasGeneric);
+
+  // family list has generic ==> parse error
+  if (hasGeneric) {
+    REPORT_UNEXPECTED_TOKEN(PEFFVGenericInFamilyList);
+    return false;
+  }
+
+  // open brace
+  if (!ExpectSymbol('{', true)) {
+    REPORT_UNEXPECTED_TOKEN(PEFFVBlockStart);
+    return false;
+  }
+
+  // list of sets of feature values, each set bound to a specific
+  // feature-type (e.g. swash, annotation)
+  for (;;) {
+    if (!GetToken(true)) {
+      REPORT_UNEXPECTED_EOF(PEFFVUnexpectedEOF);
+      break;
+    }
+    if (mToken.IsSymbol('}')) { // done!
+      UngetToken();
+      break;
+    }
+
+    if (!ParseFontFeatureValueSet(valuesRule)) {
+      if (!SkipAtRule(false)) {
+        break;
+      }
+    }
+  }
+  if (!ExpectSymbol('}', true)) {
+    REPORT_UNEXPECTED_TOKEN(PEFFVUnexpectedBlockEnd);
+    SkipUntil('}');
+    return false;
+  }
+
+  (*aAppendFunc)(valuesRule, aData);
+  return true;
+}
+
+#define NUMVALUES_NO_LIMIT  0xFFFF
+
+// parse a single value set containing name-value pairs for a single feature type
+//   @<feature-type> { [ <feature-ident> : <feature-index>+ ; ]* }
+//   Ex: @swash { flowing: 1; delicate: 2; }
+bool
+CSSParserImpl::ParseFontFeatureValueSet(nsCSSFontFeatureValuesRule
+                                                            *aFeatureValuesRule)
+{
+  // -- @keyword (e.g. swash, styleset)
+  if (eCSSToken_AtKeyword != mToken.mType) {
+    REPORT_UNEXPECTED_TOKEN(PEFontFeatureValuesNoAt);
+    OUTPUT_ERROR();
+    UngetToken();
+    return false;
+  }
+
+  // which font-specific variant of font-variant-alternates
+  int32_t whichVariant;
+  nsCSSKeyword keyword = nsCSSKeywords::LookupKeyword(mToken.mIdent);
+  if (keyword == eCSSKeyword_UNKNOWN ||
+      !nsCSSProps::FindKeyword(keyword,
+                               nsCSSProps::kFontVariantAlternatesFuncsKTable,
+                               whichVariant))
+  {
+    if (!NonMozillaVendorIdentifier(mToken.mIdent)) {
+      REPORT_UNEXPECTED_TOKEN(PEFFVUnknownFontVariantPropValue);
+      OUTPUT_ERROR();
+    }
+    UngetToken();
+    return false;
+  }
+
+  nsAutoString featureType(mToken.mIdent);
+
+  // open brace
+  if (!ExpectSymbol('{', true)) {
+    REPORT_UNEXPECTED_TOKEN(PEFFVValueSetStart);
+    return false;
+  }
+
+  // styleset and character-variant can be multi-valued, otherwise single value
+  int32_t limitNumValues;
+
+  switch (keyword) {
+    case eCSSKeyword_styleset:
+      limitNumValues = NUMVALUES_NO_LIMIT;
+      break;
+    case eCSSKeyword_character_variant:
+      limitNumValues = 2;
+      break;
+    default:
+      limitNumValues = 1;
+      break;
+  }
+
+  // -- ident integer+ [, ident integer+]
+  nsAutoTArray<gfxFontFeatureValueSet::ValueList, 5> values;
+
+  // list of font-feature-values-declaration's
+  for (;;) {
+    nsAutoString valueId;
+
+    if (!GetToken(true)) {
+      REPORT_UNEXPECTED_EOF(PEFFVUnexpectedEOF);
+      break;
+    }
+
+    // ignore extra semicolons
+    if (mToken.IsSymbol(';')) {
+      continue;
+    }
+
+    // close brace ==> done
+    if (mToken.IsSymbol('}')) {
+      break;
+    }
+
+    // ident
+    if (eCSSToken_Ident != mToken.mType) {
+      REPORT_UNEXPECTED_TOKEN(PEFFVExpectedIdent);
+      if (!SkipDeclaration(true)) {
+        break;
+      }
+      continue;
+    }
+
+    valueId.Assign(mToken.mIdent);
+
+    // colon
+    if (!ExpectSymbol(':', true)) {
+      REPORT_UNEXPECTED_TOKEN(PEParseDeclarationNoColon);
+      OUTPUT_ERROR();
+      if (!SkipDeclaration(true)) {
+        break;
+      }
+      continue;
+    }
+
+    // value list
+    nsAutoTArray<uint32_t,4>   featureSelectors;
+
+    nsCSSValue intValue;
+    while (ParseNonNegativeVariant(intValue, VARIANT_INTEGER, nullptr)) {
+      featureSelectors.AppendElement(uint32_t(intValue.GetIntValue()));
+    }
+
+    int32_t numValues = featureSelectors.Length();
+
+    if (numValues == 0) {
+      REPORT_UNEXPECTED_TOKEN(PEFFVExpectedValue);
+      OUTPUT_ERROR();
+      if (!SkipDeclaration(true)) {
+        break;
+      }
+      continue;
+    }
+
+    if (numValues > limitNumValues) {
+      REPORT_UNEXPECTED_P(PEFFVTooManyValues, featureType);
+      OUTPUT_ERROR();
+      if (!SkipDeclaration(true)) {
+        break;
+      }
+      continue;
+    }
+
+    if (!GetToken(true)) {
+      REPORT_UNEXPECTED_EOF(PEFFVUnexpectedEOF);
+      gfxFontFeatureValueSet::ValueList v(valueId, featureSelectors);
+      values.AppendElement(v);
+      break;
+    }
+
+    // ';' or '}' to end definition
+    if (!mToken.IsSymbol(';') && !mToken.IsSymbol('}')) {
+      REPORT_UNEXPECTED_TOKEN(PEFFVValueDefinitionTrailing);
+      OUTPUT_ERROR();
+      if (!SkipDeclaration(true)) {
+        break;
+      }
+      continue;
+    }
+
+    gfxFontFeatureValueSet::ValueList v(valueId, featureSelectors);
+    values.AppendElement(v);
+
+    if (mToken.IsSymbol('}')) {
+      break;
+    }
+ }
+
+  aFeatureValuesRule->AddValueList(whichVariant, values);
+  return true;
+}
 
 bool
 CSSParserImpl::ParseKeyframesRule(RuleAppendFunc aAppendFunc, void* aData)
 {
   if (!GetToken(true)) {
     REPORT_UNEXPECTED_EOF(PEKeyframeNameEOF);
     return false;
   }
@@ -8007,17 +8242,17 @@ CSSParserImpl::ParseCalcTerm(nsCSSValue&
       SkipUntil(')');
       return false;
     }
     return true;
   }
   // ... or just a value
   UngetToken();
   // Always pass VARIANT_NUMBER to ParseVariant so that unitless zero
-  // always gets picked up 
+  // always gets picked up
   if (!ParseVariant(aValue, aVariantMask | VARIANT_NUMBER, nullptr)) {
     return false;
   }
   // ...and do the VARIANT_NUMBER check ourselves.
   if (!(aVariantMask & VARIANT_NUMBER) && aValue.GetUnit() == eCSSUnit_Number) {
     return false;
   }
   // If we did the value parsing, we need to adjust aVariantMask to
--- a/layout/style/nsCSSRuleProcessor.cpp
+++ b/layout/style/nsCSSRuleProcessor.cpp
@@ -644,17 +644,17 @@ void RuleHash::AppendRule(const RuleSele
   else if (selector->mLowercaseTag) {
     RuleValue ruleValue(aRuleInfo, mRuleCount++, mQuirksMode);
     if (!mTagTable.ops) {
       PL_DHashTableInit(&mTagTable, &RuleHash_TagTable_Ops, nullptr,
                         sizeof(RuleHashTagTableEntry), 16);
     }
     AppendRuleToTagTable(&mTagTable, selector->mLowercaseTag, ruleValue);
     RULE_HASH_STAT_INCREMENT(mTagSelectors);
-    if (selector->mCasedTag && 
+    if (selector->mCasedTag &&
         selector->mCasedTag != selector->mLowercaseTag) {
       AppendRuleToTagTable(&mTagTable, selector->mCasedTag, ruleValue);
       RULE_HASH_STAT_INCREMENT(mTagSelectors);
     }
   }
   else if (kNameSpaceID_Unknown != selector->mNameSpace) {
     if (!mNameSpaceTable.ops) {
       PL_DHashTableInit(&mNameSpaceTable, &RuleHash_NameSpaceTable_Ops, nullptr,
@@ -973,16 +973,17 @@ struct RuleCascadeData {
   PLDHashTable             mAttributeSelectors;
   PLDHashTable             mAnonBoxRules;
 #ifdef MOZ_XUL
   PLDHashTable             mXULTreeRules;
 #endif
 
   nsTArray<nsFontFaceRuleContainer> mFontFaceRules;
   nsTArray<nsCSSKeyframesRule*> mKeyframesRules;
+  nsTArray<nsCSSFontFeatureValuesRule*> mFontFeatureValuesRules;
   nsTArray<nsCSSPageRule*> mPageRules;
 
   // Looks up or creates the appropriate list in |mAttributeSelectors|.
   // Returns null only on allocation failure.
   nsTArray<nsCSSSelector*>* AttributeListFor(nsIAtom* aAttribute);
 
   nsMediaQueryResultCacheKey mCacheKey;
   RuleCascadeData*  mNext; // for a different medium
@@ -1024,16 +1025,17 @@ RuleCascadeData::SizeOfIncludingThis(nsM
                                         SizeOfRuleHashTableEntry, aMallocSizeOf);
 #ifdef MOZ_XUL
   n += PL_DHashTableSizeOfExcludingThis(&mXULTreeRules,
                                         SizeOfRuleHashTableEntry, aMallocSizeOf);
 #endif
 
   n += mFontFaceRules.SizeOfExcludingThis(aMallocSizeOf);
   n += mKeyframesRules.SizeOfExcludingThis(aMallocSizeOf);
+  n += mFontFeatureValuesRules.SizeOfExcludingThis(aMallocSizeOf);
   n += mPageRules.SizeOfExcludingThis(aMallocSizeOf);
 
   return n;
 }
 
 nsTArray<nsCSSSelector*>*
 RuleCascadeData::AttributeListFor(nsIAtom* aAttribute)
 {
@@ -1432,21 +1434,21 @@ static bool AttrMatchesValue(const nsAtt
   const nsDefaultStringComparator defaultComparator;
   const nsASCIICaseInsensitiveStringComparator ciComparator;
   const nsStringComparator& comparator =
       (aAttrSelector->mCaseSensitive || !isHTML)
                 ? static_cast<const nsStringComparator&>(defaultComparator)
                 : static_cast<const nsStringComparator&>(ciComparator);
 
   switch (aAttrSelector->mFunction) {
-    case NS_ATTR_FUNC_EQUALS: 
+    case NS_ATTR_FUNC_EQUALS:
       return aValue.Equals(aAttrSelector->mValue, comparator);
-    case NS_ATTR_FUNC_INCLUDES: 
+    case NS_ATTR_FUNC_INCLUDES:
       return ValueIncludes(aValue, aAttrSelector->mValue, comparator);
-    case NS_ATTR_FUNC_DASHMATCH: 
+    case NS_ATTR_FUNC_DASHMATCH:
       return nsStyleUtil::DashMatchCompare(aValue, aAttrSelector->mValue, comparator);
     case NS_ATTR_FUNC_ENDSMATCH:
       return StringEndsWith(aValue, aAttrSelector->mValue, comparator);
     case NS_ATTR_FUNC_BEGINSMATCH:
       return StringBeginsWith(aValue, aAttrSelector->mValue, comparator);
     case NS_ATTR_FUNC_CONTAINSMATCH:
       return FindInReadable(aAttrSelector->mValue, aValue, comparator);
     default:
@@ -2744,16 +2746,31 @@ nsCSSRuleProcessor::AppendPageRules(
     if (!aArray.AppendElements(cascade->mPageRules)) {
       return false;
     }
   }
   
   return true;
 }
 
+bool
+nsCSSRuleProcessor::AppendFontFeatureValuesRules(
+                              nsPresContext *aPresContext,
+                              nsTArray<nsCSSFontFeatureValuesRule*>& aArray)
+{
+  RuleCascadeData* cascade = GetRuleCascade(aPresContext);
+
+  if (cascade) {
+    if (!aArray.AppendElements(cascade->mFontFeatureValuesRules))
+      return false;
+  }
+
+  return true;
+}
+
 nsresult
 nsCSSRuleProcessor::ClearRuleCascades()
 {
   // We rely on our caller (perhaps indirectly) to do something that
   // will rebuild style data and the user font set (either
   // nsIPresShell::ReconstructStyleData or
   // nsPresContext::RebuildAllStyleData).
   RuleCascadeData *data = mRuleCascades;
@@ -3042,22 +3059,24 @@ static PLDHashTableOps gRulesByWeightOps
     PL_DHashFinalizeStub,
     InitWeightEntry
 };
 
 struct CascadeEnumData {
   CascadeEnumData(nsPresContext* aPresContext,
                   nsTArray<nsFontFaceRuleContainer>& aFontFaceRules,
                   nsTArray<nsCSSKeyframesRule*>& aKeyframesRules,
+                  nsTArray<nsCSSFontFeatureValuesRule*>& aFontFeatureValuesRules,
                   nsTArray<nsCSSPageRule*>& aPageRules,
                   nsMediaQueryResultCacheKey& aKey,
                   uint8_t aSheetType)
     : mPresContext(aPresContext),
       mFontFaceRules(aFontFaceRules),
       mKeyframesRules(aKeyframesRules),
+      mFontFeatureValuesRules(aFontFeatureValuesRules),
       mPageRules(aPageRules),
       mCacheKey(aKey),
       mSheetType(aSheetType)
   {
     if (!PL_DHashTableInit(&mRulesByWeight, &gRulesByWeightOps, nullptr,
                           sizeof(RuleByWeightEntry), 64))
       mRulesByWeight.ops = nullptr;
 
@@ -3071,16 +3090,17 @@ struct CascadeEnumData {
     if (mRulesByWeight.ops)
       PL_DHashTableFinish(&mRulesByWeight);
     PL_FinishArenaPool(&mArena);
   }
 
   nsPresContext* mPresContext;
   nsTArray<nsFontFaceRuleContainer>& mFontFaceRules;
   nsTArray<nsCSSKeyframesRule*>& mKeyframesRules;
+  nsTArray<nsCSSFontFeatureValuesRule*>& mFontFeatureValuesRules;
   nsTArray<nsCSSPageRule*>& mPageRules;
   nsMediaQueryResultCacheKey& mCacheKey;
   PLArenaPool mArena;
   // Hooray, a manual PLDHashTable since nsClassHashtable doesn't
   // provide a getter that gives me a *reference* to the value.
   PLDHashTable mRulesByWeight; // of PerWeightDataListItem linked lists
   uint8_t mSheetType;
 };
@@ -3088,17 +3108,19 @@ struct CascadeEnumData {
 /*
  * This enumerates style rules in a sheet (and recursively into any
  * grouping rules) in order to:
  *  (1) add any style rules, in order, into data->mRulesByWeight (for
  *      the primary CSS cascade), where they are separated by weight
  *      but kept in order per-weight, and
  *  (2) add any @font-face rules, in order, into data->mFontFaceRules.
  *  (3) add any @keyframes rules, in order, into data->mKeyframesRules.
- *  (4) add any @page rules, in order, into data->mPageRules.
+ *  (4) add any @font-feature-value rules, in order,
+ *      into data->mFontFeatureValuesRules.
+ *  (5) add any @page rules, in order, into data->mPageRules.
  */
 static bool
 CascadeRuleEnumFunc(css::Rule* aRule, void* aData)
 {
   CascadeEnumData* data = (CascadeEnumData*)aData;
   int32_t type = aRule->GetType();
 
   if (css::Rule::STYLE_RULE == type) {
@@ -3141,16 +3163,23 @@ CascadeRuleEnumFunc(css::Rule* aRule, vo
   }
   else if (css::Rule::KEYFRAMES_RULE == type) {
     nsCSSKeyframesRule *keyframesRule =
       static_cast<nsCSSKeyframesRule*>(aRule);
     if (!data->mKeyframesRules.AppendElement(keyframesRule)) {
       return false;
     }
   }
+  else if (css::Rule::FONT_FEATURE_VALUES_RULE == type) {
+    nsCSSFontFeatureValuesRule *fontFeatureValuesRule =
+      static_cast<nsCSSFontFeatureValuesRule*>(aRule);
+    if (!data->mFontFeatureValuesRules.AppendElement(fontFeatureValuesRule)) {
+      return false;
+    }
+  }
   else if (css::Rule::PAGE_RULE == type) {
     nsCSSPageRule* pageRule = static_cast<nsCSSPageRule*>(aRule);
     if (!data->mPageRules.AppendElement(pageRule)) {
       return false;
     }
   }
   return true;
 }
@@ -3247,16 +3276,17 @@ nsCSSRuleProcessor::RefreshRuleCascade(n
 
   if (mSheets.Length() != 0) {
     nsAutoPtr<RuleCascadeData> newCascade(
       new RuleCascadeData(aPresContext->Medium(),
                           eCompatibility_NavQuirks == aPresContext->CompatibilityMode()));
     if (newCascade) {
       CascadeEnumData data(aPresContext, newCascade->mFontFaceRules,
                            newCascade->mKeyframesRules,
+                           newCascade->mFontFeatureValuesRules,
                            newCascade->mPageRules,
                            newCascade->mCacheKey,
                            mSheetType);
       if (!data.mRulesByWeight.ops)
         return; /* out of memory */
 
       for (uint32_t i = 0; i < mSheets.Length(); ++i) {
         if (!CascadeSheet(mSheets.ElementAt(i), &data))
--- a/layout/style/nsCSSRuleProcessor.h
+++ b/layout/style/nsCSSRuleProcessor.h
@@ -123,16 +123,19 @@ public:
                            nsTArray<nsFontFaceRuleContainer>& aArray);
 
   bool AppendKeyframesRules(nsPresContext* aPresContext,
                             nsTArray<nsCSSKeyframesRule*>& aArray);
 
   bool AppendPageRules(nsPresContext* aPresContext,
                        nsTArray<nsCSSPageRule*>& aArray);
 
+  bool AppendFontFeatureValuesRules(nsPresContext* aPresContext,
+                              nsTArray<nsCSSFontFeatureValuesRule*>& aArray);
+
   /**
    * Returns the scope element for the scoped style sheets this rule
    * processor is for.  If this is not a rule processor for scoped style
    * sheets, it returns null.
    */
   mozilla::dom::Element* GetScopeElement() const { return mScopeElement; }
 
 #ifdef DEBUG
--- a/layout/style/nsCSSRules.cpp
+++ b/layout/style/nsCSSRules.cpp
@@ -29,16 +29,17 @@
 #include "nsError.h"
 #include "nsStyleUtil.h"
 #include "mozilla/css/Declaration.h"
 #include "nsCSSParser.h"
 #include "nsPrintfCString.h"
 #include "nsDOMClassInfoID.h"
 #include "mozilla/dom/CSSStyleDeclarationBinding.h"
 #include "StyleRule.h"
+#include "nsFont.h"
 
 using namespace mozilla;
 
 #define IMPL_STYLE_RULE_INHERIT_GET_DOM_RULE_WEAK(class_, super_) \
   /* virtual */ nsIDOMCSSRule* class_::GetDOMRule()               \
   { return this; }                                                \
   /* virtual */ nsIDOMCSSRule* class_::GetExistingDOMRule()       \
   { return this; }
@@ -713,17 +714,17 @@ GroupRule::AppendRulesToCssText(nsAStrin
       domRule->GetCssText(cssText);
       aCssText.Append(NS_LITERAL_STRING("  ") +
                       cssText +
                       NS_LITERAL_STRING("\n"));
     }
   }
 
   aCssText.AppendLiteral("}");
-  
+
   return NS_OK;
 }
 
 // nsIDOMCSSMediaRule or nsIDOMCSSMozDocumentRule methods
 nsresult
 GroupRule::GetCssRules(nsIDOMCSSRuleList* *aRuleList)
 {
   if (!mRuleCollection) {
@@ -1905,16 +1906,265 @@ nsCSSFontFaceRule::SizeOfIncludingThis(n
   return aMallocSizeOf(this);
 
   // Measurement of the following members may be added later if DMD finds it is
   // worthwhile:
   // - mDecl
 }
 
 
+// -----------------------------------
+// nsCSSFontFeatureValuesRule
+//
+
+/* virtual */ already_AddRefed<css::Rule>
+nsCSSFontFeatureValuesRule::Clone() const
+{
+  nsRefPtr<css::Rule> clone = new nsCSSFontFeatureValuesRule(*this);
+  return clone.forget();
+}
+
+NS_IMPL_ADDREF(nsCSSFontFeatureValuesRule)
+NS_IMPL_RELEASE(nsCSSFontFeatureValuesRule)
+
+DOMCI_DATA(CSSFontFeatureValuesRule, nsCSSFontFeatureValuesRule)
+
+// QueryInterface implementation for nsCSSFontFeatureValuesRule
+NS_INTERFACE_MAP_BEGIN(nsCSSFontFeatureValuesRule)
+  NS_INTERFACE_MAP_ENTRY(nsIStyleRule)
+  NS_INTERFACE_MAP_ENTRY(nsIDOMCSSFontFeatureValuesRule)
+  NS_INTERFACE_MAP_ENTRY(nsIDOMCSSRule)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStyleRule)
+  NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(CSSFontFeatureValuesRule)
+NS_INTERFACE_MAP_END
+
+IMPL_STYLE_RULE_INHERIT(nsCSSFontFeatureValuesRule, Rule)
+
+static void
+FamilyListToString(const nsTArray<nsString>& aFamilyList, nsAString& aOutStr)
+{
+  uint32_t i, n = aFamilyList.Length();
+
+  for (i = 0; i < n; i++) {
+    nsStyleUtil::AppendEscapedCSSString(aFamilyList[i], aOutStr);
+    if (i != n - 1) {
+      aOutStr.AppendLiteral(", ");
+    }
+  }
+}
+
+static void
+FeatureValuesToString(
+  const nsTArray<gfxFontFeatureValueSet::FeatureValues>& aFeatureValues,
+  nsAString& aOutStr)
+{
+  uint32_t i, n;
+
+  // append values
+  n = aFeatureValues.Length();
+  for (i = 0; i < n; i++) {
+    const gfxFontFeatureValueSet::FeatureValues& fv = aFeatureValues[i];
+
+    // @alternate
+    aOutStr.AppendLiteral("  @");
+    nsAutoString functAlt;
+    nsStyleUtil::GetFunctionalAlternatesName(fv.alternate, functAlt);
+    aOutStr.Append(functAlt);
+    aOutStr.AppendLiteral(" {");
+
+    // for each ident-values tuple
+    uint32_t j, numValues = fv.valuelist.Length();
+    for (j = 0; j < numValues; j++) {
+      aOutStr.AppendLiteral(" ");
+      const gfxFontFeatureValueSet::ValueList& vlist = fv.valuelist[j];
+      nsStyleUtil::AppendEscapedCSSIdent(vlist.name, aOutStr);
+      aOutStr.AppendLiteral(":");
+
+      uint32_t k, numSelectors = vlist.featureSelectors.Length();
+      for (k = 0; k < numSelectors; k++) {
+        aOutStr.AppendLiteral(" ");
+        aOutStr.AppendInt(vlist.featureSelectors[k]);
+      }
+
+      aOutStr.AppendLiteral(";");
+    }
+    aOutStr.AppendLiteral(" }\n");
+  }
+}
+
+static void
+FontFeatureValuesRuleToString(
+  const nsTArray<nsString>& aFamilyList,
+  const nsTArray<gfxFontFeatureValueSet::FeatureValues>& aFeatureValues,
+  nsAString& aOutStr)
+{
+  aOutStr.AssignLiteral("@font-feature-values ");
+  nsAutoString familyListStr, valueTextStr;
+  FamilyListToString(aFamilyList, familyListStr);
+  aOutStr.Append(familyListStr);
+  aOutStr.AppendLiteral(" {\n");
+  FeatureValuesToString(aFeatureValues, valueTextStr);
+  aOutStr.Append(valueTextStr);
+  aOutStr.AppendLiteral("}");
+}
+
+#ifdef DEBUG
+void
+nsCSSFontFeatureValuesRule::List(FILE* out, int32_t aIndent) const
+{
+  nsAutoString text;
+  FontFeatureValuesRuleToString(mFamilyList, mFeatureValues, text);
+  NS_ConvertUTF16toUTF8 utf8(text);
+
+  // replace newlines with newlines plus indent spaces
+  char* indent = new char[(aIndent + 1) * 2];
+  int32_t i;
+  for (i = 1; i < (aIndent + 1) * 2 - 1; i++) {
+    indent[i] = 0x20;
+  }
+  indent[0] = 0xa;
+  indent[aIndent * 2 + 1] = 0;
+  utf8.ReplaceSubstring("\n", indent);
+  delete [] indent;
+
+  for (i = aIndent; --i >= 0; ) fputs("  ", out);
+  fprintf(out, "%s\n", utf8.get());
+}
+#endif
+
+/* virtual */ int32_t
+nsCSSFontFeatureValuesRule::GetType() const
+{
+  return Rule::FONT_FEATURE_VALUES_RULE;
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::GetType(uint16_t* aType)
+{
+  *aType = nsIDOMCSSRule::FONT_FEATURE_VALUES_RULE;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::GetCssText(nsAString& aCssText)
+{
+  FontFeatureValuesRuleToString(mFamilyList, mFeatureValues, aCssText);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::SetCssText(const nsAString& aCssText)
+{
+  // FIXME: implement???
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::GetParentStyleSheet(nsIDOMCSSStyleSheet** aSheet)
+{
+  return Rule::GetParentStyleSheet(aSheet);
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::GetParentRule(nsIDOMCSSRule** aParentRule)
+{
+  return Rule::GetParentRule(aParentRule);
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::GetFontFamily(nsAString& aFontFamily)
+{
+  FamilyListToString(mFamilyList, aFontFamily);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::SetFontFamily(const nsAString& aFontFamily)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::GetValueText(nsAString& aValueText)
+{
+  FeatureValuesToString(mFeatureValues, aValueText);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCSSFontFeatureValuesRule::SetValueText(const nsAString& aValueText)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+struct MakeFamilyArray {
+  MakeFamilyArray(nsTArray<nsString>& aFamilyArray)
+    : familyArray(aFamilyArray), hasGeneric(false)
+  {}
+
+  static bool
+  AddFamily(const nsString& aFamily, bool aGeneric, void* aData)
+  {
+    MakeFamilyArray *familyArr = reinterpret_cast<MakeFamilyArray*> (aData);
+    if (!aGeneric && !aFamily.IsEmpty()) {
+      familyArr->familyArray.AppendElement(aFamily);
+    }
+    if (aGeneric) {
+      familyArr->hasGeneric = true;
+    }
+    return true;
+  }
+
+  nsTArray<nsString>& familyArray;
+  bool hasGeneric;
+};
+
+void
+nsCSSFontFeatureValuesRule::SetFamilyList(const nsAString& aFamilyList,
+                                          bool& aContainsGeneric)
+{
+  nsFont font(aFamilyList, 0, 0, 0, 0, 0, 0);
+  MakeFamilyArray families(mFamilyList);
+  font.EnumerateFamilies(MakeFamilyArray::AddFamily, (void*) &families);
+  aContainsGeneric = families.hasGeneric;
+}
+
+void
+nsCSSFontFeatureValuesRule::AddValueList(int32_t aVariantAlternate,
+                     nsTArray<gfxFontFeatureValueSet::ValueList>& aValueList)
+{
+  uint32_t i, len = mFeatureValues.Length();
+  bool foundAlternate = false;
+
+  // add to an existing list for a given property value
+  for (i = 0; i < len; i++) {
+    gfxFontFeatureValueSet::FeatureValues& f = mFeatureValues.ElementAt(i);
+
+    if (f.alternate == uint32_t(aVariantAlternate)) {
+      f.valuelist.AppendElements(aValueList);
+      foundAlternate = true;
+      break;
+    }
+  }
+
+  // create a new list for a given property value
+  if (!foundAlternate) {
+    gfxFontFeatureValueSet::FeatureValues &f = *mFeatureValues.AppendElement();
+    f.alternate = aVariantAlternate;
+    f.valuelist.AppendElements(aValueList);
+  }
+}
+
+size_t
+nsCSSFontFeatureValuesRule::SizeOfIncludingThis(
+  nsMallocSizeOfFun aMallocSizeOf) const
+{
+  return aMallocSizeOf(this);
+}
+
 // -------------------------------------------
 // nsCSSKeyframeStyleDeclaration
 //
 
 nsCSSKeyframeStyleDeclaration::nsCSSKeyframeStyleDeclaration(nsCSSKeyframeRule *aRule)
   : mRule(aRule)
 {
 }
--- a/layout/style/nsCSSRules.h
+++ b/layout/style/nsCSSRules.h
@@ -10,32 +10,34 @@
 #define nsCSSRules_h_
 
 #include "mozilla/Attributes.h"
 
 #include "mozilla/css/GroupRule.h"
 #include "mozilla/Preferences.h"
 #include "nsIDOMCSSConditionRule.h"
 #include "nsIDOMCSSFontFaceRule.h"
+#include "nsIDOMCSSFontFeatureValuesRule.h"
 #include "nsIDOMCSSGroupingRule.h"
 #include "nsIDOMCSSMediaRule.h"
 #include "nsIDOMCSSMozDocumentRule.h"
 #include "nsIDOMCSSSupportsRule.h"
 #include "nsIDOMMozCSSKeyframeRule.h"
 #include "nsIDOMMozCSSKeyframesRule.h"
 #include "nsIDOMCSSStyleDeclaration.h"
 #include "nsAutoPtr.h"
 #include "nsCSSProperty.h"
 #include "nsCSSValue.h"
 #include "nsIDOMCSSCharsetRule.h"
 #include "nsTArray.h"
 #include "nsDOMCSSDeclaration.h"
 #include "Declaration.h"
 #include "nsIDOMCSSPageRule.h"
 #include "StyleRule.h"
+#include "gfxFontFeatures.h"
 
 class nsMediaList;
 
 namespace mozilla {
 
 class ErrorResult;
 
 namespace css {
@@ -207,17 +209,17 @@ public:
                                JS::Handle<JSObject*> scope) MOZ_OVERRIDE;
 
 protected:
   friend class nsCSSFontFaceRule;
 #define CSS_FONT_DESC(name_, method_) nsCSSValue m##method_;
 #include "nsCSSFontDescList.h"
 #undef CSS_FONT_DESC
 
-  static nsCSSValue nsCSSFontFaceStyleDecl::* const Fields[];  
+  static nsCSSValue nsCSSFontFaceStyleDecl::* const Fields[];
   inline nsCSSFontFaceRule* ContainingRule();
   inline const nsCSSFontFaceRule* ContainingRule() const;
 
 private:
   // NOT TO BE IMPLEMENTED
   // This object cannot be allocated on its own, only as part of
   // nsCSSFontFaceRule.
   void* operator new(size_t size) CPP_THROW_NEW;
@@ -259,17 +261,17 @@ public:
 
   virtual size_t SizeOfIncludingThis(nsMallocSizeOfFun aMallocSizeOf) const MOZ_OVERRIDE;
 
 protected:
   friend class nsCSSFontFaceStyleDecl;
   nsCSSFontFaceStyleDecl mDecl;
 };
 
-// nsFontFaceRuleContainer - used for associating sheet type with 
+// nsFontFaceRuleContainer - used for associating sheet type with
 // specific @font-face rules
 struct nsFontFaceRuleContainer {
   nsRefPtr<nsCSSFontFaceRule> mRule;
   uint8_t mSheetType;
 };
 
 inline nsCSSFontFaceRule*
 nsCSSFontFaceStyleDecl::ContainingRule()
@@ -280,16 +282,71 @@ nsCSSFontFaceStyleDecl::ContainingRule()
 
 inline const nsCSSFontFaceRule*
 nsCSSFontFaceStyleDecl::ContainingRule() const
 {
   return reinterpret_cast<const nsCSSFontFaceRule*>
     (reinterpret_cast<const char*>(this) - offsetof(nsCSSFontFaceRule, mDecl));
 }
 
+class nsCSSFontFeatureValuesRule MOZ_FINAL :
+                                       public mozilla::css::Rule,
+                                       public nsIDOMCSSFontFeatureValuesRule
+{
+public:
+  nsCSSFontFeatureValuesRule() {}
+
+  nsCSSFontFeatureValuesRule(const nsCSSFontFeatureValuesRule& aCopy)
+    // copy everything except our reference count
+    : mozilla::css::Rule(aCopy),
+      mFamilyList(aCopy.mFamilyList),
+      mFeatureValues(aCopy.mFeatureValues) {}
+
+  NS_DECL_ISUPPORTS
+
+  // nsIStyleRule methods
+#ifdef DEBUG
+  virtual void List(FILE* out = stdout, int32_t aIndent = 0) const MOZ_OVERRIDE;
+#endif
+
+  // Rule methods
+  DECL_STYLE_RULE_INHERIT
+
+  virtual int32_t GetType() const MOZ_OVERRIDE;
+  virtual already_AddRefed<mozilla::css::Rule> Clone() const MOZ_OVERRIDE;
+
+  // nsIDOMCSSRule interface
+  NS_DECL_NSIDOMCSSRULE
+
+  // nsIDOMCSSFontFaceRule interface
+  NS_DECL_NSIDOMCSSFONTFEATUREVALUESRULE
+
+  const nsTArray<nsString>& GetFamilyList() { return mFamilyList; }
+  void SetFamilyList(const nsAString& aFamilyList, bool& aContainsGeneric);
+
+  void AddValueList(int32_t aVariantAlternate,
+                    nsTArray<gfxFontFeatureValueSet::ValueList>& aValueList);
+
+  const nsTArray<gfxFontFeatureValueSet::FeatureValues>& GetFeatureValues()
+  {
+    return mFeatureValues;
+  }
+
+  virtual size_t SizeOfIncludingThis(nsMallocSizeOfFun aMallocSizeOf) const;
+
+  static bool PrefEnabled()
+  {
+    return mozilla::Preferences::GetBool("layout.css.font-features.enabled");
+  }
+
+protected:
+  nsTArray<nsString> mFamilyList;
+  nsTArray<gfxFontFeatureValueSet::FeatureValues> mFeatureValues;
+};
+
 namespace mozilla {
 namespace css {
 
 class CharsetRule MOZ_FINAL : public Rule,
                               public nsIDOMCSSCharsetRule
 {
 public:
   CharsetRule(const nsAString& aEncoding);
--- a/layout/style/nsStyleSet.cpp
+++ b/layout/style/nsStyleSet.cpp
@@ -105,16 +105,17 @@ static const nsStyleSet::sheetType gCSSS
 };
 
 nsStyleSet::nsStyleSet()
   : mRuleTree(nullptr),
     mBatching(0),
     mInShutdown(false),
     mAuthorStyleDisabled(false),
     mInReconstruct(false),
+    mInitFontFeatureValuesLookup(true),
     mDirty(0),
     mUnusedRuleNodeCount(0)
 {
 }
 
 size_t
 nsStyleSet::SizeOfIncludingThis(nsMallocSizeOfFun aMallocSizeOf) const
 {
@@ -1099,17 +1100,17 @@ nsStyleSet::WalkRuleProcessors(nsIStyleR
     (*aFunc)(mRuleProcessors[eAgentSheet], aData);
 
   bool skipUserStyles = aData->mElement->IsInNativeAnonymousSubtree();
   if (!skipUserStyles && mRuleProcessors[eUserSheet]) // NOTE: different
     (*aFunc)(mRuleProcessors[eUserSheet], aData);
 
   if (mRuleProcessors[ePresHintSheet])
     (*aFunc)(mRuleProcessors[ePresHintSheet], aData);
-  
+
   bool cutOffInheritance = false;
   if (mBindingManager) {
     // We can supply additional document-level sheets that should be walked.
     if (aWalkAllXBLStylesheets) {
       mBindingManager->WalkAllRules(aFunc, aData);
     } else {
       mBindingManager->WalkRules(aFunc, aData, &cutOffInheritance);
     }
@@ -1536,16 +1537,69 @@ nsStyleSet::AppendKeyframesRules(nsPresC
                                     (mRuleProcessors[gCSSSheetTypes[i]].get());
     if (ruleProc && !ruleProc->AppendKeyframesRules(aPresContext, aArray))
       return false;
   }
   return true;
 }
 
 bool
+nsStyleSet::AppendFontFeatureValuesRules(nsPresContext* aPresContext,
+                                 nsTArray<nsCSSFontFeatureValuesRule*>& aArray)
+{
+  NS_ENSURE_FALSE(mInShutdown, false);
+
+  for (uint32_t i = 0; i < NS_ARRAY_LENGTH(gCSSSheetTypes); ++i) {
+    nsCSSRuleProcessor *ruleProc = static_cast<nsCSSRuleProcessor*>
+                                    (mRuleProcessors[gCSSSheetTypes[i]].get());
+    if (ruleProc &&
+        !ruleProc->AppendFontFeatureValuesRules(aPresContext, aArray))
+    {
+      return false;
+    }
+  }
+  return true;
+}
+
+already_AddRefed<gfxFontFeatureValueSet>
+nsStyleSet::GetFontFeatureValuesLookup()
+{
+  if (mInitFontFeatureValuesLookup) {
+    mInitFontFeatureValuesLookup = false;
+
+    nsTArray<nsCSSFontFeatureValuesRule*> rules;
+    AppendFontFeatureValuesRules(PresContext(), rules);
+
+    mFontFeatureValuesLookup = new gfxFontFeatureValueSet();
+
+    uint32_t i, numRules = rules.Length();
+    for (i = 0; i < numRules; i++) {
+      nsCSSFontFeatureValuesRule *rule = rules[i];
+
+      const nsTArray<nsString>& familyList = rule->GetFamilyList();
+      const nsTArray<gfxFontFeatureValueSet::FeatureValues>&
+        featureValues = rule->GetFeatureValues();
+
+      // for each family
+      uint32_t f, numFam;
+
+      numFam = familyList.Length();
+      for (f = 0; f < numFam; f++) {
+        const nsString& family = familyList.ElementAt(f);
+        nsAutoString silly(family);
+        mFontFeatureValuesLookup->AddFontFeatureValues(silly, featureValues);
+      }
+    }
+  }
+
+  nsRefPtr<gfxFontFeatureValueSet> lookup = mFontFeatureValuesLookup;
+  return lookup.forget();
+}
+
+bool
 nsStyleSet::AppendPageRules(nsPresContext* aPresContext,
                             nsTArray<nsCSSPageRule*>& aArray)
 {
   NS_ENSURE_FALSE(mInShutdown, false);
 
   for (uint32_t i = 0; i < NS_ARRAY_LENGTH(gCSSSheetTypes); ++i) {
     if (gCSSSheetTypes[i] == eScopedDocSheet)
       continue;
@@ -1807,17 +1861,17 @@ struct MOZ_STACK_CLASS AttributeData : p
   AttributeData(nsPresContext* aPresContext,
                 Element* aElement, nsIAtom* aAttribute, int32_t aModType,
                 bool aAttrHasChanged, TreeMatchContext& aTreeMatchContext)
     : AttributeRuleProcessorData(aPresContext, aElement, aAttribute, aModType,
                                  aAttrHasChanged, aTreeMatchContext),
       mHint(nsRestyleHint(0))
   {}
   nsRestyleHint   mHint;
-}; 
+};
 
 static bool
 SheetHasAttributeStyle(nsIStyleRuleProcessor* aProcessor, void *aData)
 {
   AttributeData* data = (AttributeData*)aData;
   nsRestyleHint hint = aProcessor->HasAttributeDependentStyle(data);
   data->mHint = nsRestyleHint(data->mHint | hint);
   return true; // continue
--- a/layout/style/nsStyleSet.h
+++ b/layout/style/nsStyleSet.h
@@ -23,16 +23,17 @@
 #include "nsAutoPtr.h"
 #include "nsIStyleRule.h"
 #include "nsCSSPseudoElements.h"
 #include "gfxFontFeatures.h"
 
 class nsIURI;
 class nsCSSFontFaceRule;
 class nsCSSKeyframesRule;
+class nsCSSFontFeatureValuesRule;
 class nsCSSPageRule;
 class nsRuleWalker;
 struct ElementDependentRuleProcessorData;
 struct TreeMatchContext;
 
 class nsEmptyStyleRule MOZ_FINAL : public nsIStyleRule
 {
   NS_DECL_ISUPPORTS
@@ -132,17 +133,17 @@ class nsStyleSet
   ProbePseudoElementStyle(mozilla::dom::Element* aParentElement,
                           nsCSSPseudoElements::Type aType,
                           nsStyleContext* aParentContext);
   already_AddRefed<nsStyleContext>
   ProbePseudoElementStyle(mozilla::dom::Element* aParentElement,
                           nsCSSPseudoElements::Type aType,
                           nsStyleContext* aParentContext,
                           TreeMatchContext& aTreeMatchContext);
-  
+
   // Get a style context for an anonymous box.  aPseudoTag is the
   // pseudo-tag to use and must be non-null.
   already_AddRefed<nsStyleContext>
   ResolveAnonymousBoxStyle(nsIAtom* aPseudoTag, nsStyleContext* aParentContext);
 
 #ifdef MOZ_XUL
   // Get a style context for a XUL tree pseudo.  aPseudoTag is the
   // pseudo-tag to use and must be non-null.  aParentContent must be
@@ -159,21 +160,23 @@ class nsStyleSet
   bool AppendFontFaceRules(nsPresContext* aPresContext,
                              nsTArray<nsFontFaceRuleContainer>& aArray);
 
   // Append all the currently-active keyframes rules to aArray.  Return
   // true for success and false for failure.
   bool AppendKeyframesRules(nsPresContext* aPresContext,
                               nsTArray<nsCSSKeyframesRule*>& aArray);
 
-  already_AddRefed<gfxFontFeatureValueSet>
-  GetFontFeatureValuesLookup()
-  {
-    return nullptr; // not implemented yet
-  }
+  // Fetch object for looking up font feature values
+  already_AddRefed<gfxFontFeatureValueSet> GetFontFeatureValuesLookup();
+
+  // Append all the currently-active font feature values rules to aArray.
+  // Return true for success and false for failure.
+  bool AppendFontFeatureValuesRules(nsPresContext* aPresContext,
+                              nsTArray<nsCSSFontFeatureValuesRule*>& aArray);
 
   // Append all the currently-active page rules to aArray.  Return
   // true for success and false for failure.
   bool AppendPageRules(nsPresContext* aPresContext,
                        nsTArray<nsCSSPageRule*>& aArray);
 
   // Begin ignoring style context destruction, to avoid lots of unnecessary
   // work on document teardown.
@@ -407,16 +410,17 @@ class nsStyleSet
                          // lexicographic tree of matched rules that style
                          // contexts use to look up properties.
 
   uint16_t mBatching;
 
   unsigned mInShutdown : 1;
   unsigned mAuthorStyleDisabled: 1;
   unsigned mInReconstruct : 1;
+  unsigned mInitFontFeatureValuesLookup : 1;
   unsigned mDirty : 9;  // one dirty bit is used per sheet type
 
   uint32_t mUnusedRuleNodeCount; // used to batch rule node GC
   nsTArray<nsStyleContext*> mRoots; // style contexts with no parent
 
   // Empty style rules to force things that restrict which properties
   // apply into different branches of the rule tree.
   nsRefPtr<nsEmptyStyleRule> mFirstLineRule, mFirstLetterRule, mPlaceholderRule;
@@ -424,16 +428,19 @@ class nsStyleSet
   // Style rule which sets all properties to their initial values for
   // determining when context-sensitive values are in use.
   nsRefPtr<nsInitialStyleRule> mInitialStyleRule;
 
   // Old rule trees, which should only be non-empty between
   // BeginReconstruct and EndReconstruct, but in case of bugs that cause
   // style contexts to exist too long, may last longer.
   nsTArray<nsRuleNode*> mOldRuleTrees;
+
+  // whether font feature values lookup object needs initialization
+  nsRefPtr<gfxFontFeatureValueSet> mFontFeatureValuesLookup;
 };
 
 #ifdef _IMPL_NS_LAYOUT
 inline
 void nsRuleNode::AddRef()
 {
   if (mRefCnt++ == 0 && !IsRoot()) {
     mPresContext->StyleSet()->RuleNodeInUse();
--- a/layout/style/test/Makefile.in
+++ b/layout/style/test/Makefile.in
@@ -96,16 +96,17 @@ MOCHITEST_FILES =	test_acid3_test46.html
 		test_css_eof_handling.html \
 		test_css_supports.html \
 		test_default_bidi_css.html \
 		test_descriptor_storage.html \
 		test_descriptor_syntax_errors.html \
 		test_dont_use_document_colors.html \
 		test_font_face_parser.html \
 		test_font_family_parsing.html \
+		test_font_feature_values_parsing.html \
 		test_garbage_at_end_of_declarations.html \
 		test_group_insertRule.html \
 		test_html_attribute_computed_values.html \
 		test_ident_escaping.html \
 		test_inherit_computation.html \
 		test_inherit_storage.html \
 		test_initial_computation.html \
 		test_initial_storage.html \
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_font_feature_values_parsing.html
@@ -0,0 +1,356 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset=utf-8>
+  <title>@font-feature-values rule parsing tests</title>
+  <link rel="author" title="John Daggett" href="mailto:jdaggett@mozilla.com">
+  <link rel="help" href="http://www.w3.org/TR/css3-fonts/#font-feature-values" />
+  <meta name="assert" content="tests that valid @font-feature-values rules parse and invalid ones don't" />
+  <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=549861 -->
+  <script type="text/javascript" src="/resources/testharness.js"></script>
+  <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+  <style type="text/css">
+  </style>
+</head>
+<body>
+<div id="log"></div>
+<pre id="display"></pre>
+<style type="text/css" id="testbox"></style>
+
+<script type="text/javascript">
+var gPrefix = "";
+var kFontFeatureValuesRuleType = 14;
+
+function ruleName() { return "@" + gPrefix + "font-feature-values"; }
+function makeRule(f, v) {
+  return ruleName() + " " + f + " { " + v + " }";
+}
+
+function _()
+{
+  var i, decl = [];
+  for (i = 0; i < arguments.length; i++) {
+    decl.push(arguments[i]);
+  }
+  return makeRule("bongo", decl.join(" "));
+}
+
+// note: because of bugs in the way family names are serialized,
+// 'serializationSame' only implies that the value definition block
+// is the same (i.e. not including the family name list)
+
+var testrules = [
+
+  /* basic syntax */
+  { rule: ruleName() + ";", invalid: true },
+  { rule: ruleName() + " bongo;", invalid: true },
+  { rule: ruleName().replace("values", "value") + " {;}", invalid: true },
+  { rule: ruleName().replace("feature", "features") + " {;}", invalid: true },
+  { rule: makeRule("bongo", ""), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", ";"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", ",;"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", ";,"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", ",;,"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset;"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset,;"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset abc;"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { ;;abc }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc;; }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc: }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc,: }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc:, }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc:,; }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { a,b }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { a;b }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { a:;b: }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { a:,;b: }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { a:1,;b: }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc 1 2 3 }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc:, 1 2 3 }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc:; 1 2 3 }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc:; 1 2 3 }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc: 1 2 3a }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@styleset { abc: 1 2 3, def: 1; }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@blah @styleset { abc: 1 2 3; }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@blah } @styleset { abc: 1 2 3; }"), serializationNoValueDefn: true },
+  { rule: makeRule("bongo", "@blah , @styleset { abc: 1 2 3; }"), serializationNoValueDefn: true },
+  { rule: ruleName() + " bongo { @styleset { abc: 1 2 3; }", serialization: _("@styleset { abc: 1 2 3; }") },
+  { rule: ruleName() + " bongo { @styleset { abc: 1 2 3 }", serialization: _("@styleset { abc: 1 2 3; }") },
+  { rule: ruleName() + " bongo { @styleset { abc: 1 2 3;", serialization: _("@styleset { abc: 1 2 3; }") },
+  { rule: ruleName() + " bongo { @styleset { abc: 1 2 3", serialization: _("@styleset { abc: 1 2 3; }") },
+  { rule: _("@styleset { ok-1: 1; }"), serializationSame: true },
+  { rule: _("@annotation { ok-1: 3; }"), serializationSame: true },
+  { rule: _("@stylistic { blah: 3; }"), serializationSame: true },
+  { rule: makeRule("bongo", "\n@styleset\n  { blah: 3; super-blah: 4 5;\n  more-blah: 5 6 7;\n }"), serializationSame: true },
+  { rule: makeRule("bongo", "\n@styleset\n  {\n blah:\n 3\n;\n super-blah:\n 4\n 5\n;\n  more-blah:\n 5 6\n 7;\n }"), serializationSame: true },
+
+  /* limits on number of values */
+  { rule: _("@stylistic { blah: 1; }"), serializationSame: true },
+  { rule: _("@styleset { blah: 1 2 3 4; }"), serializationSame: true },
+  { rule: _("@character-variant { blah: 1 2; }"), serializationSame: true },
+  { rule: _("@swash { blah: 1; }"), serializationSame: true },
+  { rule: _("@ornaments { blah: 1; }"), serializationSame: true },
+  { rule: _("@annotation { blah: 1; }"), serializationSame: true },
+
+  /* values ignored when used */
+  { rule: _("@styleset { blah: 0; }"), serializationSame: true },
+  { rule: _("@styleset { blah: 120 124; }"), serializationSame: true },
+  { rule: _("@character-variant { blah: 0; }"), serializationSame: true },
+  { rule: _("@character-variant { blah: 111; }"), serializationSame: true },
+  { rule: _("@character-variant { blah: 111 13; }"), serializationSame: true },
+
+  /* invalid value name */
+  { rulesrc: ["styleset { blah: 1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["stylistic { blah: 1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["character-variant { blah: 1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["swash { blah: 1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["ornaments { blah: 1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["annotation { blah: 1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@bongo { blah: 1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@bongo { blah: 1 2 3 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@bongo { blah: 1 2 3; burp: 1;;; }"], serializationNoValueDefn: true },
+
+  /* values */
+  { rulesrc: ["@styleset { blah: -1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: 1 -1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: 1.5 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: 15px }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: red }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: (1) }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah:(1) }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah:, 1 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: <1> }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: 1! }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: 1,, }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: 1 1 1 1; }"], serializationSame: true },
+
+  /* limits on number of values */
+  { rulesrc: ["@stylistic { blah: 1 2 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@character-variant { blah: 1 2 3 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@swash { blah: 1 2 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@ornaments { blah: 1 2 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@annotation { blah: 1 2 }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { blah: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19; }"], serializationSame: true },
+
+  /* family names */
+  { rule: makeRule("bongo", "@styleset { blah: 1; }"), serializationSame: true },
+  { rule: makeRule("\"bongo\"", "@styleset { blah: 1; }"), serializationSame: true },
+  { rule: makeRule("'bongo'", "@styleset { blah: 1; }"), serializationSame: true },
+  { rule: makeRule("\\62 ongo", "@styleset { blah: 1; }"), serializationSame: true },
+  { rule: makeRule("bongo, super bongo, bongo the supreme", "@styleset { blah: 1; }"), serializationSame: true },
+  { rule: makeRule("bongo,, super bongo", "@styleset { blah: 1; }"), invalid: true },
+  { rule: makeRule("bongo,*", "@styleset { blah: 1; }"), invalid: true },
+  { rule: makeRule("bongo, sans-serif", "@styleset { blah: 1; }"), invalid: true },
+  { rule: makeRule("serif, sans-serif", "@styleset { blah: 1; }"), invalid: true },
+  { rule: makeRule("'serif', 'sans-serif'", "@styleset { blah: 1; }"), serializationSame: true },
+  { rule: makeRule("bongo, \"super bongo\", 'bongo the supreme'", "@styleset { blah: 1; }"), serializationSame: true },
+  { rule: makeRule("毎日カレーを食べたい!", "@styleset { blah: 1; }"), serializationSame: true },
+  { rule: makeRule("毎日カレーを食べたい!, 納豆嫌い", "@styleset { blah: 1; }"), serializationSame: true },
+
+  { rule: makeRule("bongo, \"super\" bongo, bongo the supreme", "@styleset { blah: 1; }"), invalid: true },
+  { rule: makeRule("--bongo", "@styleset { blah: 1; }"), invalid: true },
+
+  /* ident tests */
+  { rule: _("@styleset { blah: 1; blah: 1; }"), serializationSame: true },
+  { rule: _("@styleset { blah: 1; de-blah: 1; blah: 2; }"), serializationSame: true },
+  { rule: _("@styleset { \\tra-la: 1; }"), serialization: _("@styleset { tra-la: 1; }") },
+  { rule: _("@styleset { b\\lah: 1; }"), serialization: _("@styleset { blah: 1; }") },
+  { rule: _("@styleset { \\62 lah: 1; }"), serialization: _("@styleset { blah: 1; }") },
+  { rule: _("@styleset { \\:blah: 1; }"), serialization: _("@styleset { \\:blah: 1; }") },
+  { rule: _("@styleset { \\;blah: 1; }"), serialization: _("@styleset { \\;blah: 1; }") },
+  { rule: _("@styleset { complex\\20 blah: 1; }"), serialization: _("@styleset { complex\\ blah: 1; }") },
+  { rule: _("@styleset { complex\\ blah: 1; }"), serializationSame: true },
+  { rule: _("@styleset { Håkon: 1; }"), serializationSame: true },
+  { rule: _("@styleset { Åквариум: 1; }"), serializationSame: true },
+  { rule: _("@styleset { \\1f449\\1f4a9\\1f448: 1; }"), serialization: _("@styleset { 👉💩👈: 1; }") },
+  { rule: _("@styleset { 魅力: 1; }"), serializationSame: true },
+  { rule: _("@styleset { 毎日カレーを食べたい!: 1; }"), serializationSame: true },
+  /* from http://en.wikipedia.org/wiki/Metal_umlaut */
+  { rule: _("@styleset { TECHNICIÄNS\\ ÖF\\ SPÅCE\\ SHIP\\ EÅRTH\\ THIS\\ IS\\ YÖÜR\\ CÄPTÅIN\\ SPEÄKING\\ YÖÜR\\ ØÅPTÅIN\\ IS\\ DEA̋D: 1; }"), serializationSame: true },
+
+  { rulesrc: ["@styleset { 123blah: 1; }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { :123blah 1; }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { :123blah: 1; }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { ?123blah: 1; }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { \"blah\": 1; }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { complex blah: 1; }"], serializationNoValueDefn: true },
+  { rulesrc: ["@styleset { complex\\  blah: 1; }"], serializationNoValueDefn: true }
+
+];
+
+// test that invalid value declarations don't affect the parsing of surrounding 
+// declarations.  So before + invalid + after should match the serialization 
+// given in s.
+
+var gSurroundingTests = [
+  // -- invalid, valid ==> valid
+  { before: "", after: "@ornaments { whatchamacallit-1: 23; thingy-dingy: 3; }", s: _("@ornaments { whatchamacallit-1: 23; thingy-dingy: 3; }") },
+
+  // -- valid, invalid ==> valid
+  { before: "@ornaments { whatchamacallit-1: 23; thingy-dingy: 7; }", after: "", s: _("@ornaments { whatchamacallit-1: 23; thingy-dingy: 7; }") },
+
+  // -- valid, invalid, valid ==> valid, valid
+  { before: "@ornaments { whatchamacallit-1: 23; thingy-dingy: 3; }", after: "@character-variant { whatchamacallit-2: 23 4; }", s: _("@ornaments { whatchamacallit-1: 23; thingy-dingy: 3; } @character-variant { whatchamacallit-2: 23 4; }") },
+
+  // -- invalid, valid, invalid ==> valid
+  { between: "@ornaments { whatchamacallit-1: 23; thingy-dingy: 4; }", s: _("@ornaments { whatchamacallit-1: 23; thingy-dingy: 4; }") }
+];
+
+/* strip out just values, along with empty value blocks (e.g. @swash { })*/
+function valuesText(ruletext)
+{
+  var t = ruletext.replace(/@[a-zA-Z0-9\-]+[ \n]*{[ \n]*}/g, "");
+  t = t.replace(/[ \n]+/g, " ");
+  t = t.replace(/^[^{]+{[ \n]*/, "");
+  t = t.replace(/[ \n]*}[^}]*$/, "");
+  t = t.replace(/[ \n]*;/g, ";");
+  return t;
+}
+
+function testParse(rulesrc)
+{
+  var sheet = document.styleSheets[1];
+  var rule = _.apply(this, rulesrc);
+
+  while(sheet.cssRules.length > 0)
+    sheet.deleteRule(0);
+  try {
+    sheet.insertRule(rule, 0);
+  } catch (e) {
+    return e.toString();
+  }
+
+  if (sheet.cssRules.length == 1 && sheet.cssRules[0].type == kFontFeatureValuesRuleType) {
+    return sheet.cssRules[0].cssText.replace(/[ \n]+/g, " ");
+  }
+
+  return "";
+}
+
+function testOneRule(testrule) {
+  var sheet = document.styleSheets[1];
+  var rule;
+
+  if ("rulesrc" in testrule) {
+    rule = _.apply(this, testrule.rulesrc);
+  } else {
+    rule = testrule.rule;
+  }
+
+  var parseErr = false;
+  var expectedErr = false;
+  var invalid = false;
+  if ("invalid" in testrule && testrule.invalid) invalid = true;
+
+  while(sheet.cssRules.length > 0)
+    sheet.deleteRule(0);
+  try {
+    sheet.insertRule(rule, 0);
+  } catch (e) {
+    expectedErr = (e.name == "SyntaxError"
+      && e instanceof DOMException
+      && e.code == DOMException.SYNTAX_ERR
+      && invalid);
+    parseErr = true;
+  }
+
+  test(function() { 
+    assert_true(!parseErr || expectedErr, "unexpected syntax error");
+    if (!parseErr) {
+      assert_equals(sheet.cssRules.length, 1, "bad rule count");
+      assert_equals(sheet.cssRules[0].type, kFontFeatureValuesRuleType, "bad rule type");
+    }
+  }, "basic parse tests - " + rule);
+
+  var sanitizedRule = rule.replace(/[ \n]+/g, " ");
+  if (parseErr) {
+    return;
+  }
+
+  // should result in one @font-feature-values rule constructed
+
+  // serialization matches expectation
+  // -- note: due to inconsistent font family serialization problems,
+  //    only the serialization of the values is tested currently
+
+  var ruleValues = valuesText(rule);
+  var serialized = sheet.cssRules[0].cssText;
+  var serializedValues = valuesText(serialized);
+  var haveSerialization = true;
+
+  if (testrule.serializationSame) {
+    test(function() {
+      assert_equals(serializedValues, ruleValues, "canonical cssText serialization doesn't match");
+    }, "serialization check - " + rule);
+  } else if ("serialization" in testrule) {
+    var s = valuesText(testrule.serialization);
+    test(function() {
+      assert_equals(serializedValues, s, "non-canonical cssText serialization doesn't match - ");
+    }, "serialization check - " + rule);
+  } else if (testrule.serializationNoValueDefn) {
+    test(function() {
+      assert_equals(serializedValues, "", "cssText serialization should have no value defintions - ");
+    }, "no value definitions in serialization - " + rule);
+
+    haveSerialization = false;
+
+    if ("rulesrc" in testrule) {
+      test(function() {
+        var j, rulesrc = testrule.rulesrc;
+    
+        // invalid value definitions shouldn't affect the parsing of valid
+        // definitions before or after an invalid one
+        for (var j = 0; j < gSurroundingTests.length; j++) {
+          var t = gSurroundingTests[j];
+          var srulesrc = [];
+    
+          if ("between" in t) {
+            srulesrc = srulesrc.concat(rulesrc);
+            srulesrc = srulesrc.concat(t.between);
+            srulesrc = srulesrc.concat(rulesrc);
+          } else {
+            if (t.before != "")
+              srulesrc = srulesrc.concat(t.before);
+            srulesrc = srulesrc.concat(rulesrc);
+            if (t.after != "")
+              srulesrc = srulesrc.concat(t.after);
+          }
+    
+          var result = testParse(srulesrc);
+          assert_equals(valuesText(result), valuesText(t.s), "invalid declarations should not affect valid ones - ");
+        }
+      }, "invalid declarations don't affect valid ones - " + rule);
+    }
+  }
+
+  // if serialization non-empty, serialization should round-trip to itself
+  if (haveSerialization) {
+    var roundTripText = testParse([serializedValues]);
+    test(function() {
+      assert_equals(valuesText(roundTripText), serializedValues,
+         "serialization should round-trip to itself - ");
+    }, "serialization round-trip - " + rule);
+  }
+}
+
+function testFontFeatureValuesRuleParsing() {
+  var i;
+  for (i = 0; i < testrules.length; i++) {
+    var testrule = testrules[i];
+    var rule;
+
+    if ("rulesrc" in testrule) {
+      rule = _.apply(this, testrule.rulesrc);
+    } else {
+      rule = testrule.rule;
+    }
+
+    testOneRule(testrule);
+    //test(function() { testOneRule(testrule); }, "parsing " + rule);
+  }
+}
+
+testFontFeatureValuesRuleParsing();
+</script>
+</body></html>