# 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/. import re from counted_unknown_properties import COUNTED_UNKNOWN_PROPERTIES PHYSICAL_SIDES = ["top", "right", "bottom", "left"] LOGICAL_SIDES = ["block-start", "block-end", "inline-start", "inline-end"] PHYSICAL_SIZES = ["width", "height"] LOGICAL_SIZES = ["block-size", "inline-size"] PHYSICAL_CORNERS = ["top-left", "top-right", "bottom-right", "bottom-left"] LOGICAL_CORNERS = ["start-start", "start-end", "end-start", "end-end"] PHYSICAL_AXES = ["x", "y"] LOGICAL_AXES = ["inline", "block"] # bool is True when logical ALL_SIDES = [(side, False) for side in PHYSICAL_SIDES] + [ (side, True) for side in LOGICAL_SIDES ] ALL_SIZES = [(size, False) for size in PHYSICAL_SIZES] + [ (size, True) for size in LOGICAL_SIZES ] ALL_CORNERS = [(corner, False) for corner in PHYSICAL_CORNERS] + [ (corner, True) for corner in LOGICAL_CORNERS ] ALL_AXES = [(axis, False) for axis in PHYSICAL_AXES] + [ (axis, True) for axis in LOGICAL_AXES ] SYSTEM_FONT_LONGHANDS = """font_family font_size font_style font_stretch font_weight""".split() # Bitfield values for all rule types which can have property declarations. STYLE_RULE = 1 << 0 PAGE_RULE = 1 << 1 KEYFRAME_RULE = 1 << 2 ALL_RULES = STYLE_RULE | PAGE_RULE | KEYFRAME_RULE DEFAULT_RULES = STYLE_RULE | KEYFRAME_RULE DEFAULT_RULES_AND_PAGE = DEFAULT_RULES | PAGE_RULE DEFAULT_RULES_EXCEPT_KEYFRAME = STYLE_RULE # Rule name to value dict RULE_VALUES = { "Style": STYLE_RULE, "Page": PAGE_RULE, "Keyframe": KEYFRAME_RULE, } def rule_values_from_arg(that): if isinstance(that, int): return that mask = 0 for rule in that.split(): mask |= RULE_VALUES[rule] return mask def maybe_moz_logical_alias(engine, side, prop): if engine == "gecko" and side[1]: axis, dir = side[0].split("-") if axis == "inline": return prop % dir return None def to_rust_ident(name): name = name.replace("-", "_") if name in ["static", "super", "box", "move"]: # Rust keywords name += "_" return name def to_snake_case(ident): return re.sub("([A-Z]+)", lambda m: "_" + m.group(1).lower(), ident).strip("_") def to_camel_case(ident): return re.sub( "(^|_|-)([a-z0-9])", lambda m: m.group(2).upper(), ident.strip("_").strip("-") ) def to_camel_case_lower(ident): camel = to_camel_case(ident) return camel[0].lower() + camel[1:] # https://drafts.csswg.org/cssom/#css-property-to-idl-attribute def to_idl_name(ident): return re.sub("-([a-z])", lambda m: m.group(1).upper(), ident) def parse_aliases(value): aliases = {} for pair in value.split(): [a, v] = pair.split("=") aliases[a] = v return aliases class Keyword(object): def __init__( self, name, values, gecko_constant_prefix=None, gecko_enum_prefix=None, custom_consts=None, extra_gecko_values=None, extra_servo_2013_values=None, extra_servo_2020_values=None, gecko_aliases=None, servo_2013_aliases=None, servo_2020_aliases=None, gecko_strip_moz_prefix=None, gecko_inexhaustive=None, ): self.name = name self.values = values.split() if gecko_constant_prefix and gecko_enum_prefix: raise TypeError( "Only one of gecko_constant_prefix and gecko_enum_prefix " "can be specified" ) self.gecko_constant_prefix = ( gecko_constant_prefix or "NS_STYLE_" + self.name.upper().replace("-", "_") ) self.gecko_enum_prefix = gecko_enum_prefix self.extra_gecko_values = (extra_gecko_values or "").split() self.extra_servo_2013_values = (extra_servo_2013_values or "").split() self.extra_servo_2020_values = (extra_servo_2020_values or "").split() self.gecko_aliases = parse_aliases(gecko_aliases or "") self.servo_2013_aliases = parse_aliases(servo_2013_aliases or "") self.servo_2020_aliases = parse_aliases(servo_2020_aliases or "") self.consts_map = {} if custom_consts is None else custom_consts self.gecko_strip_moz_prefix = ( True if gecko_strip_moz_prefix is None else gecko_strip_moz_prefix ) self.gecko_inexhaustive = gecko_inexhaustive or (gecko_enum_prefix is None) def values_for(self, engine): if engine == "gecko": return self.values + self.extra_gecko_values elif engine == "servo-2013": return self.values + self.extra_servo_2013_values elif engine == "servo-2020": return self.values + self.extra_servo_2020_values else: raise Exception("Bad engine: " + engine) def aliases_for(self, engine): if engine == "gecko": return self.gecko_aliases elif engine == "servo-2013": return self.servo_2013_aliases elif engine == "servo-2020": return self.servo_2020_aliases else: raise Exception("Bad engine: " + engine) def gecko_constant(self, value): moz_stripped = ( value.replace("-moz-", "") if self.gecko_strip_moz_prefix else value.replace("-moz-", "moz-") ) mapped = self.consts_map.get(value) if self.gecko_enum_prefix: parts = moz_stripped.replace("-", "_").split("_") parts = mapped if mapped else [p.title() for p in parts] return self.gecko_enum_prefix + "::" + "".join(parts) else: suffix = mapped if mapped else moz_stripped.replace("-", "_") return self.gecko_constant_prefix + "_" + suffix.upper() def needs_cast(self): return self.gecko_enum_prefix is None def maybe_cast(self, type_str): return "as " + type_str if self.needs_cast() else "" def casted_constant_name(self, value, cast_type): if cast_type is None: raise TypeError("We should specify the cast_type.") if self.gecko_enum_prefix is None: return cast_type.upper() + "_" + self.gecko_constant(value) else: return ( cast_type.upper() + "_" + self.gecko_constant(value).upper().replace("::", "_") ) def arg_to_bool(arg): if isinstance(arg, bool): return arg assert arg in ["True", "False"], "Unexpected value for boolean arguement: " + repr( arg ) return arg == "True" def parse_property_aliases(alias_list): result = [] if alias_list: for alias in alias_list.split(): (name, _, pref) = alias.partition(":") result.append((name, pref)) return result def to_phys(name, logical, physical): return name.replace(logical, physical).replace("inset-", "") class Property(object): def __init__( self, name, spec, servo_2013_pref, servo_2020_pref, gecko_pref, enabled_in, rule_types_allowed, aliases, extra_prefixes, flags, ): self.name = name if not spec: raise TypeError("Spec should be specified for " + name) self.spec = spec self.ident = to_rust_ident(name) self.camel_case = to_camel_case(self.ident) self.servo_2013_pref = servo_2013_pref self.servo_2020_pref = servo_2020_pref self.gecko_pref = gecko_pref self.rule_types_allowed = rule_values_from_arg(rule_types_allowed) # For enabled_in, the setup is as follows: # It needs to be one of the four values: ["", "ua", "chrome", "content"] # * "chrome" implies "ua", and implies that they're explicitly # enabled. # * "" implies the property will never be parsed. # * "content" implies the property is accessible unconditionally, # modulo a pref, set via servo_pref / gecko_pref. assert enabled_in in ("", "ua", "chrome", "content") self.enabled_in = enabled_in self.aliases = parse_property_aliases(aliases) self.extra_prefixes = parse_property_aliases(extra_prefixes) self.flags = flags.split() if flags else [] def rule_types_allowed_names(self): for name in RULE_VALUES: if self.rule_types_allowed & RULE_VALUES[name] != 0: yield name def experimental(self, engine): if engine == "gecko": return bool(self.gecko_pref) elif engine == "servo-2013": return bool(self.servo_2013_pref) elif engine == "servo-2020": return bool(self.servo_2020_pref) else: raise Exception("Bad engine: " + engine) def explicitly_enabled_in_ua_sheets(self): return self.enabled_in in ("ua", "chrome") def explicitly_enabled_in_chrome(self): return self.enabled_in == "chrome" def enabled_in_content(self): return self.enabled_in == "content" def nscsspropertyid(self): return "nsCSSPropertyID::eCSSProperty_" + self.ident class Longhand(Property): def __init__( self, style_struct, name, spec=None, animation_value_type=None, keyword=None, predefined_type=None, servo_2013_pref=None, servo_2020_pref=None, gecko_pref=None, enabled_in="content", need_index=False, gecko_ffi_name=None, has_effect_on_gecko_scrollbars=None, rule_types_allowed=DEFAULT_RULES, cast_type="u8", logical=False, logical_group=None, aliases=None, extra_prefixes=None, boxed=False, flags=None, allow_quirks="No", ignored_when_colors_disabled=False, simple_vector_bindings=False, vector=False, servo_restyle_damage="repaint", ): Property.__init__( self, name=name, spec=spec, servo_2013_pref=servo_2013_pref, servo_2020_pref=servo_2020_pref, gecko_pref=gecko_pref, enabled_in=enabled_in, rule_types_allowed=rule_types_allowed, aliases=aliases, extra_prefixes=extra_prefixes, flags=flags, ) self.keyword = keyword self.predefined_type = predefined_type self.style_struct = style_struct self.has_effect_on_gecko_scrollbars = has_effect_on_gecko_scrollbars assert ( has_effect_on_gecko_scrollbars in [None, False, True] and not style_struct.inherited or (gecko_pref is None and enabled_in != "") == (has_effect_on_gecko_scrollbars is None) ), ( "Property " + name + ": has_effect_on_gecko_scrollbars must be " + "specified, and must have a value of True or False, iff a " + "property is inherited and is behind a Gecko pref or internal" ) self.need_index = need_index self.gecko_ffi_name = gecko_ffi_name or "m" + self.camel_case self.cast_type = cast_type self.logical = arg_to_bool(logical) self.logical_group = logical_group if self.logical: assert logical_group, "Property " + name + " must have a logical group" self.boxed = arg_to_bool(boxed) self.allow_quirks = allow_quirks self.ignored_when_colors_disabled = ignored_when_colors_disabled self.is_vector = vector self.simple_vector_bindings = simple_vector_bindings # This is done like this since just a plain bool argument seemed like # really random. if animation_value_type is None: raise TypeError( "animation_value_type should be specified for (" + name + ")" ) self.animation_value_type = animation_value_type self.animatable = animation_value_type != "none" self.transitionable = ( animation_value_type != "none" and animation_value_type != "discrete" ) self.is_animatable_with_computed_value = ( animation_value_type == "ComputedValue" or animation_value_type == "discrete" ) # See compute_damage for the various values this can take self.servo_restyle_damage = servo_restyle_damage @staticmethod def type(): return "longhand" # For a given logical property return all the physical property names # corresponding to it. def all_physical_mapped_properties(self, data): if not self.logical: return [] candidates = [ s for s in LOGICAL_SIDES + LOGICAL_SIZES + LOGICAL_CORNERS if s in self.name ] + [s for s in LOGICAL_AXES if self.name.endswith(s)] assert len(candidates) == 1 logical_side = candidates[0] physical = ( PHYSICAL_SIDES if logical_side in LOGICAL_SIDES else PHYSICAL_SIZES if logical_side in LOGICAL_SIZES else PHYSICAL_AXES if logical_side in LOGICAL_AXES else LOGICAL_CORNERS ) return [ data.longhands_by_name[to_phys(self.name, logical_side, physical_side)] for physical_side in physical ] def may_be_disabled_in(self, shorthand, engine): if engine == "gecko": return self.gecko_pref and self.gecko_pref != shorthand.gecko_pref elif engine == "servo-2013": return ( self.servo_2013_pref and self.servo_2013_pref != shorthand.servo_2013_pref ) elif engine == "servo-2020": return ( self.servo_2020_pref and self.servo_2020_pref != shorthand.servo_2020_pref ) else: raise Exception("Bad engine: " + engine) def base_type(self): if self.predefined_type and not self.is_vector: return "crate::values::specified::{}".format(self.predefined_type) return "longhands::{}::SpecifiedValue".format(self.ident) def specified_type(self): if self.predefined_type and not self.is_vector: ty = "crate::values::specified::{}".format(self.predefined_type) else: ty = "longhands::{}::SpecifiedValue".format(self.ident) if self.boxed: ty = "Box<{}>".format(ty) return ty def specified_is_copy(self): if self.is_vector or self.boxed: return False if self.predefined_type: return self.predefined_type in { "AlignContent", "AlignItems", "AlignSelf", "Appearance", "AspectRatio", "BreakBetween", "BreakWithin", "BackgroundRepeat", "BorderImageRepeat", "BorderStyle", "table::CaptionSide", "Clear", "ColumnCount", "Contain", "ContentVisibility", "ContainerType", "Display", "FillRule", "Float", "FontSizeAdjust", "FontStretch", "FontStyle", "FontSynthesis", "FontVariantEastAsian", "FontVariantLigatures", "FontVariantNumeric", "FontWeight", "GreaterThanOrEqualToOneNumber", "GridAutoFlow", "ImageRendering", "InitialLetter", "Integer", "JustifyContent", "JustifyItems", "JustifySelf", "LineBreak", "LineClamp", "MasonryAutoFlow", "BoolInteger", "text::MozControlCharacterVisibility", "MathDepth", "MozScriptMinSize", "MozScriptSizeMultiplier", "TextDecorationSkipInk", "NonNegativeNumber", "OffsetRotate", "Opacity", "OutlineStyle", "Overflow", "OverflowAnchor", "OverflowClipBox", "OverflowWrap", "OverscrollBehavior", "Percentage", "PrintColorAdjust", "Resize", "RubyPosition", "SVGOpacity", "SVGPaintOrder", "ScrollbarGutter", "ScrollSnapAlign", "ScrollSnapAxis", "ScrollSnapStop", "ScrollSnapStrictness", "ScrollSnapType", "TextAlign", "TextAlignLast", "TextDecorationLine", "TextEmphasisPosition", "TextJustify", "TextTransform", "TextUnderlinePosition", "TouchAction", "TransformStyle", "UserSelect", "WordBreak", "XSpan", "XTextZoom", "ZIndex", } if self.name == "overflow-y": return True return bool(self.keyword) def animated_type(self): assert self.animatable computed = "<{} as ToComputedValue>::ComputedValue".format(self.base_type()) if self.is_animatable_with_computed_value: return computed return "<{} as ToAnimatedValue>::AnimatedValue".format(computed) class Shorthand(Property): def __init__( self, name, sub_properties, spec=None, servo_2013_pref=None, servo_2020_pref=None, gecko_pref=None, enabled_in="content", rule_types_allowed=DEFAULT_RULES, aliases=None, extra_prefixes=None, flags=None, ): Property.__init__( self, name=name, spec=spec, servo_2013_pref=servo_2013_pref, servo_2020_pref=servo_2020_pref, gecko_pref=gecko_pref, enabled_in=enabled_in, rule_types_allowed=rule_types_allowed, aliases=aliases, extra_prefixes=extra_prefixes, flags=flags, ) self.sub_properties = sub_properties def get_animatable(self): for sub in self.sub_properties: if sub.animatable: return True return False def get_transitionable(self): transitionable = False for sub in self.sub_properties: if sub.transitionable: transitionable = True break return transitionable animatable = property(get_animatable) transitionable = property(get_transitionable) @staticmethod def type(): return "shorthand" class Alias(object): def __init__(self, name, original, gecko_pref): self.name = name self.ident = to_rust_ident(name) self.camel_case = to_camel_case(self.ident) self.original = original self.enabled_in = original.enabled_in self.animatable = original.animatable self.servo_2013_pref = original.servo_2013_pref self.servo_2020_pref = original.servo_2020_pref self.gecko_pref = gecko_pref self.transitionable = original.transitionable self.rule_types_allowed = original.rule_types_allowed self.flags = original.flags @staticmethod def type(): return "alias" def rule_types_allowed_names(self): for name in RULE_VALUES: if self.rule_types_allowed & RULE_VALUES[name] != 0: yield name def experimental(self, engine): if engine == "gecko": return bool(self.gecko_pref) elif engine == "servo-2013": return bool(self.servo_2013_pref) elif engine == "servo-2020": return bool(self.servo_2020_pref) else: raise Exception("Bad engine: " + engine) def explicitly_enabled_in_ua_sheets(self): return self.enabled_in in ["ua", "chrome"] def explicitly_enabled_in_chrome(self): return self.enabled_in == "chrome" def enabled_in_content(self): return self.enabled_in == "content" def nscsspropertyid(self): return "nsCSSPropertyID::eCSSPropertyAlias_%s" % self.ident class Method(object): def __init__(self, name, return_type=None, arg_types=None, is_mut=False): self.name = name self.return_type = return_type self.arg_types = arg_types or [] self.is_mut = is_mut def arg_list(self): args = ["_: " + x for x in self.arg_types] args = ["&mut self" if self.is_mut else "&self"] + args return ", ".join(args) def signature(self): sig = "fn %s(%s)" % (self.name, self.arg_list()) if self.return_type: sig = sig + " -> " + self.return_type return sig def declare(self): return self.signature() + ";" def stub(self): return self.signature() + "{ unimplemented!() }" class StyleStruct(object): def __init__(self, name, inherited, gecko_name=None, additional_methods=None): self.gecko_struct_name = "Gecko" + name self.name = name self.name_lower = to_snake_case(name) self.ident = to_rust_ident(self.name_lower) self.longhands = [] self.inherited = inherited self.gecko_name = gecko_name or name self.gecko_ffi_name = "nsStyle" + self.gecko_name self.additional_methods = additional_methods or [] class PropertiesData(object): def __init__(self, engine): self.engine = engine self.style_structs = [] self.current_style_struct = None self.longhands = [] self.longhands_by_name = {} self.longhands_by_logical_group = {} self.longhand_aliases = [] self.shorthands = [] self.shorthands_by_name = {} self.shorthand_aliases = [] self.counted_unknown_properties = [ CountedUnknownProperty(p) for p in COUNTED_UNKNOWN_PROPERTIES ] def new_style_struct(self, *args, **kwargs): style_struct = StyleStruct(*args, **kwargs) self.style_structs.append(style_struct) self.current_style_struct = style_struct def active_style_structs(self): return [s for s in self.style_structs if s.additional_methods or s.longhands] def add_prefixed_aliases(self, property): # FIXME Servo's DOM architecture doesn't support vendor-prefixed properties. # See servo/servo#14941. if self.engine == "gecko": for (prefix, pref) in property.extra_prefixes: property.aliases.append(("-%s-%s" % (prefix, property.name), pref)) def declare_longhand(self, name, engines=None, **kwargs): engines = engines.split() if self.engine not in engines: return longhand = Longhand(self.current_style_struct, name, **kwargs) self.add_prefixed_aliases(longhand) longhand.aliases = [Alias(xp[0], longhand, xp[1]) for xp in longhand.aliases] self.longhand_aliases += longhand.aliases self.current_style_struct.longhands.append(longhand) self.longhands.append(longhand) self.longhands_by_name[name] = longhand if longhand.logical_group: self.longhands_by_logical_group.setdefault( longhand.logical_group, [] ).append(longhand) return longhand def declare_shorthand(self, name, sub_properties, engines, *args, **kwargs): engines = engines.split() if self.engine not in engines: return sub_properties = [self.longhands_by_name[s] for s in sub_properties] shorthand = Shorthand(name, sub_properties, *args, **kwargs) self.add_prefixed_aliases(shorthand) shorthand.aliases = [Alias(xp[0], shorthand, xp[1]) for xp in shorthand.aliases] self.shorthand_aliases += shorthand.aliases self.shorthands.append(shorthand) self.shorthands_by_name[name] = shorthand return shorthand def shorthands_except_all(self): return [s for s in self.shorthands if s.name != "all"] def all_aliases(self): return self.longhand_aliases + self.shorthand_aliases def _add_logical_props(data, props): groups = set() for prop in props: if prop not in data.longhands_by_name: assert data.engine in ["servo-2013", "servo-2020"] continue prop = data.longhands_by_name[prop] if prop.logical_group: groups.add(prop.logical_group) for group in groups: for prop in data.longhands_by_logical_group[group]: props.add(prop.name) # These are probably Gecko bugs and should be supported per spec. def _remove_common_first_line_and_first_letter_properties(props, engine): if engine == "gecko": props.remove("tab-size") props.remove("hyphens") props.remove("line-break") props.remove("text-align-last") props.remove("text-emphasis-position") props.remove("text-emphasis-style") props.remove("text-emphasis-color") props.remove("overflow-wrap") props.remove("text-align") props.remove("text-justify") props.remove("white-space") props.remove("word-break") props.remove("text-indent") class PropertyRestrictions: @staticmethod def logical_group(data, group): return [p.name for p in data.longhands_by_logical_group[group]] @staticmethod def shorthand(data, shorthand): if shorthand not in data.shorthands_by_name: return [] return [p.name for p in data.shorthands_by_name[shorthand].sub_properties] @staticmethod def spec(data, spec_path): return [p.name for p in data.longhands if spec_path in p.spec] # https://drafts.csswg.org/css-pseudo/#first-letter-styling @staticmethod def first_letter(data): props = set( [ "color", "opacity", "float", "initial-letter", # Kinda like css-fonts? "-moz-osx-font-smoothing", # Kinda like css-text? "-webkit-text-stroke-width", "-webkit-text-fill-color", "-webkit-text-stroke-color", "vertical-align", "line-height", # Kinda like css-backgrounds? "background-blend-mode", ] + PropertyRestrictions.shorthand(data, "padding") + PropertyRestrictions.shorthand(data, "margin") + PropertyRestrictions.spec(data, "css-fonts") + PropertyRestrictions.spec(data, "css-backgrounds") + PropertyRestrictions.spec(data, "css-text") + PropertyRestrictions.spec(data, "css-shapes") + PropertyRestrictions.spec(data, "css-text-decor") ) _add_logical_props(data, props) _remove_common_first_line_and_first_letter_properties(props, data.engine) return props # https://drafts.csswg.org/css-pseudo/#first-line-styling @staticmethod def first_line(data): props = set( [ # Per spec. "color", "opacity", # Kinda like css-fonts? "-moz-osx-font-smoothing", # Kinda like css-text? "-webkit-text-stroke-width", "-webkit-text-fill-color", "-webkit-text-stroke-color", "vertical-align", "line-height", # Kinda like css-backgrounds? "background-blend-mode", ] + PropertyRestrictions.spec(data, "css-fonts") + PropertyRestrictions.spec(data, "css-backgrounds") + PropertyRestrictions.spec(data, "css-text") + PropertyRestrictions.spec(data, "css-text-decor") ) # These are probably Gecko bugs and should be supported per spec. for prop in PropertyRestrictions.shorthand(data, "border"): props.remove(prop) for prop in PropertyRestrictions.shorthand(data, "border-radius"): props.remove(prop) props.remove("box-shadow") _remove_common_first_line_and_first_letter_properties(props, data.engine) return props # https://drafts.csswg.org/css-pseudo/#placeholder # # The spec says that placeholder and first-line have the same restrictions, # but that's not true in Gecko and we also allow a handful other properties # for ::placeholder. @staticmethod def placeholder(data): props = PropertyRestrictions.first_line(data) props.add("opacity") props.add("white-space") props.add("text-overflow") props.add("text-align") props.add("text-justify") return props # https://drafts.csswg.org/css-pseudo/#marker-pseudo @staticmethod def marker(data): return set( [ "white-space", "color", "text-combine-upright", "text-transform", "unicode-bidi", "direction", "content", "line-height", "-moz-osx-font-smoothing", ] + PropertyRestrictions.spec(data, "css-fonts") + PropertyRestrictions.spec(data, "css-animations") + PropertyRestrictions.spec(data, "css-transitions") ) # https://www.w3.org/TR/webvtt1/#the-cue-pseudo-element @staticmethod def cue(data): return set( [ "color", "opacity", "visibility", "text-shadow", "white-space", "text-combine-upright", "ruby-position", # XXX Should these really apply to cue? "font-synthesis", "-moz-osx-font-smoothing", # FIXME(emilio): background-blend-mode should be part of the # background shorthand, and get reset, per # https://drafts.fxtf.org/compositing/#background-blend-mode "background-blend-mode", ] + PropertyRestrictions.shorthand(data, "text-decoration") + PropertyRestrictions.shorthand(data, "background") + PropertyRestrictions.shorthand(data, "outline") + PropertyRestrictions.shorthand(data, "font") ) class CountedUnknownProperty: def __init__(self, name): self.name = name self.ident = to_rust_ident(name) self.camel_case = to_camel_case(self.ident)