Bug 1354876 - Add font-variant-{alternates,east-asian,ligutures,numeric}. r?heycam draft
authorHiroyuki Ikezoe <hikezoe@mozilla.com>
Thu, 13 Apr 2017 15:36:30 +0900
changeset 561770 4bc4c40d4d05e5f5c1534f1306307d88f0c71f66
parent 561769 7f65baee8219704322f08331790c264cf15d6e1a
child 561771 7d01504138229b8f52ec25e79717b5368644ae96
child 561786 6b4d814d8b1db7883b6be8e047c0e045a2a11a66
push id53857
push userhikezoe@mozilla.com
push dateThu, 13 Apr 2017 06:38:38 +0000
reviewersheycam
bugs1354876
milestone55.0a1
Bug 1354876 - Add font-variant-{alternates,east-asian,ligutures,numeric}. r?heycam MozReview-Commit-ID: IGytm9rwZ40
servo/components/style/properties/gecko.mako.rs
servo/components/style/properties/longhand/font.mako.rs
--- a/servo/components/style/properties/gecko.mako.rs
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -274,16 +274,34 @@ def set_gecko_property(ffi_name, expr):
             % endfor
             % if keyword.gecko_inexhaustive:
             x => panic!("Found unexpected value in style struct for ${ident} property: {:?}", x),
             % endif
         }
     }
 </%def>
 
+<%def name="impl_bitflags_setter(ident, gecko_ffi_name, bit_map, gecko_bit_prefix, cast_type='u8')">
+    #[allow(non_snake_case)]
+    pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
+        % for gecko_bit in bit_map.values():
+        use gecko_bindings::structs::${gecko_bit_prefix}${gecko_bit};
+        % endfor
+
+        let mut bits: ${cast_type} = 0;
+        % for servo_bit, gecko_bit in bit_map.iteritems():
+        if v.contains(longhands::${ident}::${servo_bit}) {
+            bits |= ${gecko_bit_prefix}${gecko_bit} as ${cast_type};
+        }
+        % endfor
+
+        self.gecko.${gecko_ffi_name}= bits as ${cast_type};
+    }
+</%def>
+
 
 /// Convert a Servo color into an nscolor; with currentColor as 0
 ///
 /// Call sites will need to be updated after https://bugzilla.mozilla.org/show_bug.cgi?id=760345
 fn color_to_nscolor_zero_currentcolor(color: Color) -> structs::nscolor {
     match color {
         Color::RGBA(rgba) => {
             convert_rgba_to_nscolor(&rgba)
@@ -1240,18 +1258,21 @@ fn static_assert() {
                                corner.y_index) %>
     % endfor
 
     pub fn outline_has_nonzero_width(&self) -> bool {
         self.gecko.mActualOutlineWidth != 0
     }
 </%self:impl_trait>
 
-<%self:impl_trait style_struct_name="Font"
-    skip_longhands="font-family font-size font-size-adjust font-weight font-synthesis -x-lang font-language-override"
+<% skip_font_longhands= """font-family font-size font-size-adjust font-weight
+                           font-synthesis -x-lang font-variant-alternates
+                           font-variant-east-asian font-variant-ligatures
+                           font-variant-numeric font-language-override""" %>
+<%self:impl_trait style_struct_name="Font" skip_longhands="${skip_font_longhands}"
     skip_additionals="*">
 
     pub fn set_font_family(&mut self, v: longhands::font_family::computed_value::T) {
         use properties::longhands::font_family::computed_value::FontFamily;
         use gecko_bindings::structs::FontFamilyType;
 
         let list = &mut self.gecko.mFont.fontlist;
         unsafe { Gecko_FontFamilyList_Clear(list); }
@@ -1384,16 +1405,86 @@ fn static_assert() {
             Gecko_nsStyleFont_CopyLangFrom(&mut self.gecko, &other.gecko);
         }
     }
 
     pub fn set_font_language_override(&mut self, v: longhands::font_language_override::computed_value::T) {
         self.gecko.mFont.languageOverride = v.0;
     }
     ${impl_simple_copy('font_language_override', 'mFont.languageOverride')}
+
+    <% font_variant_alternates_map = { "HISTORICAL_FORMS": "HISTORICAL",
+                                       "STYLISTIC": "STYLISTIC",
+                                       "STYLESET": "STYLESET",
+                                       "CHARACTER_VARIANT": "CHARACTER_VARIANT",
+                                       "SWASH": "SWASH",
+                                       "ORNAMENTS": "ORNAMENTS",
+                                       "ANNOTATION": "ANNOTATION" } %>
+    // FIXME: Set alternateValues as well.
+    // self.gecko.mFont.alternateValues = xxx;
+    ${impl_bitflags_setter('font_variant_alternates',
+                           'mFont.variantAlternates',
+                           font_variant_alternates_map,
+                           'NS_FONT_VARIANT_ALTERNATES_',
+                           cast_type='u16')}
+    #[allow(non_snake_case)]
+    pub fn copy_font_variant_alternates_from(&mut self, other: &Self) {
+        self.gecko.mFont.variantAlternates = other.gecko.mFont.variantAlternates;
+        // FIXME: Copy alternateValues as well.
+        // self.gecko.mFont.alternateValues = other.gecko.mFont.alternateValues;
+    }
+
+    ///                                servo_bit: gecko_bit
+    <% font_variant_ligatures_map = { "NONE": "NONE",
+                                      "COMMON_LIGATURES": "COMMON",
+                                      "NO_COMMON_LIGATURES": "NO_COMMON",
+                                      "DISCRETIONARY_LIGATURES": "DISCRETIONARY",
+                                      "NO_DISCRETIONARY_LIGATURES": "NO_DISCRETIONARY",
+                                      "HISTORICAL_LIGATURES": "HISTORICAL",
+                                      "NO_HISTORICAL_LIGATURES": "NO_HISTORICAL",
+                                      "CONTEXTUAL": "CONTEXTUAL",
+                                      "NO_CONTEXTUAL": "NO_CONTEXTUAL" } %>
+    ${impl_bitflags_setter('font_variant_ligatures',
+                           'mFont.variantLigatures',
+                           font_variant_ligatures_map,
+                           'NS_FONT_VARIANT_LIGATURES_',
+                           cast_type='u16')}
+    ${impl_simple_copy('font_variant_ligatures', 'mFont.variantLigatures')}
+
+    ///                                servo_bit: gecko_bit
+    <% font_variant_east_asian_map = { "JIS78": "JIS78",
+                                       "JIS83": "JIS83",
+                                       "JIS90": "JIS90",
+                                       "JIS04": "JIS04",
+                                       "SIMPLIFIED": "SIMPLIFIED",
+                                       "TRADITIONAL": "TRADITIONAL",
+                                       "FULL_WIDTH": "FULL_WIDTH",
+                                       "PROPORTIONAL_WIDTH": "PROP_WIDTH",
+                                       "RUBY": "RUBY" } %>
+    ${impl_bitflags_setter('font_variant_east_asian',
+                           'mFont.variantEastAsian',
+                           font_variant_east_asian_map,
+                           'NS_FONT_VARIANT_EAST_ASIAN_',
+                           cast_type='u16')}
+    ${impl_simple_copy('font_variant_east_asian', 'mFont.variantEastAsian')}
+
+    ///                             servo_bit: gecko_bit
+    <% font_variant_numeric_map = { "LINING_NUMS": "LINING",
+                                    "OLDSTYLE_NUMS": "OLDSTYLE",
+                                    "PROPORTIONAL_NUMS": "PROPORTIONAL",
+                                    "TABULAR_NUMS": "TABULAR",
+                                    "DIAGONAL_FRACTIONS": "DIAGONAL_FRACTIONS",
+                                    "STACKED_FRACTIONS": "STACKED_FRACTIONS",
+                                    "SLASHED_ZERO": "SLASHZERO",
+                                    "ORDINAL": "ORDINAL" } %>
+    ${impl_bitflags_setter('font_variant_numeric',
+                           'mFont.variantNumeric',
+                           font_variant_numeric_map,
+                           'NS_FONT_VARIANT_NUMERIC_')}
+    ${impl_simple_copy('font_variant_numeric', 'mFont.variantNumeric')}
 </%self:impl_trait>
 
 <%def name="impl_copy_animation_or_transition_value(type, ident, gecko_ffi_name)">
     #[allow(non_snake_case)]
     pub fn copy_${type}_${ident}_from(&mut self, other: &Self) {
         unsafe { self.gecko.m${type.capitalize()}s.ensure_len(other.gecko.m${type.capitalize()}s.len()) };
 
         let count = other.gecko.m${type.capitalize()}${gecko_ffi_name}Count;
--- a/servo/components/style/properties/longhand/font.mako.rs
+++ b/servo/components/style/properties/longhand/font.mako.rs
@@ -854,16 +854,501 @@
 ${helpers.single_keyword("font-kerning",
                          "auto none normal",
                          products="gecko",
                          gecko_ffi_name="mFont.kerning",
                          gecko_constant_prefix="NS_FONT_KERNING",
                          spec="https://drafts.csswg.org/css-fonts/#propdef-font-stretch",
                          animation_type="none")}
 
+/// FIXME: Implement proper handling of each values.
+/// https://github.com/servo/servo/issues/15957
+<%helpers:longhand name="font-variant-alternates" products="gecko" animation_type="none"
+                   spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-alternates">
+    use std::fmt;
+    use style_traits::ToCss;
+    use values::HasViewportPercentage;
+    use values::computed::ComputedValueAsSpecified;
+
+    impl ComputedValueAsSpecified for SpecifiedValue {}
+    no_viewport_percentage!(SpecifiedValue);
+
+    bitflags! {
+        pub flags SpecifiedValue: u8 {
+            const NORMAL = 0,
+            const HISTORICAL_FORMS = 0x01,
+            const STYLISTIC = 0x02,
+            const STYLESET = 0x04,
+            const CHARACTER_VARIANT = 0x08,
+            const SWASH = 0x10,
+            const ORNAMENTS = 0x20,
+            const ANNOTATION = 0x40,
+        }
+    }
+
+    impl ToCss for SpecifiedValue {
+        fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
+            if self.is_empty() {
+                return dest.write_str("normal")
+            }
+
+            let mut has_any = false;
+
+            macro_rules! write_value {
+                ($ident:ident => $str:expr) => {
+                    if self.intersects($ident) {
+                        if has_any {
+                            try!(dest.write_str(" "));
+                        }
+                        has_any = true;
+                        try!(dest.write_str($str));
+                    }
+                }
+            }
+
+            write_value!(HISTORICAL_FORMS => "historical-forms");
+            write_value!(STYLISTIC => "stylistic");
+            write_value!(STYLESET => "styleset");
+            write_value!(CHARACTER_VARIANT => "character-variant");
+            write_value!(SWASH => "swash");
+            write_value!(ORNAMENTS => "ornaments");
+            write_value!(ANNOTATION => "annotation");
+
+            debug_assert!(has_any);
+            Ok(())
+        }
+    }
+
+    pub mod computed_value {
+        pub type T = super::SpecifiedValue;
+    }
+    #[inline]
+    pub fn get_initial_value() -> computed_value::T {
+        computed_value::T::empty()
+    }
+    #[inline]
+    pub fn get_initial_specified_value() -> SpecifiedValue {
+        SpecifiedValue::empty()
+    }
+
+    /// normal |
+    ///  [ stylistic(<feature-value-name>)           ||
+    ///    historical-forms                          ||
+    ///    styleset(<feature-value-name> #)          ||
+    ///    character-variant(<feature-value-name> #) ||
+    ///    swash(<feature-value-name>)               ||
+    ///    ornaments(<feature-value-name>)           ||
+    ///    annotation(<feature-value-name>) ]
+    pub fn parse(_context: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue, ()> {
+        let mut result = SpecifiedValue::empty();
+
+        if input.try(|input| input.expect_ident_matching("normal")).is_ok() {
+            return Ok(result)
+        }
+
+        while let Ok(ident) = input.try(|input| input.expect_ident()) {
+            let flag = match_ignore_ascii_case! { &ident,
+                "stylistic" => STYLISTIC,
+                "historical-forms" => HISTORICAL_FORMS,
+                "styleset" => STYLESET,
+                "character-variant" => CHARACTER_VARIANT,
+                "swash" => SWASH,
+                "ornaments" => ORNAMENTS,
+                "annotation" => ANNOTATION,
+                _ => return Err(()),
+            };
+            if result.intersects(flag) {
+                return Err(())
+            }
+            result.insert(flag);
+        }
+
+        if !result.is_empty() {
+            Ok(result)
+        } else {
+            Err(())
+        }
+    }
+</%helpers:longhand>
+
+macro_rules! exclusive_value {
+    (($value:ident, $set:expr) => $ident:ident) => {
+        if $value.intersects($set) {
+            return Err(())
+        } else {
+            $ident
+        }
+    }
+}
+
+<%helpers:longhand name="font-variant-east-asian" products="gecko" animation_type="none"
+                   spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-east-asian">
+    use std::fmt;
+    use style_traits::ToCss;
+    use values::HasViewportPercentage;
+    use values::computed::ComputedValueAsSpecified;
+
+    impl ComputedValueAsSpecified for SpecifiedValue {}
+    no_viewport_percentage!(SpecifiedValue);
+
+    bitflags! {
+        pub flags SpecifiedValue: u16 {
+            const NORMAL = 0,
+            const JIS78 = 0x01,
+            const JIS83 = 0x02,
+            const JIS90 = 0x04,
+            const JIS04 = 0x08,
+            const SIMPLIFIED = 0x10,
+            const TRADITIONAL = 0x20,
+            const FULL_WIDTH = 0x40,
+            const PROPORTIONAL_WIDTH = 0x80,
+            const RUBY = 0x100,
+        }
+    }
+
+    impl ToCss for SpecifiedValue {
+        fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
+            if self.is_empty() {
+                return dest.write_str("normal")
+            }
+
+            let mut has_any = false;
+
+            macro_rules! write_value {
+                ($ident:ident => $str:expr) => {
+                    if self.intersects($ident) {
+                        if has_any {
+                            try!(dest.write_str(" "));
+                        }
+                        has_any = true;
+                        try!(dest.write_str($str));
+                    }
+                }
+            }
+
+            write_value!(JIS78 => "jis78");
+            write_value!(JIS83 => "jis83");
+            write_value!(JIS90 => "jis90");
+            write_value!(JIS04 => "jis04");
+            write_value!(SIMPLIFIED => "simplified");
+            write_value!(TRADITIONAL => "traditional");
+            write_value!(FULL_WIDTH => "full-width");
+            write_value!(PROPORTIONAL_WIDTH => "proportional-width");
+            write_value!(RUBY => "ruby");
+
+            debug_assert!(has_any);
+            Ok(())
+        }
+    }
+
+    pub mod computed_value {
+        pub type T = super::SpecifiedValue;
+    }
+    #[inline]
+    pub fn get_initial_value() -> computed_value::T {
+        computed_value::T::empty()
+    }
+    #[inline]
+    pub fn get_initial_specified_value() -> SpecifiedValue {
+        SpecifiedValue::empty()
+    }
+
+    /// normal | [ <east-asian-variant-values> || <east-asian-width-values> || ruby ]
+    /// <east-asian-variant-values> = [ jis78 | jis83 | jis90 | jis04 | simplified | traditional ]
+    /// <east-asian-width-values>   = [ full-width | proportional-width ]
+    <% east_asian_variant_values = "JIS78 | JIS83 | JIS90 | JIS04 | SIMPLIFIED | TRADITIONAL" %>
+    <% east_asian_width_values = "FULL_WIDTH | PROPORTIONAL_WIDTH" %>
+    pub fn parse(_context: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue, ()> {
+        let mut result = SpecifiedValue::empty();
+
+        if input.try(|input| input.expect_ident_matching("normal")).is_ok() {
+            return Ok(result)
+        }
+
+        while let Ok(ident) = input.try(|input| input.expect_ident()) {
+            let flag = match_ignore_ascii_case! { &ident,
+                "jis78" =>
+                    exclusive_value!((result, ${east_asian_variant_values}) => JIS78),
+                "jis83" =>
+                    exclusive_value!((result, ${east_asian_variant_values}) => JIS83),
+                "jis90" =>
+                    exclusive_value!((result, ${east_asian_variant_values}) => JIS90),
+                "jis04" =>
+                    exclusive_value!((result, ${east_asian_variant_values}) => JIS04),
+                "simplified" =>
+                    exclusive_value!((result, ${east_asian_variant_values}) => SIMPLIFIED),
+                "traditional" =>
+                    exclusive_value!((result, ${east_asian_variant_values}) => TRADITIONAL),
+                "full-width" =>
+                    exclusive_value!((result, ${east_asian_width_values}) => FULL_WIDTH),
+                "proportional-width" =>
+                    exclusive_value!((result, ${east_asian_width_values}) => PROPORTIONAL_WIDTH),
+                "ruby" =>
+                    exclusive_value!((result, RUBY) => RUBY),
+                _ => return Err(()),
+            };
+            result.insert(flag);
+        }
+
+        if !result.is_empty() {
+            Ok(result)
+        } else {
+            Err(())
+        }
+    }
+</%helpers:longhand>
+
+<%helpers:longhand name="font-variant-ligatures" products="gecko" animation_type="none"
+                   spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-ligatures">
+    use std::fmt;
+    use style_traits::ToCss;
+    use values::HasViewportPercentage;
+    use values::computed::ComputedValueAsSpecified;
+
+    impl ComputedValueAsSpecified for SpecifiedValue {}
+    no_viewport_percentage!(SpecifiedValue);
+
+    bitflags! {
+        pub flags SpecifiedValue: u16 {
+            const NORMAL = 0,
+            const NONE = 0x01,
+            const COMMON_LIGATURES = 0x02,
+            const NO_COMMON_LIGATURES = 0x04,
+            const DISCRETIONARY_LIGATURES = 0x08,
+            const NO_DISCRETIONARY_LIGATURES = 0x10,
+            const HISTORICAL_LIGATURES = 0x20,
+            const NO_HISTORICAL_LIGATURES = 0x40,
+            const CONTEXTUAL = 0x80,
+            const NO_CONTEXTUAL = 0x100,
+        }
+    }
+
+    impl ToCss for SpecifiedValue {
+        fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
+            if self.is_empty() {
+                return dest.write_str("normal")
+            }
+            if self.contains(NONE) {
+                return dest.write_str("none")
+            }
+
+            let mut has_any = false;
+
+            macro_rules! write_value {
+                ($ident:ident => $str:expr) => {
+                    if self.intersects($ident) {
+                        if has_any {
+                            try!(dest.write_str(" "));
+                        }
+                        has_any = true;
+                        try!(dest.write_str($str));
+                    }
+                }
+            }
+
+            write_value!(COMMON_LIGATURES => "common-ligatures");
+            write_value!(NO_COMMON_LIGATURES => "no-common-ligatures");
+            write_value!(DISCRETIONARY_LIGATURES => "discretionary-ligatures");
+            write_value!(NO_DISCRETIONARY_LIGATURES => "no-discretionary-ligatures");
+            write_value!(HISTORICAL_LIGATURES => "historical-ligatures");
+            write_value!(NO_HISTORICAL_LIGATURES => "no-historical-ligatures");
+            write_value!(CONTEXTUAL => "contextual");
+            write_value!(NO_CONTEXTUAL => "no-contextual");
+
+            debug_assert!(has_any);
+            Ok(())
+        }
+    }
+
+    pub mod computed_value {
+        pub type T = super::SpecifiedValue;
+    }
+    #[inline]
+    pub fn get_initial_value() -> computed_value::T {
+        computed_value::T::empty()
+    }
+    #[inline]
+    pub fn get_initial_specified_value() -> SpecifiedValue {
+        SpecifiedValue::empty()
+    }
+
+    /// normal | none |
+    /// [ <common-lig-values> ||
+    ///   <discretionary-lig-values> ||
+    ///   <historical-lig-values> ||
+    ///   <contextual-alt-values> ]
+    /// <common-lig-values>        = [ common-ligatures | no-common-ligatures ]
+    /// <discretionary-lig-values> = [ discretionary-ligatures | no-discretionary-ligatures ]
+    /// <historical-lig-values>    = [ historical-ligatures | no-historical-ligatures ]
+    /// <contextual-alt-values>    = [ contextual | no-contextual ]
+    <% common_lig_values = "COMMON_LIGATURES | NO_COMMON_LIGATURES" %>
+    <% discretionary_lig_values = "DISCRETIONARY_LIGATURES | NO_DISCRETIONARY_LIGATURES" %>
+    <% historical_lig_values = "HISTORICAL_LIGATURES | NO_HISTORICAL_LIGATURES" %>
+    <% contextual_alt_values = "CONTEXTUAL | NO_CONTEXTUAL" %>
+    pub fn parse(_context: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue, ()> {
+        let mut result = SpecifiedValue::empty();
+
+        if input.try(|input| input.expect_ident_matching("normal")).is_ok() {
+            return Ok(result)
+        }
+        if input.try(|input| input.expect_ident_matching("none")).is_ok() {
+            return Ok(NONE)
+        }
+
+        while let Ok(ident) = input.try(|input| input.expect_ident()) {
+            let flag = match_ignore_ascii_case! { &ident,
+                "common-ligatures" =>
+                    exclusive_value!((result, ${common_lig_values}) => COMMON_LIGATURES),
+                "no-common-ligatures" =>
+                    exclusive_value!((result, ${common_lig_values}) => NO_COMMON_LIGATURES),
+                "discretionary-ligatures" =>
+                    exclusive_value!((result, ${discretionary_lig_values}) => DISCRETIONARY_LIGATURES),
+                "no-discretionary-ligatures" =>
+                    exclusive_value!((result, ${discretionary_lig_values}) => NO_DISCRETIONARY_LIGATURES),
+                "historical-ligatures" =>
+                    exclusive_value!((result, ${historical_lig_values}) => HISTORICAL_LIGATURES),
+                "no-historical-ligatures" =>
+                    exclusive_value!((result, ${historical_lig_values}) => NO_HISTORICAL_LIGATURES),
+                "contextual" =>
+                    exclusive_value!((result, ${contextual_alt_values}) => CONTEXTUAL),
+                "no-contextual" =>
+                    exclusive_value!((result, ${contextual_alt_values}) => NO_CONTEXTUAL),
+                _ => return Err(()),
+            };
+            result.insert(flag);
+        }
+
+        if !result.is_empty() {
+            Ok(result)
+        } else {
+            Err(())
+        }
+    }
+</%helpers:longhand>
+
+<%helpers:longhand name="font-variant-numeric" products="gecko" animation_type="none"
+                   spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-numeric">
+    use std::fmt;
+    use style_traits::ToCss;
+    use values::HasViewportPercentage;
+    use values::computed::ComputedValueAsSpecified;
+
+    impl ComputedValueAsSpecified for SpecifiedValue {}
+    no_viewport_percentage!(SpecifiedValue);
+
+    bitflags! {
+        pub flags SpecifiedValue: u8 {
+            const NORMAL = 0,
+            const LINING_NUMS = 0x01,
+            const OLDSTYLE_NUMS = 0x02,
+            const PROPORTIONAL_NUMS = 0x04,
+            const TABULAR_NUMS = 0x08,
+            const DIAGONAL_FRACTIONS = 0x10,
+            const STACKED_FRACTIONS = 0x20,
+            const SLASHED_ZERO = 0x40,
+            const ORDINAL = 0x80,
+        }
+    }
+
+    impl ToCss for SpecifiedValue {
+        fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
+            if self.is_empty() {
+                return dest.write_str("normal")
+            }
+
+            let mut has_any = false;
+
+            macro_rules! write_value {
+                ($ident:ident => $str:expr) => {
+                    if self.intersects($ident) {
+                        if has_any {
+                            try!(dest.write_str(" "));
+                        }
+                        has_any = true;
+                        try!(dest.write_str($str));
+                    }
+                }
+            }
+
+            write_value!(LINING_NUMS => "lining-nums");
+            write_value!(OLDSTYLE_NUMS => "oldstyle-nums");
+            write_value!(PROPORTIONAL_NUMS => "proportional-nums");
+            write_value!(TABULAR_NUMS => "tabular-nums");
+            write_value!(DIAGONAL_FRACTIONS => "diagonal-fractions");
+            write_value!(STACKED_FRACTIONS => "stacked-fractions");
+            write_value!(SLASHED_ZERO => "slashed-zero");
+            write_value!(ORDINAL => "ordinal");
+
+            debug_assert!(has_any);
+            Ok(())
+        }
+    }
+
+    pub mod computed_value {
+        pub type T = super::SpecifiedValue;
+    }
+    #[inline]
+    pub fn get_initial_value() -> computed_value::T {
+        computed_value::T::empty()
+    }
+    #[inline]
+    pub fn get_initial_specified_value() -> SpecifiedValue {
+        SpecifiedValue::empty()
+    }
+
+    /// normal |
+    ///  [ <numeric-figure-values>   ||
+    ///    <numeric-spacing-values>  ||
+    ///    <numeric-fraction-values> ||
+    ///    ordinal                   ||
+    ///    slashed-zero ]
+    /// <numeric-figure-values>   = [ lining-nums | oldstyle-nums ]
+    /// <numeric-spacing-values>  = [ proportional-nums | tabular-nums ]
+    /// <numeric-fraction-values> = [ diagonal-fractions | stacked-fractions ]
+    <% numeric_figure_values = "LINING_NUMS | OLDSTYLE_NUMS" %>
+    <% numeric_spacing_values = "PROPORTIONAL_NUMS | TABULAR_NUMS" %>
+    <% numeric_fraction_values = "DIAGONAL_FRACTIONS | STACKED_FRACTIONS" %>
+    pub fn parse(_context: &ParserContext, input: &mut Parser) -> Result<SpecifiedValue, ()> {
+        let mut result = SpecifiedValue::empty();
+
+        if input.try(|input| input.expect_ident_matching("normal")).is_ok() {
+            return Ok(result)
+        }
+
+        while let Ok(ident) = input.try(|input| input.expect_ident()) {
+            let flag = match_ignore_ascii_case! { &ident,
+                "ordinal" =>
+                    exclusive_value!((result, ORDINAL) => ORDINAL),
+                "slashed-zero" =>
+                    exclusive_value!((result, SLASHED_ZERO) => SLASHED_ZERO),
+                "lining-nums" =>
+                    exclusive_value!((result, ${numeric_figure_values}) => LINING_NUMS ),
+                "oldstyle-nums" =>
+                    exclusive_value!((result, ${numeric_figure_values}) => OLDSTYLE_NUMS ),
+                "proportional-nums" =>
+                    exclusive_value!((result, ${numeric_spacing_values}) => PROPORTIONAL_NUMS ),
+                "tabular-nums" =>
+                    exclusive_value!((result, ${numeric_spacing_values}) => TABULAR_NUMS ),
+                "diagonal-fractions" =>
+                    exclusive_value!((result, ${numeric_fraction_values}) => DIAGONAL_FRACTIONS ),
+                "stacked-fractions" =>
+                    exclusive_value!((result, ${numeric_fraction_values}) => STACKED_FRACTIONS ),
+                _ => return Err(()),
+            };
+            result.insert(flag);
+        }
+
+        if !result.is_empty() {
+            Ok(result)
+        } else {
+            Err(())
+        }
+    }
+</%helpers:longhand>
+
 ${helpers.single_keyword("font-variant-position",
                          "normal sub super",
                          products="gecko",
                          gecko_ffi_name="mFont.variantPosition",
                          gecko_constant_prefix="NS_FONT_VARIANT_POSITION",
                          spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-position",
                          animation_type="none")}