diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /servo/components/style/values/specified/font.rs | |
parent | Initial commit. (diff) | |
download | firefox-upstream/124.0.1.tar.xz firefox-upstream/124.0.1.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'servo/components/style/values/specified/font.rs')
-rw-r--r-- | servo/components/style/values/specified/font.rs | 2222 |
1 files changed, 2222 insertions, 0 deletions
diff --git a/servo/components/style/values/specified/font.rs b/servo/components/style/values/specified/font.rs new file mode 100644 index 0000000000..2435682ce3 --- /dev/null +++ b/servo/components/style/values/specified/font.rs @@ -0,0 +1,2222 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! Specified values for font properties + +use crate::context::QuirksMode; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::font::{FamilyName, FontFamilyList, SingleFontFamily}; +use crate::values::computed::Percentage as ComputedPercentage; +use crate::values::computed::{font as computed, Length, NonNegativeLength}; +use crate::values::computed::{CSSPixelLength, Context, ToComputedValue}; +use crate::values::generics::font::{ + self as generics, FeatureTagValue, FontSettings, FontTag, GenericLineHeight, VariationValue, +}; +use crate::values::generics::NonNegative; +use crate::values::specified::length::{FontBaseSize, LineHeightBase, PX_PER_PT}; +use crate::values::specified::{AllowQuirks, Angle, Integer, LengthPercentage}; +use crate::values::specified::{ + FontRelativeLength, NoCalcLength, NonNegativeLengthPercentage, NonNegativeNumber, + NonNegativePercentage, Number, +}; +use crate::values::{serialize_atom_identifier, CustomIdent, SelectorParseErrorKind}; +use crate::Atom; +use cssparser::{Parser, Token}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalSizeOf}; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +// FIXME(emilio): The system font code is copy-pasta, and should be cleaned up. +macro_rules! system_font_methods { + ($ty:ident, $field:ident) => { + system_font_methods!($ty); + + fn compute_system(&self, _context: &Context) -> <$ty as ToComputedValue>::ComputedValue { + debug_assert!(matches!(*self, $ty::System(..))); + #[cfg(feature = "gecko")] + { + _context.cached_system_font.as_ref().unwrap().$field.clone() + } + #[cfg(feature = "servo")] + { + unreachable!() + } + } + }; + + ($ty:ident) => { + /// Get a specified value that represents a system font. + pub fn system_font(f: SystemFont) -> Self { + $ty::System(f) + } + + /// Retreive a SystemFont from the specified value. + pub fn get_system(&self) -> Option<SystemFont> { + if let $ty::System(s) = *self { + Some(s) + } else { + None + } + } + }; +} + +/// System fonts. +#[repr(u8)] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum SystemFont { + /// https://drafts.csswg.org/css-fonts/#valdef-font-caption + Caption, + /// https://drafts.csswg.org/css-fonts/#valdef-font-icon + Icon, + /// https://drafts.csswg.org/css-fonts/#valdef-font-menu + Menu, + /// https://drafts.csswg.org/css-fonts/#valdef-font-message-box + MessageBox, + /// https://drafts.csswg.org/css-fonts/#valdef-font-small-caption + SmallCaption, + /// https://drafts.csswg.org/css-fonts/#valdef-font-status-bar + StatusBar, + /// Internal system font, used by the `<menupopup>`s on macOS. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozPullDownMenu, + /// Internal system font, used for `<button>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButton, + /// Internal font, used by `<select>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozList, + /// Internal font, used by `<input>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozField, + #[css(skip)] + End, // Just for indexing purposes. +} + +const DEFAULT_SCRIPT_MIN_SIZE_PT: u32 = 8; +const DEFAULT_SCRIPT_SIZE_MULTIPLIER: f64 = 0.71; + +/// The minimum font-weight value per: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values +pub const MIN_FONT_WEIGHT: f32 = 1.; + +/// The maximum font-weight value per: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values +pub const MAX_FONT_WEIGHT: f32 = 1000.; + +/// A specified font-weight value. +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontWeight { + /// `<font-weight-absolute>` + Absolute(AbsoluteFontWeight), + /// Bolder variant + Bolder, + /// Lighter variant + Lighter, + /// System font variant. + #[css(skip)] + System(SystemFont), +} + +impl FontWeight { + system_font_methods!(FontWeight, font_weight); + + /// `normal` + #[inline] + pub fn normal() -> Self { + FontWeight::Absolute(AbsoluteFontWeight::Normal) + } + + /// Get a specified FontWeight from a gecko keyword + pub fn from_gecko_keyword(kw: u32) -> Self { + debug_assert!(kw % 100 == 0); + debug_assert!(kw as f32 <= MAX_FONT_WEIGHT); + FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::new(kw as f32))) + } +} + +impl ToComputedValue for FontWeight { + type ComputedValue = computed::FontWeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontWeight::Absolute(ref abs) => abs.compute(), + FontWeight::Bolder => context + .builder + .get_parent_font() + .clone_font_weight() + .bolder(), + FontWeight::Lighter => context + .builder + .get_parent_font() + .clone_font_weight() + .lighter(), + FontWeight::System(_) => self.compute_system(context), + } + } + + #[inline] + fn from_computed_value(computed: &computed::FontWeight) -> Self { + FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::from_computed_value( + &computed.value(), + ))) + } +} + +/// An absolute font-weight value for a @font-face rule. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-absolute-values +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AbsoluteFontWeight { + /// A `<number>`, with the additional constraints specified in: + /// + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + Weight(Number), + /// Normal font weight. Same as 400. + Normal, + /// Bold font weight. Same as 700. + Bold, +} + +impl AbsoluteFontWeight { + /// Returns the computed value for this absolute font weight. + pub fn compute(&self) -> computed::FontWeight { + match *self { + AbsoluteFontWeight::Weight(weight) => computed::FontWeight::from_float(weight.get()), + AbsoluteFontWeight::Normal => computed::FontWeight::NORMAL, + AbsoluteFontWeight::Bold => computed::FontWeight::BOLD, + } + } +} + +impl Parse for AbsoluteFontWeight { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(number) = input.try_parse(|input| Number::parse(context, input)) { + // We could add another AllowedNumericType value, but it doesn't + // seem worth it just for a single property with such a weird range, + // so we do the clamping here manually. + if !number.was_calc() && + (number.get() < MIN_FONT_WEIGHT || number.get() > MAX_FONT_WEIGHT) + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(AbsoluteFontWeight::Weight(number)); + } + + Ok(try_match_ident_ignore_ascii_case! { input, + "normal" => AbsoluteFontWeight::Normal, + "bold" => AbsoluteFontWeight::Bold, + }) + } +} + +/// The specified value of the `font-style` property, without the system font +/// crap. +pub type SpecifiedFontStyle = generics::FontStyle<Angle>; + +impl ToCss for SpecifiedFontStyle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + generics::FontStyle::Normal => dest.write_str("normal"), + generics::FontStyle::Italic => dest.write_str("italic"), + generics::FontStyle::Oblique(ref angle) => { + dest.write_str("oblique")?; + if *angle != Self::default_angle() { + dest.write_char(' ')?; + angle.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl Parse for SpecifiedFontStyle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "normal" => generics::FontStyle::Normal, + "italic" => generics::FontStyle::Italic, + "oblique" => { + let angle = input.try_parse(|input| Self::parse_angle(context, input)) + .unwrap_or_else(|_| Self::default_angle()); + + generics::FontStyle::Oblique(angle) + }, + }) + } +} + +impl ToComputedValue for SpecifiedFontStyle { + type ComputedValue = computed::FontStyle; + + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + match *self { + Self::Normal => computed::FontStyle::NORMAL, + Self::Italic => computed::FontStyle::ITALIC, + Self::Oblique(ref angle) => computed::FontStyle::oblique(angle.degrees()), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + if *computed == computed::FontStyle::NORMAL { + return Self::Normal; + } + if *computed == computed::FontStyle::ITALIC { + return Self::Italic; + } + let degrees = computed.oblique_degrees(); + generics::FontStyle::Oblique(Angle::from_degrees(degrees, /* was_calc = */ false)) + } +} + +/// From https://drafts.csswg.org/css-fonts-4/#valdef-font-style-oblique-angle: +/// +/// Values less than -90deg or values greater than 90deg are +/// invalid and are treated as parse errors. +/// +/// The maximum angle value that `font-style: oblique` should compute to. +pub const FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES: f32 = 90.; + +/// The minimum angle value that `font-style: oblique` should compute to. +pub const FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES: f32 = -90.; + +impl SpecifiedFontStyle { + /// Gets a clamped angle in degrees from a specified Angle. + pub fn compute_angle_degrees(angle: &Angle) -> f32 { + angle + .degrees() + .max(FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES) + .min(FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES) + } + + /// Parse a suitable angle for font-style: oblique. + pub fn parse_angle<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Angle, ParseError<'i>> { + let angle = Angle::parse(context, input)?; + if angle.was_calc() { + return Ok(angle); + } + + let degrees = angle.degrees(); + if degrees < FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES || + degrees > FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(angle); + } + + /// The default angle for `font-style: oblique`. + pub fn default_angle() -> Angle { + Angle::from_degrees( + computed::FontStyle::DEFAULT_OBLIQUE_DEGREES as f32, + /* was_calc = */ false, + ) + } +} + +/// The specified value of the `font-style` property. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum FontStyle { + Specified(SpecifiedFontStyle), + #[css(skip)] + System(SystemFont), +} + +impl FontStyle { + /// Return the `normal` value. + #[inline] + pub fn normal() -> Self { + FontStyle::Specified(generics::FontStyle::Normal) + } + + system_font_methods!(FontStyle, font_style); +} + +impl ToComputedValue for FontStyle { + type ComputedValue = computed::FontStyle; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStyle::Specified(ref specified) => specified.to_computed_value(context), + FontStyle::System(..) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStyle::Specified(SpecifiedFontStyle::from_computed_value(computed)) + } +} + +/// A value for the `font-stretch` property. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop +#[allow(missing_docs)] +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontStretch { + Stretch(NonNegativePercentage), + Keyword(FontStretchKeyword), + #[css(skip)] + System(SystemFont), +} + +/// A keyword value for `font-stretch`. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum FontStretchKeyword { + Normal, + Condensed, + UltraCondensed, + ExtraCondensed, + SemiCondensed, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} + +impl FontStretchKeyword { + /// Turns the keyword into a computed value. + pub fn compute(&self) -> computed::FontStretch { + computed::FontStretch::from_keyword(*self) + } + + /// Does the opposite operation to `compute`, in order to serialize keywords + /// if possible. + pub fn from_percentage(p: f32) -> Option<Self> { + computed::FontStretch::from_percentage(p).as_keyword() + } +} + +impl FontStretch { + /// `normal`. + pub fn normal() -> Self { + FontStretch::Keyword(FontStretchKeyword::Normal) + } + + system_font_methods!(FontStretch, font_stretch); +} + +impl ToComputedValue for FontStretch { + type ComputedValue = computed::FontStretch; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStretch::Stretch(ref percentage) => { + let percentage = percentage.to_computed_value(context).0; + computed::FontStretch::from_percentage(percentage.0) + }, + FontStretch::Keyword(ref kw) => kw.compute(), + FontStretch::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStretch::Stretch(NonNegativePercentage::from_computed_value(&NonNegative( + computed.to_percentage(), + ))) + } +} + +#[cfg(feature = "gecko")] +fn math_depth_enabled(_context: &ParserContext) -> bool { + static_prefs::pref!("layout.css.math-depth.enabled") +} + +#[cfg(feature = "servo")] +fn math_depth_enabled(_context: &ParserContext) -> bool { + false +} + +/// CSS font keywords +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum FontSizeKeyword { + #[css(keyword = "xx-small")] + XXSmall, + XSmall, + Small, + Medium, + Large, + XLarge, + #[css(keyword = "xx-large")] + XXLarge, + #[css(keyword = "xxx-large")] + XXXLarge, + /// Indicate whether to apply font-size: math is specified so that extra + /// scaling due to math-depth changes is applied during the cascade. + #[parse(condition = "math_depth_enabled")] + Math, + #[css(skip)] + None, +} + +impl FontSizeKeyword { + /// Convert to an HTML <font size> value + #[inline] + pub fn html_size(self) -> u8 { + self as u8 + } +} + +impl Default for FontSizeKeyword { + fn default() -> Self { + FontSizeKeyword::Medium + } +} + +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))] +/// Additional information for keyword-derived font sizes. +pub struct KeywordInfo { + /// The keyword used + pub kw: FontSizeKeyword, + /// A factor to be multiplied by the computed size of the keyword + #[css(skip)] + pub factor: f32, + /// An additional fixed offset to add to the kw * factor in the case of + /// `calc()`. + #[css(skip)] + pub offset: CSSPixelLength, +} + +impl KeywordInfo { + /// KeywordInfo value for font-size: medium + pub fn medium() -> Self { + Self::new(FontSizeKeyword::Medium) + } + + /// KeywordInfo value for font-size: none + pub fn none() -> Self { + Self::new(FontSizeKeyword::None) + } + + fn new(kw: FontSizeKeyword) -> Self { + KeywordInfo { + kw, + factor: 1., + offset: CSSPixelLength::new(0.), + } + } + + /// Computes the final size for this font-size keyword, accounting for + /// text-zoom. + fn to_computed_value(&self, context: &Context) -> CSSPixelLength { + debug_assert_ne!(self.kw, FontSizeKeyword::None); + debug_assert_ne!(self.kw, FontSizeKeyword::Math); + let base = context.maybe_zoom_text(self.kw.to_length(context).0); + base * self.factor + context.maybe_zoom_text(self.offset) + } + + /// Given a parent keyword info (self), apply an additional factor/offset to + /// it. + fn compose(self, factor: f32) -> Self { + if self.kw == FontSizeKeyword::None { + return self; + } + KeywordInfo { + kw: self.kw, + factor: self.factor * factor, + offset: self.offset * factor, + } + } +} + +impl SpecifiedValueInfo for KeywordInfo { + fn collect_completion_keywords(f: KeywordsCollectFn) { + <FontSizeKeyword as SpecifiedValueInfo>::collect_completion_keywords(f); + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// A specified font-size value +pub enum FontSize { + /// A length; e.g. 10px. + Length(LengthPercentage), + /// A keyword value, along with a ratio and absolute offset. + /// The ratio in any specified keyword value + /// will be 1 (with offset 0), but we cascade keywordness even + /// after font-relative (percent and em) values + /// have been applied, which is where the ratio + /// comes in. The offset comes in if we cascaded a calc value, + /// where the font-relative portion (em and percentage) will + /// go into the ratio, and the remaining units all computed together + /// will go into the offset. + /// See bug 1355707. + Keyword(KeywordInfo), + /// font-size: smaller + Smaller, + /// font-size: larger + Larger, + /// Derived from a specified system font. + #[css(skip)] + System(SystemFont), +} + +/// Specifies a prioritized list of font family names or generic family names. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[cfg_attr(feature = "servo", derive(Hash))] +pub enum FontFamily { + /// List of `font-family` + #[css(comma)] + Values(#[css(iterable)] FontFamilyList), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontFamily { + system_font_methods!(FontFamily, font_family); +} + +impl ToComputedValue for FontFamily { + type ComputedValue = computed::FontFamily; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontFamily::Values(ref list) => computed::FontFamily { + families: list.clone(), + is_system_font: false, + is_initial: false, + }, + FontFamily::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontFamily) -> Self { + FontFamily::Values(other.families.clone()) + } +} + +#[cfg(feature = "gecko")] +impl MallocSizeOf for FontFamily { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + FontFamily::Values(ref v) => { + // Although the family list is refcounted, we always attribute + // its size to the specified value. + v.list.unconditional_size_of(ops) + }, + FontFamily::System(_) => 0, + } + } +} + +impl Parse for FontFamily { + /// <family-name># + /// <family-name> = <string> | [ <ident>+ ] + /// TODO: <generic-family> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontFamily, ParseError<'i>> { + let values = + input.parse_comma_separated(|input| SingleFontFamily::parse(context, input))?; + Ok(FontFamily::Values(FontFamilyList { + list: crate::ArcSlice::from_iter(values.into_iter()), + })) + } +} + +impl SpecifiedValueInfo for FontFamily {} + +/// `FamilyName::parse` is based on `SingleFontFamily::parse` and not the other +/// way around because we want the former to exclude generic family keywords. +impl Parse for FamilyName { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + match SingleFontFamily::parse(context, input) { + Ok(SingleFontFamily::FamilyName(name)) => Ok(name), + Ok(SingleFontFamily::Generic(_)) => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Err(e) => Err(e), + } + } +} + +/// A factor for one of the font-size-adjust metrics, which may be either a number +/// or the `from-font` keyword. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontSizeAdjustFactor { + /// An explicitly-specified number. + Number(NonNegativeNumber), + /// The from-font keyword: resolve the number from font metrics. + FromFont, +} + +/// Specified value for font-size-adjust, intended to help +/// preserve the readability of text when font fallback occurs. +/// +/// https://drafts.csswg.org/css-fonts-5/#font-size-adjust-prop +pub type FontSizeAdjust = generics::GenericFontSizeAdjust<FontSizeAdjustFactor>; + +impl Parse for FontSizeAdjust { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + // First check if we have an adjustment factor without a metrics-basis keyword. + if let Ok(factor) = input.try_parse(|i| FontSizeAdjustFactor::parse(context, i)) { + return Ok(Self::ExHeight(factor)); + } + + let ident = input.expect_ident()?; + let basis = match_ignore_ascii_case! { &ident, + "none" => return Ok(Self::None), + // Check for size adjustment basis keywords. + "ex-height" => Self::ExHeight, + "cap-height" => Self::CapHeight, + "ch-width" => Self::ChWidth, + "ic-width" => Self::IcWidth, + "ic-height" => Self::IcHeight, + // Unknown keyword. + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + }; + + Ok(basis(FontSizeAdjustFactor::parse(context, input)?)) + } +} + +/// This is the ratio applied for font-size: larger +/// and smaller by both Firefox and Chrome +const LARGER_FONT_SIZE_RATIO: f32 = 1.2; + +/// The default font size. +pub const FONT_MEDIUM_PX: f32 = 16.0; +/// The default line height. +pub const FONT_MEDIUM_LINE_HEIGHT_PX: f32 = FONT_MEDIUM_PX * 1.2; + +impl FontSizeKeyword { + #[inline] + #[cfg(feature = "servo")] + fn to_length(&self, _: &Context) -> NonNegativeLength { + let medium = Length::new(FONT_MEDIUM_PX); + // https://drafts.csswg.org/css-fonts-3/#font-size-prop + NonNegative(match *self { + FontSizeKeyword::XXSmall => medium * 3.0 / 5.0, + FontSizeKeyword::XSmall => medium * 3.0 / 4.0, + FontSizeKeyword::Small => medium * 8.0 / 9.0, + FontSizeKeyword::Medium => medium, + FontSizeKeyword::Large => medium * 6.0 / 5.0, + FontSizeKeyword::XLarge => medium * 3.0 / 2.0, + FontSizeKeyword::XXLarge => medium * 2.0, + FontSizeKeyword::XXXLarge => medium * 3.0, + FontSizeKeyword::Math | FontSizeKeyword::None => unreachable!(), + }) + } + + #[cfg(feature = "gecko")] + #[inline] + fn to_length(&self, cx: &Context) -> NonNegativeLength { + let font = cx.style().get_font(); + let family = &font.mFont.family.families; + let generic = family + .single_generic() + .unwrap_or(computed::GenericFontFamily::None); + let base_size = unsafe { + Atom::with(font.mLanguage.mRawPtr, |language| { + cx.device().base_size_for_generic(language, generic) + }) + }; + self.to_length_without_context(cx.quirks_mode, base_size) + } + + /// Resolve a keyword length without any context, with explicit arguments. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_length_without_context( + &self, + quirks_mode: QuirksMode, + base_size: Length, + ) -> NonNegativeLength { + debug_assert_ne!(*self, FontSizeKeyword::Math); + // The tables in this function are originally from + // nsRuleNode::CalcFontPointSize in Gecko: + // + // https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#3150 + // + // Mapping from base size and HTML size to pixels + // The first index is (base_size - 9), the second is the + // HTML size. "0" is CSS keyword xx-small, not HTML size 0, + // since HTML size 0 is the same as 1. + // + // xxs xs s m l xl xxl - + // - 0/1 2 3 4 5 6 7 + static FONT_SIZE_MAPPING: [[i32; 8]; 8] = [ + [9, 9, 9, 9, 11, 14, 18, 27], + [9, 9, 9, 10, 12, 15, 20, 30], + [9, 9, 10, 11, 13, 17, 22, 33], + [9, 9, 10, 12, 14, 18, 24, 36], + [9, 10, 12, 13, 16, 20, 26, 39], + [9, 10, 12, 14, 17, 21, 28, 42], + [9, 10, 13, 15, 18, 23, 30, 45], + [9, 10, 13, 16, 18, 24, 32, 48], + ]; + + // This table gives us compatibility with WinNav4 for the default fonts only. + // In WinNav4, the default fonts were: + // + // Times/12pt == Times/16px at 96ppi + // Courier/10pt == Courier/13px at 96ppi + // + // xxs xs s m l xl xxl - + // - 1 2 3 4 5 6 7 + static QUIRKS_FONT_SIZE_MAPPING: [[i32; 8]; 8] = [ + [9, 9, 9, 9, 11, 14, 18, 28], + [9, 9, 9, 10, 12, 15, 20, 31], + [9, 9, 9, 11, 13, 17, 22, 34], + [9, 9, 10, 12, 14, 18, 24, 37], + [9, 9, 10, 13, 16, 20, 26, 40], + [9, 9, 11, 14, 17, 21, 28, 42], + [9, 10, 12, 15, 17, 23, 30, 45], + [9, 10, 13, 16, 18, 24, 32, 48], + ]; + + static FONT_SIZE_FACTORS: [i32; 8] = [60, 75, 89, 100, 120, 150, 200, 300]; + let base_size_px = base_size.px().round() as i32; + let html_size = self.html_size() as usize; + NonNegative(if base_size_px >= 9 && base_size_px <= 16 { + let mapping = if quirks_mode == QuirksMode::Quirks { + QUIRKS_FONT_SIZE_MAPPING + } else { + FONT_SIZE_MAPPING + }; + Length::new(mapping[(base_size_px - 9) as usize][html_size] as f32) + } else { + base_size * FONT_SIZE_FACTORS[html_size] as f32 / 100.0 + }) + } +} + +impl FontSize { + /// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-font-size> + pub fn from_html_size(size: u8) -> Self { + FontSize::Keyword(KeywordInfo::new(match size { + // If value is less than 1, let it be 1. + 0 | 1 => FontSizeKeyword::XSmall, + 2 => FontSizeKeyword::Small, + 3 => FontSizeKeyword::Medium, + 4 => FontSizeKeyword::Large, + 5 => FontSizeKeyword::XLarge, + 6 => FontSizeKeyword::XXLarge, + // If value is greater than 7, let it be 7. + _ => FontSizeKeyword::XXXLarge, + })) + } + + /// Compute it against a given base font size + pub fn to_computed_value_against( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> computed::FontSize { + let compose_keyword = |factor| { + context + .style() + .get_parent_font() + .clone_font_size() + .keyword_info + .compose(factor) + }; + let mut info = KeywordInfo::none(); + let size = match *self { + FontSize::Length(LengthPercentage::Length(ref l)) => { + if let NoCalcLength::FontRelative(ref value) = *l { + if let FontRelativeLength::Em(em) = *value { + // If the parent font was keyword-derived, this is + // too. Tack the em unit onto the factor + info = compose_keyword(em); + } + } + let result = + l.to_computed_value_with_base_size(context, base_size, line_height_base); + if l.should_zoom_text() { + context.maybe_zoom_text(result) + } else { + result + } + }, + FontSize::Length(LengthPercentage::Percentage(pc)) => { + // If the parent font was keyword-derived, this is too. + // Tack the % onto the factor + info = compose_keyword(pc.0); + (base_size.resolve(context).computed_size() * pc.0).normalized() + }, + FontSize::Length(LengthPercentage::Calc(ref calc)) => { + let calc = calc.to_computed_value_zoomed(context, base_size, line_height_base); + calc.resolve(base_size.resolve(context).computed_size()) + }, + FontSize::Keyword(i) => { + if i.kw == FontSizeKeyword::Math { + // Scaling is done in recompute_math_font_size_if_needed(). + info = compose_keyword(1.); + info.kw = FontSizeKeyword::Math; + FontRelativeLength::Em(1.).to_computed_value( + context, + base_size, + line_height_base, + ) + } else { + // As a specified keyword, this is keyword derived + info = i; + i.to_computed_value(context).clamp_to_non_negative() + } + }, + FontSize::Smaller => { + info = compose_keyword(1. / LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(1. / LARGER_FONT_SIZE_RATIO).to_computed_value( + context, + base_size, + line_height_base, + ) + }, + FontSize::Larger => { + info = compose_keyword(LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(LARGER_FONT_SIZE_RATIO).to_computed_value( + context, + base_size, + line_height_base, + ) + }, + + FontSize::System(_) => { + #[cfg(feature = "servo")] + { + unreachable!() + } + #[cfg(feature = "gecko")] + { + context + .cached_system_font + .as_ref() + .unwrap() + .font_size + .computed_size() + } + }, + }; + computed::FontSize { + computed_size: NonNegative(size), + used_size: NonNegative(size), + keyword_info: info, + } + } +} + +impl ToComputedValue for FontSize { + type ComputedValue = computed::FontSize; + + #[inline] + fn to_computed_value(&self, context: &Context) -> computed::FontSize { + self.to_computed_value_against( + context, + FontBaseSize::InheritedStyle, + LineHeightBase::InheritedStyle, + ) + } + + #[inline] + fn from_computed_value(computed: &computed::FontSize) -> Self { + FontSize::Length(LengthPercentage::Length( + ToComputedValue::from_computed_value(&computed.computed_size()), + )) + } +} + +impl FontSize { + system_font_methods!(FontSize); + + /// Get initial value for specified font size. + #[inline] + pub fn medium() -> Self { + FontSize::Keyword(KeywordInfo::medium()) + } + + /// Parses a font-size, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<FontSize, ParseError<'i>> { + if let Ok(lp) = input + .try_parse(|i| LengthPercentage::parse_non_negative_quirky(context, i, allow_quirks)) + { + return Ok(FontSize::Length(lp)); + } + + if let Ok(kw) = input.try_parse(|i| FontSizeKeyword::parse(context, i)) { + return Ok(FontSize::Keyword(KeywordInfo::new(kw))); + } + + try_match_ident_ignore_ascii_case! { input, + "smaller" => Ok(FontSize::Smaller), + "larger" => Ok(FontSize::Larger), + } + } +} + +impl Parse for FontSize { + /// <length> | <percentage> | <absolute-size> | <relative-size> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontSize, ParseError<'i>> { + FontSize::parse_quirky(context, input, AllowQuirks::No) + } +} + +bitflags! { + #[derive(Clone, Copy)] + /// Flags of variant alternates in bit + struct VariantAlternatesParsingFlags: u8 { + /// None of variant alternates enabled + const NORMAL = 0; + /// Historical forms + const HISTORICAL_FORMS = 0x01; + /// Stylistic Alternates + const STYLISTIC = 0x02; + /// Stylistic Sets + const STYLESET = 0x04; + /// Character Variant + const CHARACTER_VARIANT = 0x08; + /// Swash glyphs + const SWASH = 0x10; + /// Ornaments glyphs + const ORNAMENTS = 0x20; + /// Annotation forms + const ANNOTATION = 0x40; + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// Set of variant alternates +pub enum VariantAlternates { + /// Enables display of stylistic alternates + #[css(function)] + Stylistic(CustomIdent), + /// Enables display with stylistic sets + #[css(comma, function)] + Styleset(#[css(iterable)] crate::OwnedSlice<CustomIdent>), + /// Enables display of specific character variants + #[css(comma, function)] + CharacterVariant(#[css(iterable)] crate::OwnedSlice<CustomIdent>), + /// Enables display of swash glyphs + #[css(function)] + Swash(CustomIdent), + /// Enables replacement of default glyphs with ornaments + #[css(function)] + Ornaments(CustomIdent), + /// Enables display of alternate annotation forms + #[css(function)] + Annotation(CustomIdent), + /// Enables display of historical forms + HistoricalForms, +} + +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +/// List of Variant Alternates +pub struct FontVariantAlternates( + #[css(if_empty = "normal", iterable)] crate::OwnedSlice<VariantAlternates>, +); + +impl FontVariantAlternates { + /// Returns the length of all variant alternates. + pub fn len(&self) -> usize { + self.0.iter().fold(0, |acc, alternate| match *alternate { + VariantAlternates::Swash(_) | + VariantAlternates::Stylistic(_) | + VariantAlternates::Ornaments(_) | + VariantAlternates::Annotation(_) => acc + 1, + VariantAlternates::Styleset(ref slice) | + VariantAlternates::CharacterVariant(ref slice) => acc + slice.len(), + _ => acc, + }) + } +} + +impl FontVariantAlternates { + #[inline] + /// Get initial specified value with VariantAlternatesList + pub fn get_initial_specified_value() -> Self { + Default::default() + } +} + +impl Parse for FontVariantAlternates { + /// 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>) ] + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariantAlternates, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(Default::default()); + } + + let mut stylistic = None; + let mut historical = None; + let mut styleset = None; + let mut character_variant = None; + let mut swash = None; + let mut ornaments = None; + let mut annotation = None; + + // Parse values for the various alternate types in any order. + let mut parsed_alternates = VariantAlternatesParsingFlags::empty(); + macro_rules! check_if_parsed( + ($input:expr, $flag:path) => ( + if parsed_alternates.contains($flag) { + return Err($input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + parsed_alternates |= $flag; + ) + ); + while let Ok(_) = input.try_parse(|input| match *input.next()? { + Token::Ident(ref value) if value.eq_ignore_ascii_case("historical-forms") => { + check_if_parsed!(input, VariantAlternatesParsingFlags::HISTORICAL_FORMS); + historical = Some(VariantAlternates::HistoricalForms); + Ok(()) + }, + Token::Function(ref name) => { + let name = name.clone(); + input.parse_nested_block(|i| { + match_ignore_ascii_case! { &name, + "swash" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::SWASH); + let ident = CustomIdent::parse(i, &[])?; + swash = Some(VariantAlternates::Swash(ident)); + Ok(()) + }, + "stylistic" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLISTIC); + let ident = CustomIdent::parse(i, &[])?; + stylistic = Some(VariantAlternates::Stylistic(ident)); + Ok(()) + }, + "ornaments" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ORNAMENTS); + let ident = CustomIdent::parse(i, &[])?; + ornaments = Some(VariantAlternates::Ornaments(ident)); + Ok(()) + }, + "annotation" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ANNOTATION); + let ident = CustomIdent::parse(i, &[])?; + annotation = Some(VariantAlternates::Annotation(ident)); + Ok(()) + }, + "styleset" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLESET); + let idents = i.parse_comma_separated(|i| { + CustomIdent::parse(i, &[]) + })?; + styleset = Some(VariantAlternates::Styleset(idents.into())); + Ok(()) + }, + "character-variant" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::CHARACTER_VARIANT); + let idents = i.parse_comma_separated(|i| { + CustomIdent::parse(i, &[]) + })?; + character_variant = Some(VariantAlternates::CharacterVariant(idents.into())); + Ok(()) + }, + _ => return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + }) + }, + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }) {} + + if parsed_alternates.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // Collect the parsed values in canonical order, so that we'll serialize correctly. + let mut alternates = Vec::new(); + macro_rules! push_if_some( + ($value:expr) => ( + if let Some(v) = $value { + alternates.push(v); + } + ) + ); + push_if_some!(stylistic); + push_if_some!(historical); + push_if_some!(styleset); + push_if_some!(character_variant); + push_if_some!(swash); + push_if_some!(ornaments); + push_if_some!(annotation); + + Ok(FontVariantAlternates(alternates.into())) + } +} + +macro_rules! impl_variant_east_asian { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants for east asian variant + pub struct FontVariantEastAsian(u16); + bitflags! { + impl FontVariantEastAsian: u16 { + /// None of the features + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantEastAsian { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(Self::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_east_asian_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u16, FontVariantEastAsian::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantEastAsian { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_east_asian! { + /// Enables rendering of JIS78 forms (OpenType feature: jp78) + JIS78 / "jis78" => NS_FONT_VARIANT_EAST_ASIAN_JIS78 = 0x01, + /// Enables rendering of JIS83 forms (OpenType feature: jp83). + JIS83 / "jis83" => NS_FONT_VARIANT_EAST_ASIAN_JIS83 = 0x02, + /// Enables rendering of JIS90 forms (OpenType feature: jp90). + JIS90 / "jis90" => NS_FONT_VARIANT_EAST_ASIAN_JIS90 = 0x04, + /// Enables rendering of JIS2004 forms (OpenType feature: jp04). + JIS04 / "jis04" => NS_FONT_VARIANT_EAST_ASIAN_JIS04 = 0x08, + /// Enables rendering of simplified forms (OpenType feature: smpl). + SIMPLIFIED / "simplified" => NS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED = 0x10, + /// Enables rendering of traditional forms (OpenType feature: trad). + TRADITIONAL / "traditional" => NS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL = 0x20, + /// Enables rendering of full-width variants (OpenType feature: fwid). + FULL_WIDTH / "full-width" => NS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH = 0x40, + /// Enables rendering of proportionally-spaced variants (OpenType feature: pwid). + PROPORTIONAL_WIDTH / "proportional-width" => NS_FONT_VARIANT_EAST_ASIAN_PROP_WIDTH = 0x80, + /// Enables display of ruby variant glyphs (OpenType feature: ruby). + RUBY / "ruby" => NS_FONT_VARIANT_EAST_ASIAN_RUBY = 0x100, +} + +#[cfg(feature = "gecko")] +impl FontVariantEastAsian { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u16) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u16 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantEastAsian, u16); + +impl Parse for FontVariantEastAsian { + /// 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 ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "jis78" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS78), + "jis83" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS83), + "jis90" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS90), + "jis04" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS04), + "simplified" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::SIMPLIFIED), + "traditional" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::TRADITIONAL), + "full-width" => + exclusive_value!((result, Self::FULL_WIDTH | + Self::PROPORTIONAL_WIDTH + ) => Self::FULL_WIDTH), + "proportional-width" => + exclusive_value!((result, Self::FULL_WIDTH | + Self::PROPORTIONAL_WIDTH + ) => Self::PROPORTIONAL_WIDTH), + "ruby" => + exclusive_value!((result, Self::RUBY) => Self::RUBY), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_ligatures { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants of ligatures + pub struct FontVariantLigatures(u16); + bitflags! { + impl FontVariantLigatures: u16 { + /// Specifies that common default features are enabled + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantLigatures { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + if self.contains(FontVariantLigatures::NONE) { + return dest.write_str("none"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(FontVariantLigatures::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_ligatures_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u16, FontVariantLigatures::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantLigatures { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_ligatures! { + /// Specifies that all types of ligatures and contextual forms + /// covered by this property are explicitly disabled + NONE / "none" => NS_FONT_VARIANT_LIGATURES_NONE = 0x01, + /// Enables display of common ligatures + COMMON_LIGATURES / "common-ligatures" => NS_FONT_VARIANT_LIGATURES_COMMON = 0x02, + /// Disables display of common ligatures + NO_COMMON_LIGATURES / "no-common-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_COMMON = 0x04, + /// Enables display of discretionary ligatures + DISCRETIONARY_LIGATURES / "discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_DISCRETIONARY = 0x08, + /// Disables display of discretionary ligatures + NO_DISCRETIONARY_LIGATURES / "no-discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_DISCRETIONARY = 0x10, + /// Enables display of historical ligatures + HISTORICAL_LIGATURES / "historical-ligatures" => NS_FONT_VARIANT_LIGATURES_HISTORICAL = 0x20, + /// Disables display of historical ligatures + NO_HISTORICAL_LIGATURES / "no-historical-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_HISTORICAL = 0x40, + /// Enables display of contextual alternates + CONTEXTUAL / "contextual" => NS_FONT_VARIANT_LIGATURES_CONTEXTUAL = 0x80, + /// Disables display of contextual alternates + NO_CONTEXTUAL / "no-contextual" => NS_FONT_VARIANT_LIGATURES_NO_CONTEXTUAL = 0x100, +} + +#[cfg(feature = "gecko")] +impl FontVariantLigatures { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u16) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u16 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantLigatures, u16); + +impl Parse for FontVariantLigatures { + /// 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 ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(Self::NONE); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "common-ligatures" => + exclusive_value!((result, Self::COMMON_LIGATURES | + Self::NO_COMMON_LIGATURES + ) => Self::COMMON_LIGATURES), + "no-common-ligatures" => + exclusive_value!((result, Self::COMMON_LIGATURES | + Self::NO_COMMON_LIGATURES + ) => Self::NO_COMMON_LIGATURES), + "discretionary-ligatures" => + exclusive_value!((result, Self::DISCRETIONARY_LIGATURES | + Self::NO_DISCRETIONARY_LIGATURES + ) => Self::DISCRETIONARY_LIGATURES), + "no-discretionary-ligatures" => + exclusive_value!((result, Self::DISCRETIONARY_LIGATURES | + Self::NO_DISCRETIONARY_LIGATURES + ) => Self::NO_DISCRETIONARY_LIGATURES), + "historical-ligatures" => + exclusive_value!((result, Self::HISTORICAL_LIGATURES | + Self::NO_HISTORICAL_LIGATURES + ) => Self::HISTORICAL_LIGATURES), + "no-historical-ligatures" => + exclusive_value!((result, Self::HISTORICAL_LIGATURES | + Self::NO_HISTORICAL_LIGATURES + ) => Self::NO_HISTORICAL_LIGATURES), + "contextual" => + exclusive_value!((result, Self::CONTEXTUAL | + Self::NO_CONTEXTUAL + ) => Self::CONTEXTUAL), + "no-contextual" => + exclusive_value!((result, Self::CONTEXTUAL | + Self::NO_CONTEXTUAL + ) => Self::NO_CONTEXTUAL), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_numeric { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants of numeric values + pub struct FontVariantNumeric(u8); + bitflags! { + impl FontVariantNumeric: u8 { + /// None of other variants are enabled. + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantNumeric { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(FontVariantNumeric::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_numeric_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u8, FontVariantNumeric::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantNumeric { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_numeric! { + /// Enables display of lining numerals. + LINING_NUMS / "lining-nums" => NS_FONT_VARIANT_NUMERIC_LINING = 0x01, + /// Enables display of old-style numerals. + OLDSTYLE_NUMS / "oldstyle-nums" => NS_FONT_VARIANT_NUMERIC_OLDSTYLE = 0x02, + /// Enables display of proportional numerals. + PROPORTIONAL_NUMS / "proportional-nums" => NS_FONT_VARIANT_NUMERIC_PROPORTIONAL = 0x04, + /// Enables display of tabular numerals. + TABULAR_NUMS / "tabular-nums" => NS_FONT_VARIANT_NUMERIC_TABULAR = 0x08, + /// Enables display of lining diagonal fractions. + DIAGONAL_FRACTIONS / "diagonal-fractions" => NS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS = 0x10, + /// Enables display of lining stacked fractions. + STACKED_FRACTIONS / "stacked-fractions" => NS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS = 0x20, + /// Enables display of letter forms used with ordinal numbers. + ORDINAL / "ordinal" => NS_FONT_VARIANT_NUMERIC_ORDINAL = 0x80, + /// Enables display of slashed zeros. + SLASHED_ZERO / "slashed-zero" => NS_FONT_VARIANT_NUMERIC_SLASHZERO = 0x40, +} + +#[cfg(feature = "gecko")] +impl FontVariantNumeric { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u8) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u8 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantNumeric, u8); + +impl Parse for FontVariantNumeric { + /// 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 ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "ordinal" => + exclusive_value!((result, Self::ORDINAL) => Self::ORDINAL), + "slashed-zero" => + exclusive_value!((result, Self::SLASHED_ZERO) => Self::SLASHED_ZERO), + "lining-nums" => + exclusive_value!((result, Self::LINING_NUMS | + Self::OLDSTYLE_NUMS + ) => Self::LINING_NUMS), + "oldstyle-nums" => + exclusive_value!((result, Self::LINING_NUMS | + Self::OLDSTYLE_NUMS + ) => Self::OLDSTYLE_NUMS), + "proportional-nums" => + exclusive_value!((result, Self::PROPORTIONAL_NUMS | + Self::TABULAR_NUMS + ) => Self::PROPORTIONAL_NUMS), + "tabular-nums" => + exclusive_value!((result, Self::PROPORTIONAL_NUMS | + Self::TABULAR_NUMS + ) => Self::TABULAR_NUMS), + "diagonal-fractions" => + exclusive_value!((result, Self::DIAGONAL_FRACTIONS | + Self::STACKED_FRACTIONS + ) => Self::DIAGONAL_FRACTIONS), + "stacked-fractions" => + exclusive_value!((result, Self::DIAGONAL_FRACTIONS | + Self::STACKED_FRACTIONS + ) => Self::STACKED_FRACTIONS), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// This property provides low-level control over OpenType or TrueType font features. +pub type FontFeatureSettings = FontSettings<FeatureTagValue<Integer>>; + +/// For font-language-override, use the same representation as the computed value. +pub use crate::values::computed::font::FontLanguageOverride; + +impl Parse for FontLanguageOverride { + /// normal | <string> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontLanguageOverride, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontLanguageOverride::normal()); + } + + let string = input.expect_string()?; + + // The OpenType spec requires tags to be 1 to 4 ASCII characters: + // https://learn.microsoft.com/en-gb/typography/opentype/spec/otff#data-types + if string.is_empty() || string.len() > 4 || !string.is_ascii() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut bytes = [b' '; 4]; + for (byte, str_byte) in bytes.iter_mut().zip(string.as_bytes()) { + *byte = *str_byte; + } + + Ok(FontLanguageOverride(u32::from_be_bytes(bytes))) + } +} + +/// A value for any of the font-synthesis-{weight,style,small-caps} properties. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum FontSynthesis { + /// This attribute may be synthesized if not supported by a face. + Auto, + /// Do not attempt to synthesis this style attribute. + None, +} + +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Allows authors to choose a palette from those supported by a color font +/// (and potentially @font-palette-values overrides). +pub struct FontPalette(Atom); + +#[allow(missing_docs)] +impl FontPalette { + pub fn normal() -> Self { + Self(atom!("normal")) + } + pub fn light() -> Self { + Self(atom!("light")) + } + pub fn dark() -> Self { + Self(atom!("dark")) + } +} + +impl Parse for FontPalette { + /// normal | light | dark | dashed-ident + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontPalette, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + match_ignore_ascii_case! { &ident, + "normal" => Ok(Self::normal()), + "light" => Ok(Self::light()), + "dark" => Ok(Self::dark()), + _ => if ident.starts_with("--") { + Ok(Self(Atom::from(ident.as_ref()))) + } else { + Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + }, + } + } +} + +impl ToCss for FontPalette { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} + +/// This property provides low-level control over OpenType or TrueType font +/// variations. +pub type FontVariationSettings = FontSettings<VariationValue<Number>>; + +fn parse_one_feature_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<Integer, ParseError<'i>> { + if let Ok(integer) = input.try_parse(|i| Integer::parse_non_negative(context, i)) { + return Ok(integer); + } + + try_match_ident_ignore_ascii_case! { input, + "on" => Ok(Integer::new(1)), + "off" => Ok(Integer::new(0)), + } +} + +impl Parse for FeatureTagValue<Integer> { + /// https://drafts.csswg.org/css-fonts-4/#feature-tag-value + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tag = FontTag::parse(context, input)?; + let value = input + .try_parse(|i| parse_one_feature_value(context, i)) + .unwrap_or_else(|_| Integer::new(1)); + + Ok(Self { tag, value }) + } +} + +impl Parse for VariationValue<Number> { + /// This is the `<string> <number>` part of the font-variation-settings + /// syntax. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tag = FontTag::parse(context, input)?; + let value = Number::parse(context, input)?; + Ok(Self { tag, value }) + } +} + +/// A metrics override value for a @font-face descriptor +/// +/// https://drafts.csswg.org/css-fonts/#font-metrics-override-desc +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum MetricsOverride { + /// A non-negative `<percentage>` of the computed font size + Override(NonNegativePercentage), + /// Normal metrics from the font. + Normal, +} + +impl MetricsOverride { + #[inline] + /// Get default value with `normal` + pub fn normal() -> MetricsOverride { + MetricsOverride::Normal + } + + /// The ToComputedValue implementation, used for @font-face descriptors. + /// + /// Valid override percentages must be non-negative; we return -1.0 to indicate + /// the absence of an override (i.e. 'normal'). + #[inline] + pub fn compute(&self) -> ComputedPercentage { + match *self { + MetricsOverride::Normal => ComputedPercentage(-1.0), + MetricsOverride::Override(percent) => ComputedPercentage(percent.0.get()), + } + } +} + +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +/// How to do font-size scaling. +pub enum XTextScale { + /// Both min-font-size and text zoom are enabled. + All, + /// Text-only zoom is enabled, but min-font-size is not honored. + ZoomOnly, + /// Neither of them is enabled. + None, +} + +impl XTextScale { + /// Returns whether text zoom is enabled. + #[inline] + pub fn text_zoom_enabled(self) -> bool { + self != Self::None + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Internal property that reflects the lang attribute +pub struct XLang(#[css(skip)] pub Atom); + +impl XLang { + #[inline] + /// Get default value for `-x-lang` + pub fn get_initial_value() -> XLang { + XLang(atom!("")) + } +} + +impl Parse for XLang { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<XLang, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Specifies the minimum font size allowed due to changes in scriptlevel. +/// Ref: https://wiki.mozilla.org/MathML:mstyle +pub struct MozScriptMinSize(pub NoCalcLength); + +impl MozScriptMinSize { + #[inline] + /// Calculate initial value of -moz-script-min-size. + pub fn get_initial_value() -> Length { + Length::new(DEFAULT_SCRIPT_MIN_SIZE_PT as f32 * PX_PER_PT) + } +} + +impl Parse for MozScriptMinSize { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozScriptMinSize, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// A value for the `math-depth` property. +/// https://mathml-refresh.github.io/mathml-core/#the-math-script-level-property +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum MathDepth { + /// Increment math-depth if math-style is compact. + AutoAdd, + + /// Add the function's argument to math-depth. + #[css(function)] + Add(Integer), + + /// Set math-depth to the specified value. + Absolute(Integer), +} + +impl Parse for MathDepth { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MathDepth, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("auto-add")) + .is_ok() + { + return Ok(MathDepth::AutoAdd); + } + if let Ok(math_depth_value) = input.try_parse(|input| Integer::parse(context, input)) { + return Ok(MathDepth::Absolute(math_depth_value)); + } + input.expect_function_matching("add")?; + let math_depth_delta_value = + input.parse_nested_block(|input| Integer::parse(context, input))?; + Ok(MathDepth::Add(math_depth_delta_value)) + } +} + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive( + Clone, + Copy, + Debug, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Specifies the multiplier to be used to adjust font size +/// due to changes in scriptlevel. +/// +/// Ref: https://www.w3.org/TR/MathML3/chapter3.html#presm.mstyle.attrs +pub struct MozScriptSizeMultiplier(pub f32); + +impl MozScriptSizeMultiplier { + #[inline] + /// Get default value of `-moz-script-size-multiplier` + pub fn get_initial_value() -> MozScriptSizeMultiplier { + MozScriptSizeMultiplier(DEFAULT_SCRIPT_SIZE_MULTIPLIER as f32) + } +} + +impl Parse for MozScriptSizeMultiplier { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozScriptSizeMultiplier, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +impl From<f32> for MozScriptSizeMultiplier { + fn from(v: f32) -> Self { + MozScriptSizeMultiplier(v) + } +} + +impl From<MozScriptSizeMultiplier> for f32 { + fn from(v: MozScriptSizeMultiplier) -> f32 { + v.0 + } +} + +/// A specified value for the `line-height` property. +pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLengthPercentage>; + +impl ToComputedValue for LineHeight { + type ComputedValue = computed::LineHeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + GenericLineHeight::Normal => GenericLineHeight::Normal, + #[cfg(feature = "gecko")] + GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, + GenericLineHeight::Number(number) => { + GenericLineHeight::Number(number.to_computed_value(context)) + }, + GenericLineHeight::Length(ref non_negative_lp) => { + let result = match non_negative_lp.0 { + LengthPercentage::Length(NoCalcLength::Absolute(ref abs)) => { + context.maybe_zoom_text(abs.to_computed_value(context)) + }, + LengthPercentage::Length(ref length) => { + // line-height units specifically resolve against parent's + // font and line-height properties, while the rest of font + // relative units still resolve against the element's own + // properties. + length.to_computed_value_with_base_size( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ) + }, + LengthPercentage::Percentage(ref p) => FontRelativeLength::Em(p.0) + .to_computed_value( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ), + LengthPercentage::Calc(ref calc) => { + let computed_calc = calc.to_computed_value_zoomed( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ); + let base = context.style().get_font().clone_font_size().computed_size(); + computed_calc.resolve(base) + }, + }; + GenericLineHeight::Length(result.into()) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + GenericLineHeight::Normal => GenericLineHeight::Normal, + #[cfg(feature = "gecko")] + GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, + GenericLineHeight::Number(ref number) => { + GenericLineHeight::Number(NonNegativeNumber::from_computed_value(number)) + }, + GenericLineHeight::Length(ref length) => { + GenericLineHeight::Length(NoCalcLength::from_computed_value(&length.0).into()) + }, + } + } +} |