summaryrefslogtreecommitdiffstats
path: root/servo/components
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components')
-rw-r--r--servo/components/derive_common/Cargo.toml16
-rw-r--r--servo/components/derive_common/cg.rs396
-rw-r--r--servo/components/derive_common/lib.rs13
-rw-r--r--servo/components/malloc_size_of/Cargo.toml53
-rw-r--r--servo/components/malloc_size_of/LICENSE-APACHE201
-rw-r--r--servo/components/malloc_size_of/LICENSE-MIT23
-rw-r--r--servo/components/malloc_size_of/lib.rs1003
-rw-r--r--servo/components/selectors/CHANGES.md1
-rw-r--r--servo/components/selectors/Cargo.toml35
-rw-r--r--servo/components/selectors/README.md25
-rw-r--r--servo/components/selectors/attr.rs183
-rw-r--r--servo/components/selectors/bloom.rs422
-rw-r--r--servo/components/selectors/build.rs77
-rw-r--r--servo/components/selectors/builder.rs391
-rw-r--r--servo/components/selectors/context.rs418
-rw-r--r--servo/components/selectors/lib.rs41
-rw-r--r--servo/components/selectors/matching.rs1370
-rw-r--r--servo/components/selectors/nth_index_cache.rs102
-rw-r--r--servo/components/selectors/parser.rs4483
-rw-r--r--servo/components/selectors/relative_selector/cache.rs81
-rw-r--r--servo/components/selectors/relative_selector/filter.rs159
-rw-r--r--servo/components/selectors/relative_selector/mod.rs6
-rw-r--r--servo/components/selectors/sink.rs31
-rw-r--r--servo/components/selectors/tree.rs168
-rw-r--r--servo/components/selectors/visitor.rs136
-rw-r--r--servo/components/servo_arc/Cargo.toml19
-rw-r--r--servo/components/servo_arc/lib.rs1195
-rw-r--r--servo/components/style/Cargo.toml91
-rw-r--r--servo/components/style/README.md6
-rw-r--r--servo/components/style/animation.rs1415
-rw-r--r--servo/components/style/applicable_declarations.rs215
-rw-r--r--servo/components/style/attr.rs599
-rw-r--r--servo/components/style/author_styles.rs70
-rw-r--r--servo/components/style/bezier.rs176
-rw-r--r--servo/components/style/bloom.rs401
-rw-r--r--servo/components/style/build.rs91
-rw-r--r--servo/components/style/build_gecko.rs400
-rw-r--r--servo/components/style/color/convert.rs902
-rw-r--r--servo/components/style/color/mix.rs558
-rw-r--r--servo/components/style/color/mod.rs613
-rw-r--r--servo/components/style/color/parsing.rs1246
-rw-r--r--servo/components/style/context.rs698
-rw-r--r--servo/components/style/counter_style/mod.rs695
-rw-r--r--servo/components/style/counter_style/predefined.rs61
-rwxr-xr-xservo/components/style/counter_style/update_predefined.py35
-rw-r--r--servo/components/style/custom_properties.rs1959
-rw-r--r--servo/components/style/custom_properties_map.rs237
-rw-r--r--servo/components/style/data.rs545
-rw-r--r--servo/components/style/dom.rs951
-rw-r--r--servo/components/style/dom_apis.rs814
-rw-r--r--servo/components/style/driver.rs164
-rw-r--r--servo/components/style/encoding_support.rs105
-rw-r--r--servo/components/style/error_reporting.rs454
-rw-r--r--servo/components/style/font_face.rs807
-rw-r--r--servo/components/style/font_metrics.rs58
-rw-r--r--servo/components/style/gecko/arc_types.rs171
-rw-r--r--servo/components/style/gecko/conversions.rs59
-rw-r--r--servo/components/style/gecko/data.rs198
-rw-r--r--servo/components/style/gecko/media_features.rs1003
-rw-r--r--servo/components/style/gecko/media_queries.rs593
-rw-r--r--servo/components/style/gecko/mod.rs23
-rw-r--r--servo/components/style/gecko/non_ts_pseudo_class_list.rs106
-rw-r--r--servo/components/style/gecko/pseudo_element.rs233
-rw-r--r--servo/components/style/gecko/pseudo_element_definition.mako.rs278
-rwxr-xr-xservo/components/style/gecko/regen_atoms.py218
-rw-r--r--servo/components/style/gecko/restyle_damage.rs121
-rw-r--r--servo/components/style/gecko/selector_parser.rs519
-rw-r--r--servo/components/style/gecko/snapshot.rs174
-rw-r--r--servo/components/style/gecko/snapshot_helpers.rs316
-rw-r--r--servo/components/style/gecko/traversal.rs53
-rw-r--r--servo/components/style/gecko/url.rs384
-rw-r--r--servo/components/style/gecko/values.rs77
-rw-r--r--servo/components/style/gecko/wrapper.rs2211
-rw-r--r--servo/components/style/gecko_bindings/mod.rs28
-rw-r--r--servo/components/style/gecko_bindings/sugar/mod.rs13
-rw-r--r--servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs15
-rw-r--r--servo/components/style/gecko_bindings/sugar/ns_compatibility.rs19
-rw-r--r--servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs111
-rw-r--r--servo/components/style/gecko_bindings/sugar/ns_t_array.rs144
-rw-r--r--servo/components/style/gecko_bindings/sugar/origin_flags.rs31
-rw-r--r--servo/components/style/gecko_bindings/sugar/ownership.rs61
-rw-r--r--servo/components/style/gecko_bindings/sugar/refptr.rs289
-rw-r--r--servo/components/style/gecko_string_cache/mod.rs497
-rw-r--r--servo/components/style/gecko_string_cache/namespace.rs105
-rw-r--r--servo/components/style/global_style_data.rs212
-rw-r--r--servo/components/style/invalidation/element/document_state.rs154
-rw-r--r--servo/components/style/invalidation/element/element_wrapper.rs388
-rw-r--r--servo/components/style/invalidation/element/invalidation_map.rs1425
-rw-r--r--servo/components/style/invalidation/element/invalidator.rs1130
-rw-r--r--servo/components/style/invalidation/element/mod.rs13
-rw-r--r--servo/components/style/invalidation/element/relative_selector.rs1164
-rw-r--r--servo/components/style/invalidation/element/restyle_hints.rs191
-rw-r--r--servo/components/style/invalidation/element/state_and_attributes.rs601
-rw-r--r--servo/components/style/invalidation/media_queries.rs130
-rw-r--r--servo/components/style/invalidation/mod.rs10
-rw-r--r--servo/components/style/invalidation/stylesheets.rs651
-rw-r--r--servo/components/style/invalidation/viewport_units.rs71
-rw-r--r--servo/components/style/lib.rs332
-rw-r--r--servo/components/style/logical_geometry.rs1629
-rw-r--r--servo/components/style/macros.rs98
-rw-r--r--servo/components/style/matching.rs1128
-rw-r--r--servo/components/style/media_queries/media_list.rs150
-rw-r--r--servo/components/style/media_queries/media_query.rs193
-rw-r--r--servo/components/style/media_queries/mod.rs18
-rw-r--r--servo/components/style/parallel.rs194
-rw-r--r--servo/components/style/parser.rs178
-rw-r--r--servo/components/style/piecewise_linear.rs281
-rw-r--r--servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whlbin0 -> 75521 bytes
-rw-r--r--servo/components/style/properties/build.py176
-rw-r--r--servo/components/style/properties/cascade.rs1381
-rw-r--r--servo/components/style/properties/computed_value_flags.rs194
-rw-r--r--servo/components/style/properties/counted_unknown_properties.py110
-rw-r--r--servo/components/style/properties/data.py1083
-rw-r--r--servo/components/style/properties/declaration_block.rs1642
-rw-r--r--servo/components/style/properties/gecko.mako.rs1806
-rw-r--r--servo/components/style/properties/helpers.mako.rs909
-rw-r--r--servo/components/style/properties/helpers/animated_properties.mako.rs785
-rw-r--r--servo/components/style/properties/longhands/background.mako.rs126
-rw-r--r--servo/components/style/properties/longhands/border.mako.rs170
-rw-r--r--servo/components/style/properties/longhands/box.mako.rs644
-rw-r--r--servo/components/style/properties/longhands/column.mako.rs90
-rw-r--r--servo/components/style/properties/longhands/counters.mako.rs52
-rw-r--r--servo/components/style/properties/longhands/effects.mako.rs92
-rw-r--r--servo/components/style/properties/longhands/font.mako.rs505
-rw-r--r--servo/components/style/properties/longhands/inherited_box.mako.rs105
-rw-r--r--servo/components/style/properties/longhands/inherited_svg.mako.rs239
-rw-r--r--servo/components/style/properties/longhands/inherited_table.mako.rs53
-rw-r--r--servo/components/style/properties/longhands/inherited_text.mako.rs414
-rw-r--r--servo/components/style/properties/longhands/inherited_ui.mako.rs135
-rw-r--r--servo/components/style/properties/longhands/list.mako.rs80
-rw-r--r--servo/components/style/properties/longhands/margin.mako.rs55
-rw-r--r--servo/components/style/properties/longhands/outline.mako.rs57
-rw-r--r--servo/components/style/properties/longhands/padding.mako.rs43
-rw-r--r--servo/components/style/properties/longhands/page.mako.rs44
-rw-r--r--servo/components/style/properties/longhands/position.mako.rs485
-rw-r--r--servo/components/style/properties/longhands/svg.mako.rs282
-rw-r--r--servo/components/style/properties/longhands/table.mako.rs30
-rw-r--r--servo/components/style/properties/longhands/text.mako.rs88
-rw-r--r--servo/components/style/properties/longhands/ui.mako.rs422
-rw-r--r--servo/components/style/properties/longhands/xul.mako.rs85
-rw-r--r--servo/components/style/properties/mod.rs1531
-rw-r--r--servo/components/style/properties/properties.html.mako31
-rw-r--r--servo/components/style/properties/properties.mako.rs2958
-rw-r--r--servo/components/style/properties/shorthands/background.mako.rs289
-rw-r--r--servo/components/style/properties/shorthands/border.mako.rs491
-rw-r--r--servo/components/style/properties/shorthands/box.mako.rs253
-rw-r--r--servo/components/style/properties/shorthands/column.mako.rs115
-rw-r--r--servo/components/style/properties/shorthands/font.mako.rs542
-rw-r--r--servo/components/style/properties/shorthands/inherited_svg.mako.rs38
-rw-r--r--servo/components/style/properties/shorthands/inherited_text.mako.rs254
-rw-r--r--servo/components/style/properties/shorthands/list.mako.rs137
-rw-r--r--servo/components/style/properties/shorthands/margin.mako.rs60
-rw-r--r--servo/components/style/properties/shorthands/outline.mako.rs80
-rw-r--r--servo/components/style/properties/shorthands/padding.mako.rs58
-rw-r--r--servo/components/style/properties/shorthands/position.mako.rs891
-rw-r--r--servo/components/style/properties/shorthands/svg.mako.rs287
-rw-r--r--servo/components/style/properties/shorthands/text.mako.rs120
-rw-r--r--servo/components/style/properties/shorthands/ui.mako.rs444
-rw-r--r--servo/components/style/properties_and_values/mod.rs12
-rw-r--r--servo/components/style/properties_and_values/registry.rs104
-rw-r--r--servo/components/style/properties_and_values/rule.rs348
-rw-r--r--servo/components/style/properties_and_values/syntax/ascii.rs60
-rw-r--r--servo/components/style/properties_and_values/syntax/data_type.rs134
-rw-r--r--servo/components/style/properties_and_values/syntax/mod.rs392
-rw-r--r--servo/components/style/properties_and_values/value.rs626
-rw-r--r--servo/components/style/queries/condition.rs366
-rw-r--r--servo/components/style/queries/feature.rs198
-rw-r--r--servo/components/style/queries/feature_expression.rs764
-rw-r--r--servo/components/style/queries/mod.rs19
-rw-r--r--servo/components/style/queries/values.rs36
-rw-r--r--servo/components/style/rule_cache.rs219
-rw-r--r--servo/components/style/rule_collector.rs505
-rw-r--r--servo/components/style/rule_tree/core.rs772
-rw-r--r--servo/components/style/rule_tree/level.rs249
-rw-r--r--servo/components/style/rule_tree/map.rs201
-rw-r--r--servo/components/style/rule_tree/mod.rs403
-rw-r--r--servo/components/style/rule_tree/source.rs75
-rw-r--r--servo/components/style/rule_tree/unsafe_box.rs74
-rw-r--r--servo/components/style/scoped_tls.rs81
-rw-r--r--servo/components/style/selector_map.rs870
-rw-r--r--servo/components/style/selector_parser.rs240
-rw-r--r--servo/components/style/servo/media_queries.rs226
-rw-r--r--servo/components/style/servo/mod.rs12
-rw-r--r--servo/components/style/servo/restyle_damage.rs268
-rw-r--r--servo/components/style/servo/selector_parser.rs806
-rw-r--r--servo/components/style/servo/url.rs238
-rw-r--r--servo/components/style/shared_lock.rs374
-rw-r--r--servo/components/style/sharing/checks.rs166
-rw-r--r--servo/components/style/sharing/mod.rs923
-rw-r--r--servo/components/style/str.rs181
-rw-r--r--servo/components/style/style_adjuster.rs1009
-rw-r--r--servo/components/style/style_resolver.rs585
-rw-r--r--servo/components/style/stylesheet_set.rs705
-rw-r--r--servo/components/style/stylesheets/container_rule.rs642
-rw-r--r--servo/components/style/stylesheets/counter_style_rule.rs7
-rw-r--r--servo/components/style/stylesheets/document_rule.rs299
-rw-r--r--servo/components/style/stylesheets/font_face_rule.rs7
-rw-r--r--servo/components/style/stylesheets/font_feature_values_rule.rs490
-rw-r--r--servo/components/style/stylesheets/font_palette_values_rule.rs264
-rw-r--r--servo/components/style/stylesheets/import_rule.rs301
-rw-r--r--servo/components/style/stylesheets/keyframes_rule.rs690
-rw-r--r--servo/components/style/stylesheets/layer_rule.rs228
-rw-r--r--servo/components/style/stylesheets/loader.rs31
-rw-r--r--servo/components/style/stylesheets/margin_rule.rs167
-rw-r--r--servo/components/style/stylesheets/media_rule.rs71
-rw-r--r--servo/components/style/stylesheets/mod.rs597
-rw-r--r--servo/components/style/stylesheets/namespace_rule.rs43
-rw-r--r--servo/components/style/stylesheets/origin.rs248
-rw-r--r--servo/components/style/stylesheets/page_rule.rs366
-rw-r--r--servo/components/style/stylesheets/property_rule.rs5
-rw-r--r--servo/components/style/stylesheets/rule_list.rs189
-rw-r--r--servo/components/style/stylesheets/rule_parser.rs982
-rw-r--r--servo/components/style/stylesheets/rules_iterator.rs331
-rw-r--r--servo/components/style/stylesheets/style_rule.rs104
-rw-r--r--servo/components/style/stylesheets/stylesheet.rs566
-rw-r--r--servo/components/style/stylesheets/supports_rule.rs397
-rw-r--r--servo/components/style/stylist.rs3503
-rw-r--r--servo/components/style/thread_state.rs98
-rw-r--r--servo/components/style/traversal.rs842
-rw-r--r--servo/components/style/traversal_flags.rs68
-rw-r--r--servo/components/style/use_counters/mod.rs96
-rw-r--r--servo/components/style/values/animated/color.rs88
-rw-r--r--servo/components/style/values/animated/effects.rs27
-rw-r--r--servo/components/style/values/animated/font.rs37
-rw-r--r--servo/components/style/values/animated/grid.rs165
-rw-r--r--servo/components/style/values/animated/lists.rs141
-rw-r--r--servo/components/style/values/animated/mod.rs487
-rw-r--r--servo/components/style/values/animated/svg.rs46
-rw-r--r--servo/components/style/values/animated/transform.rs1667
-rw-r--r--servo/components/style/values/computed/align.rs91
-rw-r--r--servo/components/style/values/computed/angle.rs101
-rw-r--r--servo/components/style/values/computed/animation.rs70
-rw-r--r--servo/components/style/values/computed/background.rs13
-rw-r--r--servo/components/style/values/computed/basic_shape.rs37
-rw-r--r--servo/components/style/values/computed/border.rs84
-rw-r--r--servo/components/style/values/computed/box.rs388
-rw-r--r--servo/components/style/values/computed/color.rs95
-rw-r--r--servo/components/style/values/computed/column.rs11
-rw-r--r--servo/components/style/values/computed/counters.rs26
-rw-r--r--servo/components/style/values/computed/easing.rs109
-rw-r--r--servo/components/style/values/computed/effects.rs44
-rw-r--r--servo/components/style/values/computed/flex.rs19
-rw-r--r--servo/components/style/values/computed/font.rs1369
-rw-r--r--servo/components/style/values/computed/image.rs205
-rw-r--r--servo/components/style/values/computed/length.rs531
-rw-r--r--servo/components/style/values/computed/length_percentage.rs1055
-rw-r--r--servo/components/style/values/computed/list.rs17
-rw-r--r--servo/components/style/values/computed/mod.rs1035
-rw-r--r--servo/components/style/values/computed/motion.rs70
-rw-r--r--servo/components/style/values/computed/outline.rs7
-rw-r--r--servo/components/style/values/computed/page.rs75
-rw-r--r--servo/components/style/values/computed/percentage.rs136
-rw-r--r--servo/components/style/values/computed/position.rs74
-rw-r--r--servo/components/style/values/computed/ratio.rs115
-rw-r--r--servo/components/style/values/computed/rect.rs11
-rw-r--r--servo/components/style/values/computed/resolution.rs56
-rw-r--r--servo/components/style/values/computed/svg.rs66
-rw-r--r--servo/components/style/values/computed/table.rs7
-rw-r--r--servo/components/style/values/computed/text.rs228
-rw-r--r--servo/components/style/values/computed/time.rs45
-rw-r--r--servo/components/style/values/computed/transform.rs559
-rw-r--r--servo/components/style/values/computed/ui.rs21
-rw-r--r--servo/components/style/values/computed/url.rs15
-rw-r--r--servo/components/style/values/distance.rs138
-rw-r--r--servo/components/style/values/generics/animation.rs140
-rw-r--r--servo/components/style/values/generics/background.rs54
-rw-r--r--servo/components/style/values/generics/basic_shape.rs567
-rw-r--r--servo/components/style/values/generics/border.rs261
-rw-r--r--servo/components/style/values/generics/box.rs211
-rw-r--r--servo/components/style/values/generics/calc.rs1820
-rw-r--r--servo/components/style/values/generics/color.rs209
-rw-r--r--servo/components/style/values/generics/column.rs45
-rw-r--r--servo/components/style/values/generics/counters.rs295
-rw-r--r--servo/components/style/values/generics/easing.rs143
-rw-r--r--servo/components/style/values/generics/effects.rs121
-rw-r--r--servo/components/style/values/generics/flex.rs33
-rw-r--r--servo/components/style/values/generics/font.rs316
-rw-r--r--servo/components/style/values/generics/grid.rs867
-rw-r--r--servo/components/style/values/generics/image.rs631
-rw-r--r--servo/components/style/values/generics/length.rs304
-rw-r--r--servo/components/style/values/generics/mod.rs388
-rw-r--r--servo/components/style/values/generics/motion.rs270
-rw-r--r--servo/components/style/values/generics/page.rs162
-rw-r--r--servo/components/style/values/generics/position.rs238
-rw-r--r--servo/components/style/values/generics/ratio.rs50
-rw-r--r--servo/components/style/values/generics/rect.rs146
-rw-r--r--servo/components/style/values/generics/size.rs101
-rw-r--r--servo/components/style/values/generics/svg.rs221
-rw-r--r--servo/components/style/values/generics/text.rs148
-rw-r--r--servo/components/style/values/generics/transform.rs879
-rw-r--r--servo/components/style/values/generics/ui.rs129
-rw-r--r--servo/components/style/values/generics/url.rs47
-rw-r--r--servo/components/style/values/mod.rs796
-rw-r--r--servo/components/style/values/resolved/color.rs48
-rw-r--r--servo/components/style/values/resolved/counters.rs51
-rw-r--r--servo/components/style/values/resolved/mod.rs275
-rw-r--r--servo/components/style/values/specified/align.rs820
-rw-r--r--servo/components/style/values/specified/angle.rs276
-rw-r--r--servo/components/style/values/specified/animation.rs463
-rw-r--r--servo/components/style/values/specified/background.rs143
-rw-r--r--servo/components/style/values/specified/basic_shape.rs719
-rw-r--r--servo/components/style/values/specified/border.rs398
-rw-r--r--servo/components/style/values/specified/box.rs1945
-rw-r--r--servo/components/style/values/specified/calc.rs1086
-rw-r--r--servo/components/style/values/specified/color.rs1175
-rw-r--r--servo/components/style/values/specified/column.rs11
-rw-r--r--servo/components/style/values/specified/counters.rs279
-rw-r--r--servo/components/style/values/specified/easing.rs192
-rw-r--r--servo/components/style/values/specified/effects.rs453
-rw-r--r--servo/components/style/values/specified/flex.rs25
-rw-r--r--servo/components/style/values/specified/font.rs2222
-rw-r--r--servo/components/style/values/specified/gecko.rs82
-rw-r--r--servo/components/style/values/specified/grid.rs441
-rw-r--r--servo/components/style/values/specified/image.rs1340
-rw-r--r--servo/components/style/values/specified/length.rs2031
-rw-r--r--servo/components/style/values/specified/list.rs202
-rw-r--r--servo/components/style/values/specified/mod.rs992
-rw-r--r--servo/components/style/values/specified/motion.rs343
-rw-r--r--servo/components/style/values/specified/outline.rs71
-rw-r--r--servo/components/style/values/specified/page.rs99
-rw-r--r--servo/components/style/values/specified/percentage.rs225
-rw-r--r--servo/components/style/values/specified/position.rs955
-rw-r--r--servo/components/style/values/specified/ratio.rs32
-rw-r--r--servo/components/style/values/specified/rect.rs11
-rw-r--r--servo/components/style/values/specified/resolution.rs141
-rw-r--r--servo/components/style/values/specified/source_size_list.rs136
-rw-r--r--servo/components/style/values/specified/svg.rs404
-rw-r--r--servo/components/style/values/specified/svg_path.rs1029
-rw-r--r--servo/components/style/values/specified/table.rs36
-rw-r--r--servo/components/style/values/specified/text.rs1193
-rw-r--r--servo/components/style/values/specified/time.rs183
-rw-r--r--servo/components/style/values/specified/transform.rs530
-rw-r--r--servo/components/style/values/specified/ui.rs257
-rw-r--r--servo/components/style/values/specified/url.rs15
-rw-r--r--servo/components/style_derive/Cargo.toml18
-rw-r--r--servo/components/style_derive/animate.rs135
-rw-r--r--servo/components/style_derive/compute_squared_distance.rs125
-rw-r--r--servo/components/style_derive/lib.rs82
-rw-r--r--servo/components/style_derive/parse.rs323
-rw-r--r--servo/components/style_derive/specified_value_info.rs195
-rw-r--r--servo/components/style_derive/to_animated_value.rs35
-rw-r--r--servo/components/style_derive/to_animated_zero.rs65
-rw-r--r--servo/components/style_derive/to_computed_value.rs205
-rw-r--r--servo/components/style_derive/to_css.rs396
-rw-r--r--servo/components/style_derive/to_resolved_value.rs52
-rw-r--r--servo/components/style_traits/Cargo.toml32
-rw-r--r--servo/components/style_traits/arc_slice.rs162
-rw-r--r--servo/components/style_traits/dom.rs26
-rw-r--r--servo/components/style_traits/lib.rs295
-rw-r--r--servo/components/style_traits/owned_slice.rs198
-rw-r--r--servo/components/style_traits/owned_str.rs81
-rw-r--r--servo/components/style_traits/specified_value_info.rs138
-rw-r--r--servo/components/style_traits/values.rs569
-rw-r--r--servo/components/to_shmem/Cargo.toml22
-rw-r--r--servo/components/to_shmem/lib.rs618
-rw-r--r--servo/components/to_shmem_derive/Cargo.toml18
-rw-r--r--servo/components/to_shmem_derive/lib.rs26
-rw-r--r--servo/components/to_shmem_derive/to_shmem.rs78
358 files changed, 133035 insertions, 0 deletions
diff --git a/servo/components/derive_common/Cargo.toml b/servo/components/derive_common/Cargo.toml
new file mode 100644
index 0000000000..b493988026
--- /dev/null
+++ b/servo/components/derive_common/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "derive_common"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+publish = false
+
+[lib]
+path = "lib.rs"
+
+[dependencies]
+darling = { version = "0.20", default-features = false }
+proc-macro2 = "1"
+quote = "1"
+syn = { version = "2", default-features = false, features = ["clone-impls", "parsing"] }
+synstructure = "0.13"
diff --git a/servo/components/derive_common/cg.rs b/servo/components/derive_common/cg.rs
new file mode 100644
index 0000000000..73301af2ff
--- /dev/null
+++ b/servo/components/derive_common/cg.rs
@@ -0,0 +1,396 @@
+/* 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/. */
+
+use darling::{FromDeriveInput, FromField, FromVariant};
+use proc_macro2::{Span, TokenStream};
+use quote::TokenStreamExt;
+use syn::{self, AngleBracketedGenericArguments, AssocType, DeriveInput, Field};
+use syn::{GenericArgument, GenericParam, Ident, Path};
+use syn::{PathArguments, PathSegment, QSelf, Type, TypeArray, TypeGroup};
+use syn::{TypeParam, TypeParen, TypePath, TypeSlice, TypeTuple};
+use syn::{Variant, WherePredicate};
+use synstructure::{self, BindStyle, BindingInfo, VariantAst, VariantInfo};
+
+/// Given an input type which has some where clauses already, like:
+///
+/// struct InputType<T>
+/// where
+/// T: Zero,
+/// {
+/// ...
+/// }
+///
+/// Add the necessary `where` clauses so that the output type of a trait
+/// fulfils them.
+///
+/// For example:
+///
+/// ```ignore
+/// <T as ToComputedValue>::ComputedValue: Zero,
+/// ```
+///
+/// This needs to run before adding other bounds to the type parameters.
+pub fn propagate_clauses_to_output_type(
+ where_clause: &mut Option<syn::WhereClause>,
+ generics: &syn::Generics,
+ trait_path: &Path,
+ trait_output: &Ident,
+) {
+ let where_clause = match *where_clause {
+ Some(ref mut clause) => clause,
+ None => return,
+ };
+ let mut extra_bounds = vec![];
+ for pred in &where_clause.predicates {
+ let ty = match *pred {
+ syn::WherePredicate::Type(ref ty) => ty,
+ ref predicate => panic!("Unhanded complex where predicate: {:?}", predicate),
+ };
+
+ let path = match ty.bounded_ty {
+ syn::Type::Path(ref p) => &p.path,
+ ref ty => panic!("Unhanded complex where type: {:?}", ty),
+ };
+
+ assert!(
+ ty.lifetimes.is_none(),
+ "Unhanded complex lifetime bound: {:?}",
+ ty,
+ );
+
+ let ident = match path_to_ident(path) {
+ Some(i) => i,
+ None => panic!("Unhanded complex where type path: {:?}", path),
+ };
+
+ if generics.type_params().any(|param| param.ident == *ident) {
+ extra_bounds.push(ty.clone());
+ }
+ }
+
+ for bound in extra_bounds {
+ let ty = bound.bounded_ty;
+ let bounds = bound.bounds;
+ where_clause
+ .predicates
+ .push(parse_quote!(<#ty as #trait_path>::#trait_output: #bounds))
+ }
+}
+
+pub fn add_predicate(where_clause: &mut Option<syn::WhereClause>, pred: WherePredicate) {
+ where_clause
+ .get_or_insert(parse_quote!(where))
+ .predicates
+ .push(pred);
+}
+
+pub fn fmap_match<F>(input: &DeriveInput, bind_style: BindStyle, f: F) -> TokenStream
+where
+ F: FnMut(&BindingInfo) -> TokenStream,
+{
+ fmap2_match(input, bind_style, f, |_| None)
+}
+
+pub fn fmap2_match<F, G>(
+ input: &DeriveInput,
+ bind_style: BindStyle,
+ mut f: F,
+ mut g: G,
+) -> TokenStream
+where
+ F: FnMut(&BindingInfo) -> TokenStream,
+ G: FnMut(&BindingInfo) -> Option<TokenStream>,
+{
+ let mut s = synstructure::Structure::new(input);
+ s.variants_mut().iter_mut().for_each(|v| {
+ v.bind_with(|_| bind_style);
+ });
+ s.each_variant(|variant| {
+ let (mapped, mapped_fields) = value(variant, "mapped");
+ let fields_pairs = variant.bindings().iter().zip(mapped_fields.iter());
+ let mut computations = quote!();
+ computations.append_all(fields_pairs.map(|(field, mapped_field)| {
+ let expr = f(field);
+ quote! { let #mapped_field = #expr; }
+ }));
+ computations.append_all(
+ mapped_fields
+ .iter()
+ .map(|mapped_field| match g(mapped_field) {
+ Some(expr) => quote! { let #mapped_field = #expr; },
+ None => quote!(),
+ }),
+ );
+ computations.append_all(mapped);
+ Some(computations)
+ })
+}
+
+pub fn fmap_trait_output(input: &DeriveInput, trait_path: &Path, trait_output: &Ident) -> Path {
+ let segment = PathSegment {
+ ident: input.ident.clone(),
+ arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+ args: input
+ .generics
+ .params
+ .iter()
+ .map(|arg| match arg {
+ &GenericParam::Lifetime(ref data) => {
+ GenericArgument::Lifetime(data.lifetime.clone())
+ },
+ &GenericParam::Type(ref data) => {
+ let ident = &data.ident;
+ GenericArgument::Type(parse_quote!(<#ident as #trait_path>::#trait_output))
+ },
+ &GenericParam::Const(ref inner) => {
+ let ident = &inner.ident;
+ GenericArgument::Const(parse_quote!(#ident))
+ },
+ })
+ .collect(),
+ colon2_token: Default::default(),
+ gt_token: Default::default(),
+ lt_token: Default::default(),
+ }),
+ };
+ segment.into()
+}
+
+pub fn map_type_params<F>(ty: &Type, params: &[&TypeParam], self_type: &Path, f: &mut F) -> Type
+where
+ F: FnMut(&Ident) -> Type,
+{
+ match *ty {
+ Type::Slice(ref inner) => Type::from(TypeSlice {
+ elem: Box::new(map_type_params(&inner.elem, params, self_type, f)),
+ ..inner.clone()
+ }),
+ Type::Array(ref inner) => {
+ //ref ty, ref expr) => {
+ Type::from(TypeArray {
+ elem: Box::new(map_type_params(&inner.elem, params, self_type, f)),
+ ..inner.clone()
+ })
+ },
+ ref ty @ Type::Never(_) => ty.clone(),
+ Type::Tuple(ref inner) => Type::from(TypeTuple {
+ elems: inner
+ .elems
+ .iter()
+ .map(|ty| map_type_params(&ty, params, self_type, f))
+ .collect(),
+ ..inner.clone()
+ }),
+ Type::Path(TypePath {
+ qself: None,
+ ref path,
+ }) => {
+ if let Some(ident) = path_to_ident(path) {
+ if params.iter().any(|ref param| &param.ident == ident) {
+ return f(ident);
+ }
+ if ident == "Self" {
+ return Type::from(TypePath {
+ qself: None,
+ path: self_type.clone(),
+ });
+ }
+ }
+ Type::from(TypePath {
+ qself: None,
+ path: map_type_params_in_path(path, params, self_type, f),
+ })
+ },
+ Type::Path(TypePath {
+ ref qself,
+ ref path,
+ }) => Type::from(TypePath {
+ qself: qself.as_ref().map(|qself| QSelf {
+ ty: Box::new(map_type_params(&qself.ty, params, self_type, f)),
+ position: qself.position,
+ ..qself.clone()
+ }),
+ path: map_type_params_in_path(path, params, self_type, f),
+ }),
+ Type::Paren(ref inner) => Type::from(TypeParen {
+ elem: Box::new(map_type_params(&inner.elem, params, self_type, f)),
+ ..inner.clone()
+ }),
+ Type::Group(ref inner) => Type::from(TypeGroup {
+ elem: Box::new(map_type_params(&inner.elem, params, self_type, f)),
+ ..inner.clone()
+ }),
+ ref ty => panic!("type {:?} cannot be mapped yet", ty),
+ }
+}
+
+fn map_type_params_in_path<F>(
+ path: &Path,
+ params: &[&TypeParam],
+ self_type: &Path,
+ f: &mut F,
+) -> Path
+where
+ F: FnMut(&Ident) -> Type,
+{
+ Path {
+ leading_colon: path.leading_colon,
+ segments: path
+ .segments
+ .iter()
+ .map(|segment| PathSegment {
+ ident: segment.ident.clone(),
+ arguments: match segment.arguments {
+ PathArguments::AngleBracketed(ref data) => {
+ PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+ args: data
+ .args
+ .iter()
+ .map(|arg| match arg {
+ ty @ &GenericArgument::Lifetime(_) => ty.clone(),
+ &GenericArgument::Type(ref data) => GenericArgument::Type(
+ map_type_params(data, params, self_type, f),
+ ),
+ &GenericArgument::AssocType(ref data) => {
+ GenericArgument::AssocType(AssocType {
+ ty: map_type_params(&data.ty, params, self_type, f),
+ ..data.clone()
+ })
+ },
+ ref arg => panic!("arguments {:?} cannot be mapped yet", arg),
+ })
+ .collect(),
+ ..data.clone()
+ })
+ },
+ ref arg @ PathArguments::None => arg.clone(),
+ ref parameters => panic!("parameters {:?} cannot be mapped yet", parameters),
+ },
+ })
+ .collect(),
+ }
+}
+
+fn path_to_ident(path: &Path) -> Option<&Ident> {
+ match *path {
+ Path {
+ leading_colon: None,
+ ref segments,
+ } if segments.len() == 1 => {
+ if segments[0].arguments.is_empty() {
+ Some(&segments[0].ident)
+ } else {
+ None
+ }
+ },
+ _ => None,
+ }
+}
+
+pub fn parse_field_attrs<A>(field: &Field) -> A
+where
+ A: FromField,
+{
+ match A::from_field(field) {
+ Ok(attrs) => attrs,
+ Err(e) => panic!("failed to parse field attributes: {}", e),
+ }
+}
+
+pub fn parse_input_attrs<A>(input: &DeriveInput) -> A
+where
+ A: FromDeriveInput,
+{
+ match A::from_derive_input(input) {
+ Ok(attrs) => attrs,
+ Err(e) => panic!("failed to parse input attributes: {}", e),
+ }
+}
+
+pub fn parse_variant_attrs_from_ast<A>(variant: &VariantAst) -> A
+where
+ A: FromVariant,
+{
+ let v = Variant {
+ ident: variant.ident.clone(),
+ attrs: variant.attrs.to_vec(),
+ fields: variant.fields.clone(),
+ discriminant: variant.discriminant.clone(),
+ };
+ parse_variant_attrs(&v)
+}
+
+pub fn parse_variant_attrs<A>(variant: &Variant) -> A
+where
+ A: FromVariant,
+{
+ match A::from_variant(variant) {
+ Ok(attrs) => attrs,
+ Err(e) => panic!("failed to parse variant attributes: {}", e),
+ }
+}
+
+pub fn ref_pattern<'a>(
+ variant: &'a VariantInfo,
+ prefix: &str,
+) -> (TokenStream, Vec<BindingInfo<'a>>) {
+ let mut v = variant.clone();
+ v.bind_with(|_| BindStyle::Ref);
+ v.bindings_mut().iter_mut().for_each(|b| {
+ b.binding = Ident::new(&format!("{}_{}", b.binding, prefix), Span::call_site())
+ });
+ (v.pat(), v.bindings().to_vec())
+}
+
+pub fn value<'a>(variant: &'a VariantInfo, prefix: &str) -> (TokenStream, Vec<BindingInfo<'a>>) {
+ let mut v = variant.clone();
+ v.bindings_mut().iter_mut().for_each(|b| {
+ b.binding = Ident::new(&format!("{}_{}", b.binding, prefix), Span::call_site())
+ });
+ v.bind_with(|_| BindStyle::Move);
+ (v.pat(), v.bindings().to_vec())
+}
+
+/// Transforms "FooBar" to "foo-bar".
+///
+/// If the first Camel segment is "Moz", "Webkit", or "Servo", the result string
+/// is prepended with "-".
+pub fn to_css_identifier(mut camel_case: &str) -> String {
+ camel_case = camel_case.trim_end_matches('_');
+ let mut first = true;
+ let mut result = String::with_capacity(camel_case.len());
+ while let Some(segment) = split_camel_segment(&mut camel_case) {
+ if first {
+ match segment {
+ "Moz" | "Webkit" | "Servo" => first = false,
+ _ => {},
+ }
+ }
+ if !first {
+ result.push('-');
+ }
+ first = false;
+ result.push_str(&segment.to_lowercase());
+ }
+ result
+}
+
+/// Transforms foo-bar to FOO_BAR.
+pub fn to_scream_case(css_case: &str) -> String {
+ css_case.to_uppercase().replace('-', "_")
+}
+
+/// Given "FooBar", returns "Foo" and sets `camel_case` to "Bar".
+fn split_camel_segment<'input>(camel_case: &mut &'input str) -> Option<&'input str> {
+ let index = match camel_case.chars().next() {
+ None => return None,
+ Some(ch) => ch.len_utf8(),
+ };
+ let end_position = camel_case[index..]
+ .find(char::is_uppercase)
+ .map_or(camel_case.len(), |pos| index + pos);
+ let result = &camel_case[..end_position];
+ *camel_case = &camel_case[end_position..];
+ Some(result)
+}
diff --git a/servo/components/derive_common/lib.rs b/servo/components/derive_common/lib.rs
new file mode 100644
index 0000000000..1441535144
--- /dev/null
+++ b/servo/components/derive_common/lib.rs
@@ -0,0 +1,13 @@
+/* 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/. */
+
+extern crate darling;
+extern crate proc_macro2;
+#[macro_use]
+extern crate quote;
+#[macro_use]
+extern crate syn;
+extern crate synstructure;
+
+pub mod cg;
diff --git a/servo/components/malloc_size_of/Cargo.toml b/servo/components/malloc_size_of/Cargo.toml
new file mode 100644
index 0000000000..cd5deaea44
--- /dev/null
+++ b/servo/components/malloc_size_of/Cargo.toml
@@ -0,0 +1,53 @@
+[package]
+name = "malloc_size_of"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MIT/Apache-2.0"
+publish = false
+
+[lib]
+path = "lib.rs"
+
+[features]
+servo = [
+ "accountable-refcell",
+ "content-security-policy",
+ "crossbeam-channel",
+ "hyper",
+ "hyper_serde",
+ "keyboard-types",
+ "serde",
+ "serde_bytes",
+ "string_cache",
+ "time",
+ "url",
+ "uuid",
+ "webrender_api",
+ "xml5ever",
+]
+
+[dependencies]
+accountable-refcell = { version = "0.2.0", optional = true }
+app_units = "0.7"
+content-security-policy = { version = "0.4.0", features = ["serde"], optional = true }
+crossbeam-channel = { version = "0.4", optional = true }
+cssparser = "0.33"
+dom = { path = "../../../dom/base/rust" }
+euclid = "0.22"
+hyper = { version = "0.12", optional = true }
+hyper_serde = { version = "0.11", optional = true }
+keyboard-types = { version = "0.4.3", optional = true }
+selectors = { path = "../selectors" }
+serde = { version = "1.0.27", optional = true }
+serde_bytes = { version = "0.11", optional = true }
+servo_arc = { path = "../servo_arc" }
+smallbitvec = "2.3.0"
+smallvec = "1.0"
+string_cache = { version = "0.8", optional = true }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+time = { version = "0.1.17", optional = true }
+url = { version = "2.4", optional = true }
+uuid = { version = "0.8", features = ["v4"], optional = true }
+void = "1.0.2"
+webrender_api = { git = "https://github.com/servo/webrender", optional = true }
+xml5ever = { version = "0.16", optional = true }
diff --git a/servo/components/malloc_size_of/LICENSE-APACHE b/servo/components/malloc_size_of/LICENSE-APACHE
new file mode 100644
index 0000000000..16fe87b06e
--- /dev/null
+++ b/servo/components/malloc_size_of/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/servo/components/malloc_size_of/LICENSE-MIT b/servo/components/malloc_size_of/LICENSE-MIT
new file mode 100644
index 0000000000..31aa79387f
--- /dev/null
+++ b/servo/components/malloc_size_of/LICENSE-MIT
@@ -0,0 +1,23 @@
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/servo/components/malloc_size_of/lib.rs b/servo/components/malloc_size_of/lib.rs
new file mode 100644
index 0000000000..32d7f03bfb
--- /dev/null
+++ b/servo/components/malloc_size_of/lib.rs
@@ -0,0 +1,1003 @@
+// Copyright 2016-2017 The Servo Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! A crate for measuring the heap usage of data structures in a way that
+//! integrates with Firefox's memory reporting, particularly the use of
+//! mozjemalloc and DMD. In particular, it has the following features.
+//! - It isn't bound to a particular heap allocator.
+//! - It provides traits for both "shallow" and "deep" measurement, which gives
+//! flexibility in the cases where the traits can't be used.
+//! - It allows for measuring blocks even when only an interior pointer can be
+//! obtained for heap allocations, e.g. `HashSet` and `HashMap`. (This relies
+//! on the heap allocator having suitable support, which mozjemalloc has.)
+//! - It allows handling of types like `Rc` and `Arc` by providing traits that
+//! are different to the ones for non-graph structures.
+//!
+//! Suggested uses are as follows.
+//! - When possible, use the `MallocSizeOf` trait. (Deriving support is
+//! provided by the `malloc_size_of_derive` crate.)
+//! - If you need an additional synchronization argument, provide a function
+//! that is like the standard trait method, but with the extra argument.
+//! - If you need multiple measurements for a type, provide a function named
+//! `add_size_of` that takes a mutable reference to a struct that contains
+//! the multiple measurement fields.
+//! - When deep measurement (via `MallocSizeOf`) cannot be implemented for a
+//! type, shallow measurement (via `MallocShallowSizeOf`) in combination with
+//! iteration can be a useful substitute.
+//! - `Rc` and `Arc` are always tricky, which is why `MallocSizeOf` is not (and
+//! should not be) implemented for them.
+//! - If an `Rc` or `Arc` is known to be a "primary" reference and can always
+//! be measured, it should be measured via the `MallocUnconditionalSizeOf`
+//! trait.
+//! - If an `Rc` or `Arc` should be measured only if it hasn't been seen
+//! before, it should be measured via the `MallocConditionalSizeOf` trait.
+//! - Using universal function call syntax is a good idea when measuring boxed
+//! fields in structs, because it makes it clear that the Box is being
+//! measured as well as the thing it points to. E.g.
+//! `<Box<_> as MallocSizeOf>::size_of(field, ops)`.
+//!
+//! Note: WebRender has a reduced fork of this crate, so that we can avoid
+//! publishing this crate on crates.io.
+
+#[cfg(feature = "servo")]
+extern crate accountable_refcell;
+extern crate app_units;
+#[cfg(feature = "servo")]
+extern crate content_security_policy;
+#[cfg(feature = "servo")]
+extern crate crossbeam_channel;
+extern crate cssparser;
+extern crate euclid;
+#[cfg(feature = "servo")]
+extern crate hyper;
+#[cfg(feature = "servo")]
+extern crate hyper_serde;
+#[cfg(feature = "servo")]
+extern crate keyboard_types;
+extern crate selectors;
+#[cfg(feature = "servo")]
+extern crate serde;
+#[cfg(feature = "servo")]
+extern crate serde_bytes;
+extern crate servo_arc;
+extern crate smallbitvec;
+extern crate smallvec;
+#[cfg(feature = "servo")]
+extern crate string_cache;
+#[cfg(feature = "servo")]
+extern crate time;
+#[cfg(feature = "url")]
+extern crate url;
+#[cfg(feature = "servo")]
+extern crate uuid;
+extern crate void;
+#[cfg(feature = "webrender_api")]
+extern crate webrender_api;
+#[cfg(feature = "servo")]
+extern crate xml5ever;
+
+#[cfg(feature = "servo")]
+use content_security_policy as csp;
+#[cfg(feature = "servo")]
+use serde_bytes::ByteBuf;
+use std::hash::{BuildHasher, Hash};
+use std::mem::size_of;
+use std::ops::Range;
+use std::ops::{Deref, DerefMut};
+use std::os::raw::c_void;
+#[cfg(feature = "servo")]
+use uuid::Uuid;
+use void::Void;
+
+/// A C function that takes a pointer to a heap allocation and returns its size.
+type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize;
+
+/// A closure implementing a stateful predicate on pointers.
+type VoidPtrToBoolFnMut = dyn FnMut(*const c_void) -> bool;
+
+/// Operations used when measuring heap usage of data structures.
+pub struct MallocSizeOfOps {
+ /// A function that returns the size of a heap allocation.
+ size_of_op: VoidPtrToSizeFn,
+
+ /// Like `size_of_op`, but can take an interior pointer. Optional because
+ /// not all allocators support this operation. If it's not provided, some
+ /// memory measurements will actually be computed estimates rather than
+ /// real and accurate measurements.
+ enclosing_size_of_op: Option<VoidPtrToSizeFn>,
+
+ /// Check if a pointer has been seen before, and remember it for next time.
+ /// Useful when measuring `Rc`s and `Arc`s. Optional, because many places
+ /// don't need it.
+ have_seen_ptr_op: Option<Box<VoidPtrToBoolFnMut>>,
+}
+
+impl MallocSizeOfOps {
+ pub fn new(
+ size_of: VoidPtrToSizeFn,
+ malloc_enclosing_size_of: Option<VoidPtrToSizeFn>,
+ have_seen_ptr: Option<Box<VoidPtrToBoolFnMut>>,
+ ) -> Self {
+ MallocSizeOfOps {
+ size_of_op: size_of,
+ enclosing_size_of_op: malloc_enclosing_size_of,
+ have_seen_ptr_op: have_seen_ptr,
+ }
+ }
+
+ /// Check if an allocation is empty. This relies on knowledge of how Rust
+ /// handles empty allocations, which may change in the future.
+ fn is_empty<T: ?Sized>(ptr: *const T) -> bool {
+ // The correct condition is this:
+ // `ptr as usize <= ::std::mem::align_of::<T>()`
+ // But we can't call align_of() on a ?Sized T. So we approximate it
+ // with the following. 256 is large enough that it should always be
+ // larger than the required alignment, but small enough that it is
+ // always in the first page of memory and therefore not a legitimate
+ // address.
+ return ptr as *const usize as usize <= 256;
+ }
+
+ /// Call `size_of_op` on `ptr`, first checking that the allocation isn't
+ /// empty, because some types (such as `Vec`) utilize empty allocations.
+ pub unsafe fn malloc_size_of<T: ?Sized>(&self, ptr: *const T) -> usize {
+ if MallocSizeOfOps::is_empty(ptr) {
+ 0
+ } else {
+ (self.size_of_op)(ptr as *const c_void)
+ }
+ }
+
+ /// Is an `enclosing_size_of_op` available?
+ pub fn has_malloc_enclosing_size_of(&self) -> bool {
+ self.enclosing_size_of_op.is_some()
+ }
+
+ /// Call `enclosing_size_of_op`, which must be available, on `ptr`, which
+ /// must not be empty.
+ pub unsafe fn malloc_enclosing_size_of<T>(&self, ptr: *const T) -> usize {
+ assert!(!MallocSizeOfOps::is_empty(ptr));
+ (self.enclosing_size_of_op.unwrap())(ptr as *const c_void)
+ }
+
+ /// Call `have_seen_ptr_op` on `ptr`.
+ pub fn have_seen_ptr<T>(&mut self, ptr: *const T) -> bool {
+ let have_seen_ptr_op = self
+ .have_seen_ptr_op
+ .as_mut()
+ .expect("missing have_seen_ptr_op");
+ have_seen_ptr_op(ptr as *const c_void)
+ }
+}
+
+/// Trait for measuring the "deep" heap usage of a data structure. This is the
+/// most commonly-used of the traits.
+pub trait MallocSizeOf {
+ /// Measure the heap usage of all descendant heap-allocated structures, but
+ /// not the space taken up by the value itself.
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize;
+}
+
+/// Trait for measuring the "shallow" heap usage of a container.
+pub trait MallocShallowSizeOf {
+ /// Measure the heap usage of immediate heap-allocated descendant
+ /// structures, but not the space taken up by the value itself. Anything
+ /// beyond the immediate descendants must be measured separately, using
+ /// iteration.
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize;
+}
+
+/// Like `MallocSizeOf`, but with a different name so it cannot be used
+/// accidentally with derive(MallocSizeOf). For use with types like `Rc` and
+/// `Arc` when appropriate (e.g. when measuring a "primary" reference).
+pub trait MallocUnconditionalSizeOf {
+ /// Measure the heap usage of all heap-allocated descendant structures, but
+ /// not the space taken up by the value itself.
+ fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize;
+}
+
+/// `MallocUnconditionalSizeOf` combined with `MallocShallowSizeOf`.
+pub trait MallocUnconditionalShallowSizeOf {
+ /// `unconditional_size_of` combined with `shallow_size_of`.
+ fn unconditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize;
+}
+
+/// Like `MallocSizeOf`, but only measures if the value hasn't already been
+/// measured. For use with types like `Rc` and `Arc` when appropriate (e.g.
+/// when there is no "primary" reference).
+pub trait MallocConditionalSizeOf {
+ /// Measure the heap usage of all heap-allocated descendant structures, but
+ /// not the space taken up by the value itself, and only if that heap usage
+ /// hasn't already been measured.
+ fn conditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize;
+}
+
+/// `MallocConditionalSizeOf` combined with `MallocShallowSizeOf`.
+pub trait MallocConditionalShallowSizeOf {
+ /// `conditional_size_of` combined with `shallow_size_of`.
+ fn conditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize;
+}
+
+impl MallocSizeOf for String {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(self.as_ptr()) }
+ }
+}
+
+impl<'a, T: ?Sized> MallocSizeOf for &'a T {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ // Zero makes sense for a non-owning reference.
+ 0
+ }
+}
+
+impl<T: ?Sized> MallocShallowSizeOf for Box<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(&**self) }
+ }
+}
+
+impl<T: MallocSizeOf + ?Sized> MallocSizeOf for Box<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.shallow_size_of(ops) + (**self).size_of(ops)
+ }
+}
+
+impl MallocSizeOf for () {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ 0
+ }
+}
+
+impl<T1, T2> MallocSizeOf for (T1, T2)
+where
+ T1: MallocSizeOf,
+ T2: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops) + self.1.size_of(ops)
+ }
+}
+
+impl<T1, T2, T3> MallocSizeOf for (T1, T2, T3)
+where
+ T1: MallocSizeOf,
+ T2: MallocSizeOf,
+ T3: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops)
+ }
+}
+
+impl<T1, T2, T3, T4> MallocSizeOf for (T1, T2, T3, T4)
+where
+ T1: MallocSizeOf,
+ T2: MallocSizeOf,
+ T3: MallocSizeOf,
+ T4: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops) + self.3.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for Option<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if let Some(val) = self.as_ref() {
+ val.size_of(ops)
+ } else {
+ 0
+ }
+ }
+}
+
+impl<T: MallocSizeOf, E: MallocSizeOf> MallocSizeOf for Result<T, E> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match *self {
+ Ok(ref x) => x.size_of(ops),
+ Err(ref e) => e.size_of(ops),
+ }
+ }
+}
+
+impl<T: MallocSizeOf + Copy> MallocSizeOf for std::cell::Cell<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.get().size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for std::cell::RefCell<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.borrow().size_of(ops)
+ }
+}
+
+impl<'a, B: ?Sized + ToOwned> MallocSizeOf for std::borrow::Cow<'a, B>
+where
+ B::Owned: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match *self {
+ std::borrow::Cow::Borrowed(_) => 0,
+ std::borrow::Cow::Owned(ref b) => b.size_of(ops),
+ }
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for [T] {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = 0;
+ for elem in self.iter() {
+ n += elem.size_of(ops);
+ }
+ n
+ }
+}
+
+#[cfg(feature = "servo")]
+impl MallocShallowSizeOf for ByteBuf {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(self.as_ptr()) }
+ }
+}
+
+#[cfg(feature = "servo")]
+impl MallocSizeOf for ByteBuf {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for elem in self.iter() {
+ n += elem.size_of(ops);
+ }
+ n
+ }
+}
+
+impl<T> MallocShallowSizeOf for Vec<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(self.as_ptr()) }
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for Vec<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for elem in self.iter() {
+ n += elem.size_of(ops);
+ }
+ n
+ }
+}
+
+impl<T> MallocShallowSizeOf for std::collections::VecDeque<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if ops.has_malloc_enclosing_size_of() {
+ if let Some(front) = self.front() {
+ // The front element is an interior pointer.
+ unsafe { ops.malloc_enclosing_size_of(&*front) }
+ } else {
+ // This assumes that no memory is allocated when the VecDeque is empty.
+ 0
+ }
+ } else {
+ // An estimate.
+ self.capacity() * size_of::<T>()
+ }
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for std::collections::VecDeque<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for elem in self.iter() {
+ n += elem.size_of(ops);
+ }
+ n
+ }
+}
+
+impl<A: smallvec::Array> MallocShallowSizeOf for smallvec::SmallVec<A> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if self.spilled() {
+ unsafe { ops.malloc_size_of(self.as_ptr()) }
+ } else {
+ 0
+ }
+ }
+}
+
+impl<A> MallocSizeOf for smallvec::SmallVec<A>
+where
+ A: smallvec::Array,
+ A::Item: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for elem in self.iter() {
+ n += elem.size_of(ops);
+ }
+ n
+ }
+}
+
+impl<T> MallocShallowSizeOf for thin_vec::ThinVec<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if self.capacity() == 0 {
+ // If it's the singleton we might not be a heap pointer.
+ return 0;
+ }
+
+ assert_eq!(
+ std::mem::size_of::<Self>(),
+ std::mem::size_of::<*const ()>()
+ );
+ unsafe { ops.malloc_size_of(*(self as *const Self as *const *const ())) }
+ }
+}
+
+impl<T: MallocSizeOf> MallocSizeOf for thin_vec::ThinVec<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for elem in self.iter() {
+ n += elem.size_of(ops);
+ }
+ n
+ }
+}
+
+macro_rules! malloc_size_of_hash_set {
+ ($ty:ty) => {
+ impl<T, S> MallocShallowSizeOf for $ty
+ where
+ T: Eq + Hash,
+ S: BuildHasher,
+ {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if ops.has_malloc_enclosing_size_of() {
+ // The first value from the iterator gives us an interior pointer.
+ // `ops.malloc_enclosing_size_of()` then gives us the storage size.
+ // This assumes that the `HashSet`'s contents (values and hashes)
+ // are all stored in a single contiguous heap allocation.
+ self.iter()
+ .next()
+ .map_or(0, |t| unsafe { ops.malloc_enclosing_size_of(t) })
+ } else {
+ // An estimate.
+ self.capacity() * (size_of::<T>() + size_of::<usize>())
+ }
+ }
+ }
+
+ impl<T, S> MallocSizeOf for $ty
+ where
+ T: Eq + Hash + MallocSizeOf,
+ S: BuildHasher,
+ {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for t in self.iter() {
+ n += t.size_of(ops);
+ }
+ n
+ }
+ }
+ };
+}
+
+malloc_size_of_hash_set!(std::collections::HashSet<T, S>);
+
+macro_rules! malloc_size_of_hash_map {
+ ($ty:ty) => {
+ impl<K, V, S> MallocShallowSizeOf for $ty
+ where
+ K: Eq + Hash,
+ S: BuildHasher,
+ {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ // See the implementation for std::collections::HashSet for details.
+ if ops.has_malloc_enclosing_size_of() {
+ self.values()
+ .next()
+ .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) })
+ } else {
+ self.capacity() * (size_of::<V>() + size_of::<K>() + size_of::<usize>())
+ }
+ }
+ }
+
+ impl<K, V, S> MallocSizeOf for $ty
+ where
+ K: Eq + Hash + MallocSizeOf,
+ V: MallocSizeOf,
+ S: BuildHasher,
+ {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for (k, v) in self.iter() {
+ n += k.size_of(ops);
+ n += v.size_of(ops);
+ }
+ n
+ }
+ }
+ };
+}
+
+malloc_size_of_hash_map!(std::collections::HashMap<K, V, S>);
+
+impl<K, V> MallocShallowSizeOf for std::collections::BTreeMap<K, V>
+where
+ K: Eq + Hash,
+{
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if ops.has_malloc_enclosing_size_of() {
+ self.values()
+ .next()
+ .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) })
+ } else {
+ self.len() * (size_of::<V>() + size_of::<K>() + size_of::<usize>())
+ }
+ }
+}
+
+impl<K, V> MallocSizeOf for std::collections::BTreeMap<K, V>
+where
+ K: Eq + Hash + MallocSizeOf,
+ V: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.shallow_size_of(ops);
+ for (k, v) in self.iter() {
+ n += k.size_of(ops);
+ n += v.size_of(ops);
+ }
+ n
+ }
+}
+
+// PhantomData is always 0.
+impl<T> MallocSizeOf for std::marker::PhantomData<T> {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ 0
+ }
+}
+
+// XXX: we don't want MallocSizeOf to be defined for Rc and Arc. If negative
+// trait bounds are ever allowed, this code should be uncommented.
+// (We do have a compile-fail test for this:
+// rc_arc_must_not_derive_malloc_size_of.rs)
+//impl<T> !MallocSizeOf for Arc<T> { }
+//impl<T> !MallocShallowSizeOf for Arc<T> { }
+
+impl<T> MallocUnconditionalShallowSizeOf for servo_arc::Arc<T> {
+ fn unconditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(self.heap_ptr()) }
+ }
+}
+
+impl<T: MallocSizeOf> MallocUnconditionalSizeOf for servo_arc::Arc<T> {
+ fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.unconditional_shallow_size_of(ops) + (**self).size_of(ops)
+ }
+}
+
+impl<T> MallocConditionalShallowSizeOf for servo_arc::Arc<T> {
+ fn conditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if ops.have_seen_ptr(self.heap_ptr()) {
+ 0
+ } else {
+ self.unconditional_shallow_size_of(ops)
+ }
+ }
+}
+
+impl<T: MallocSizeOf> MallocConditionalSizeOf for servo_arc::Arc<T> {
+ fn conditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if ops.have_seen_ptr(self.heap_ptr()) {
+ 0
+ } else {
+ self.unconditional_size_of(ops)
+ }
+ }
+}
+
+/// If a mutex is stored directly as a member of a data type that is being measured,
+/// it is the unique owner of its contents and deserves to be measured.
+///
+/// If a mutex is stored inside of an Arc value as a member of a data type that is being measured,
+/// the Arc will not be automatically measured so there is no risk of overcounting the mutex's
+/// contents.
+impl<T: MallocSizeOf> MallocSizeOf for std::sync::Mutex<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ (*self.lock().unwrap()).size_of(ops)
+ }
+}
+
+impl MallocSizeOf for smallbitvec::SmallBitVec {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ if let Some(ptr) = self.heap_ptr() {
+ unsafe { ops.malloc_size_of(ptr) }
+ } else {
+ 0
+ }
+ }
+}
+
+impl<T: MallocSizeOf, Unit> MallocSizeOf for euclid::Length<T, Unit> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Scale<T, Src, Dst> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Point2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.x.size_of(ops) + self.y.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Rect<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.origin.size_of(ops) + self.size.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::SideOffsets2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.top.size_of(ops) +
+ self.right.size_of(ops) +
+ self.bottom.size_of(ops) +
+ self.left.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Size2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.width.size_of(ops) + self.height.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Transform2D<T, Src, Dst> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.m11.size_of(ops) +
+ self.m12.size_of(ops) +
+ self.m21.size_of(ops) +
+ self.m22.size_of(ops) +
+ self.m31.size_of(ops) +
+ self.m32.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Transform3D<T, Src, Dst> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.m11.size_of(ops) +
+ self.m12.size_of(ops) +
+ self.m13.size_of(ops) +
+ self.m14.size_of(ops) +
+ self.m21.size_of(ops) +
+ self.m22.size_of(ops) +
+ self.m23.size_of(ops) +
+ self.m24.size_of(ops) +
+ self.m31.size_of(ops) +
+ self.m32.size_of(ops) +
+ self.m33.size_of(ops) +
+ self.m34.size_of(ops) +
+ self.m41.size_of(ops) +
+ self.m42.size_of(ops) +
+ self.m43.size_of(ops) +
+ self.m44.size_of(ops)
+ }
+}
+
+impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Vector2D<T, U> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.x.size_of(ops) + self.y.size_of(ops)
+ }
+}
+
+impl MallocSizeOf for selectors::parser::AncestorHashes {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let selectors::parser::AncestorHashes { ref packed_hashes } = *self;
+ packed_hashes.size_of(ops)
+ }
+}
+
+impl<Impl: selectors::parser::SelectorImpl> MallocUnconditionalSizeOf
+ for selectors::parser::Selector<Impl>
+where
+ Impl::NonTSPseudoClass: MallocSizeOf,
+ Impl::PseudoElement: MallocSizeOf,
+{
+ fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = 0;
+
+ // It's OK to measure this ThinArc directly because it's the
+ // "primary" reference. (The secondary references are on the
+ // Stylist.)
+ n += unsafe { ops.malloc_size_of(self.thin_arc_heap_ptr()) };
+ for component in self.iter_raw_match_order() {
+ n += component.size_of(ops);
+ }
+
+ n
+ }
+}
+
+impl<Impl: selectors::parser::SelectorImpl> MallocUnconditionalSizeOf
+ for selectors::parser::SelectorList<Impl>
+where
+ Impl::NonTSPseudoClass: MallocSizeOf,
+ Impl::PseudoElement: MallocSizeOf,
+{
+ fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = 0;
+
+ // It's OK to measure this ThinArc directly because it's the "primary" reference. (The
+ // secondary references are on the Stylist.)
+ n += unsafe { ops.malloc_size_of(self.thin_arc_heap_ptr()) };
+ if self.len() > 1 {
+ for selector in self.slice().iter() {
+ n += selector.size_of(ops);
+ }
+ }
+ n
+ }
+}
+
+impl<Impl: selectors::parser::SelectorImpl> MallocUnconditionalSizeOf
+ for selectors::parser::Component<Impl>
+where
+ Impl::NonTSPseudoClass: MallocSizeOf,
+ Impl::PseudoElement: MallocSizeOf,
+{
+ fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ use selectors::parser::Component;
+
+ match self {
+ Component::AttributeOther(ref attr_selector) => attr_selector.size_of(ops),
+ Component::Negation(ref components) => components.unconditional_size_of(ops),
+ Component::NonTSPseudoClass(ref pseudo) => (*pseudo).size_of(ops),
+ Component::Slotted(ref selector) | Component::Host(Some(ref selector)) => {
+ selector.unconditional_size_of(ops)
+ },
+ Component::Is(ref list) | Component::Where(ref list) => list.unconditional_size_of(ops),
+ Component::Has(ref relative_selectors) => relative_selectors.size_of(ops),
+ Component::NthOf(ref nth_of_data) => nth_of_data.size_of(ops),
+ Component::PseudoElement(ref pseudo) => (*pseudo).size_of(ops),
+ Component::Combinator(..) |
+ Component::ExplicitAnyNamespace |
+ Component::ExplicitNoNamespace |
+ Component::DefaultNamespace(..) |
+ Component::Namespace(..) |
+ Component::ExplicitUniversalType |
+ Component::LocalName(..) |
+ Component::ID(..) |
+ Component::Part(..) |
+ Component::Class(..) |
+ Component::AttributeInNoNamespaceExists { .. } |
+ Component::AttributeInNoNamespace { .. } |
+ Component::Root |
+ Component::Empty |
+ Component::Scope |
+ Component::ParentSelector |
+ Component::Nth(..) |
+ Component::Host(None) |
+ Component::RelativeSelectorAnchor |
+ Component::Invalid(..) => 0,
+ }
+ }
+}
+
+impl<Impl: selectors::parser::SelectorImpl> MallocSizeOf
+ for selectors::attr::AttrSelectorWithOptionalNamespace<Impl>
+{
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ 0
+ }
+}
+
+impl MallocSizeOf for Void {
+ #[inline]
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ void::unreachable(*self)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Static: string_cache::StaticAtomSet> MallocSizeOf for string_cache::Atom<Static> {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ 0
+ }
+}
+
+/// For use on types where size_of() returns 0.
+#[macro_export]
+macro_rules! malloc_size_of_is_0(
+ ($($ty:ty),+) => (
+ $(
+ impl $crate::MallocSizeOf for $ty {
+ #[inline(always)]
+ fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize {
+ 0
+ }
+ }
+ )+
+ );
+ ($($ty:ident<$($gen:ident),+>),+) => (
+ $(
+ impl<$($gen: $crate::MallocSizeOf),+> $crate::MallocSizeOf for $ty<$($gen),+> {
+ #[inline(always)]
+ fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize {
+ 0
+ }
+ }
+ )+
+ );
+);
+
+malloc_size_of_is_0!(bool, char, str);
+malloc_size_of_is_0!(u8, u16, u32, u64, u128, usize);
+malloc_size_of_is_0!(i8, i16, i32, i64, i128, isize);
+malloc_size_of_is_0!(f32, f64);
+
+malloc_size_of_is_0!(std::sync::atomic::AtomicBool);
+malloc_size_of_is_0!(std::sync::atomic::AtomicIsize);
+malloc_size_of_is_0!(std::sync::atomic::AtomicUsize);
+malloc_size_of_is_0!(std::num::NonZeroUsize);
+
+malloc_size_of_is_0!(Range<u8>, Range<u16>, Range<u32>, Range<u64>, Range<usize>);
+malloc_size_of_is_0!(Range<i8>, Range<i16>, Range<i32>, Range<i64>, Range<isize>);
+malloc_size_of_is_0!(Range<f32>, Range<f64>);
+
+malloc_size_of_is_0!(app_units::Au);
+
+malloc_size_of_is_0!(cssparser::TokenSerializationType, cssparser::SourceLocation, cssparser::SourcePosition);
+
+malloc_size_of_is_0!(dom::ElementState, dom::DocumentState);
+
+#[cfg(feature = "servo")]
+malloc_size_of_is_0!(csp::Destination);
+
+#[cfg(feature = "servo")]
+malloc_size_of_is_0!(Uuid);
+
+#[cfg(feature = "url")]
+impl MallocSizeOf for url::Host {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match *self {
+ url::Host::Domain(ref s) => s.size_of(ops),
+ _ => 0,
+ }
+ }
+}
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::BorderRadius);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::BorderStyle);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::BoxShadowClipMode);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::ColorF);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::ComplexClipRegion);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::ExtendMode);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::FilterOp);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::ExternalScrollId);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::FontInstanceKey);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::GradientStop);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::GlyphInstance);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::NinePatchBorder);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::ImageKey);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::ImageRendering);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::LineStyle);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::MixBlendMode);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::NormalBorder);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::RepeatMode);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::StickyOffsetBounds);
+#[cfg(feature = "webrender_api")]
+malloc_size_of_is_0!(webrender_api::TransformStyle);
+
+#[cfg(feature = "servo")]
+impl MallocSizeOf for keyboard_types::Key {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match self {
+ keyboard_types::Key::Character(ref s) => s.size_of(ops),
+ _ => 0,
+ }
+ }
+}
+
+#[cfg(feature = "servo")]
+malloc_size_of_is_0!(keyboard_types::Modifiers);
+
+#[cfg(feature = "servo")]
+impl MallocSizeOf for xml5ever::QualName {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.prefix.size_of(ops) + self.ns.size_of(ops) + self.local.size_of(ops)
+ }
+}
+
+#[cfg(feature = "servo")]
+malloc_size_of_is_0!(time::Duration);
+#[cfg(feature = "servo")]
+malloc_size_of_is_0!(time::Tm);
+
+#[cfg(feature = "servo")]
+impl<T> MallocSizeOf for hyper_serde::Serde<T>
+where
+ for<'de> hyper_serde::De<T>: serde::Deserialize<'de>,
+ for<'a> hyper_serde::Ser<'a, T>: serde::Serialize,
+ T: MallocSizeOf,
+{
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.0.size_of(ops)
+ }
+}
+
+// Placeholder for unique case where internals of Sender cannot be measured.
+// malloc size of is 0 macro complains about type supplied!
+#[cfg(feature = "servo")]
+impl<T> MallocSizeOf for crossbeam_channel::Sender<T> {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ 0
+ }
+}
+
+#[cfg(feature = "servo")]
+impl MallocSizeOf for hyper::StatusCode {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ 0
+ }
+}
+
+/// Measurable that defers to inner value and used to verify MallocSizeOf implementation in a
+/// struct.
+#[derive(Clone)]
+pub struct Measurable<T: MallocSizeOf>(pub T);
+
+impl<T: MallocSizeOf> Deref for Measurable<T> {
+ type Target = T;
+
+ fn deref(&self) -> &T {
+ &self.0
+ }
+}
+
+impl<T: MallocSizeOf> DerefMut for Measurable<T> {
+ fn deref_mut(&mut self) -> &mut T {
+ &mut self.0
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<T: MallocSizeOf> MallocSizeOf for accountable_refcell::RefCell<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.borrow().size_of(ops)
+ }
+}
diff --git a/servo/components/selectors/CHANGES.md b/servo/components/selectors/CHANGES.md
new file mode 100644
index 0000000000..b1e9511dca
--- /dev/null
+++ b/servo/components/selectors/CHANGES.md
@@ -0,0 +1 @@
+- `parser.rs` no longer wraps values in quotes (`"..."`) but expects their `to_css` impl to already wrap it ([Gecko Bug 1854809](https://bugzilla.mozilla.org/show_bug.cgi?id=1854809))
diff --git a/servo/components/selectors/Cargo.toml b/servo/components/selectors/Cargo.toml
new file mode 100644
index 0000000000..88360d7dd2
--- /dev/null
+++ b/servo/components/selectors/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "selectors"
+version = "0.22.0"
+authors = ["The Servo Project Developers"]
+documentation = "https://docs.rs/selectors/"
+description = "CSS Selectors matching for Rust"
+repository = "https://github.com/servo/servo"
+readme = "README.md"
+keywords = ["css", "selectors"]
+license = "MPL-2.0"
+build = "build.rs"
+
+[lib]
+name = "selectors"
+path = "lib.rs"
+
+[features]
+bench = []
+
+[dependencies]
+bitflags = "2"
+cssparser = "0.33"
+derive_more = { version = "0.99", default-features = false, features = ["add", "add_assign"] }
+fxhash = "0.2"
+log = "0.4"
+phf = "0.11"
+precomputed-hash = "0.1"
+servo_arc = { version = "0.1", path = "../servo_arc" }
+smallvec = "1.0"
+to_shmem = { path = "../to_shmem" }
+to_shmem_derive = { path = "../to_shmem_derive" }
+new_debug_unreachable = "1"
+
+[build-dependencies]
+phf_codegen = "0.11"
diff --git a/servo/components/selectors/README.md b/servo/components/selectors/README.md
new file mode 100644
index 0000000000..dac4a7ff91
--- /dev/null
+++ b/servo/components/selectors/README.md
@@ -0,0 +1,25 @@
+rust-selectors
+==============
+
+* [![Build Status](https://travis-ci.com/servo/rust-selectors.svg?branch=master)](
+ https://travis-ci.com/servo/rust-selectors)
+* [Documentation](https://docs.rs/selectors/)
+* [crates.io](https://crates.io/crates/selectors)
+
+CSS Selectors library for Rust.
+Includes parsing and serilization of selectors,
+as well as matching against a generic tree of elements.
+Pseudo-elements and most pseudo-classes are generic as well.
+
+**Warning:** breaking changes are made to this library fairly frequently
+(13 times in 2016, for example).
+However you can use this crate without updating it that often,
+old versions stay available on crates.io and Cargo will only automatically update
+to versions that are numbered as compatible.
+
+To see how to use this library with your own tree representation,
+see [Kuchiki’s `src/select.rs`](https://github.com/kuchiki-rs/kuchiki/blob/master/src/select.rs).
+(Note however that Kuchiki is not always up to date with the latest rust-selectors version,
+so that code may need to be tweaked.)
+If you don’t already have a tree data structure,
+consider using [Kuchiki](https://github.com/kuchiki-rs/kuchiki) itself.
diff --git a/servo/components/selectors/attr.rs b/servo/components/selectors/attr.rs
new file mode 100644
index 0000000000..fee2962237
--- /dev/null
+++ b/servo/components/selectors/attr.rs
@@ -0,0 +1,183 @@
+/* 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/. */
+
+use crate::parser::SelectorImpl;
+use cssparser::ToCss;
+use std::fmt;
+
+#[derive(Clone, Eq, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+pub struct AttrSelectorWithOptionalNamespace<Impl: SelectorImpl> {
+ #[shmem(field_bound)]
+ pub namespace: Option<NamespaceConstraint<(Impl::NamespacePrefix, Impl::NamespaceUrl)>>,
+ #[shmem(field_bound)]
+ pub local_name: Impl::LocalName,
+ pub local_name_lower: Impl::LocalName,
+ #[shmem(field_bound)]
+ pub operation: ParsedAttrSelectorOperation<Impl::AttrValue>,
+}
+
+impl<Impl: SelectorImpl> AttrSelectorWithOptionalNamespace<Impl> {
+ pub fn namespace(&self) -> Option<NamespaceConstraint<&Impl::NamespaceUrl>> {
+ self.namespace.as_ref().map(|ns| match ns {
+ NamespaceConstraint::Any => NamespaceConstraint::Any,
+ NamespaceConstraint::Specific((_, ref url)) => NamespaceConstraint::Specific(url),
+ })
+ }
+}
+
+#[derive(Clone, Eq, PartialEq, ToShmem)]
+pub enum NamespaceConstraint<NamespaceUrl> {
+ Any,
+
+ /// Empty string for no namespace
+ Specific(NamespaceUrl),
+}
+
+#[derive(Clone, Eq, PartialEq, ToShmem)]
+pub enum ParsedAttrSelectorOperation<AttrValue> {
+ Exists,
+ WithValue {
+ operator: AttrSelectorOperator,
+ case_sensitivity: ParsedCaseSensitivity,
+ value: AttrValue,
+ },
+}
+
+#[derive(Clone, Eq, PartialEq)]
+pub enum AttrSelectorOperation<AttrValue> {
+ Exists,
+ WithValue {
+ operator: AttrSelectorOperator,
+ case_sensitivity: CaseSensitivity,
+ value: AttrValue,
+ },
+}
+
+impl<AttrValue> AttrSelectorOperation<AttrValue> {
+ pub fn eval_str(&self, element_attr_value: &str) -> bool
+ where
+ AttrValue: AsRef<str>,
+ {
+ match *self {
+ AttrSelectorOperation::Exists => true,
+ AttrSelectorOperation::WithValue {
+ operator,
+ case_sensitivity,
+ ref value,
+ } => operator.eval_str(element_attr_value, value.as_ref(), case_sensitivity),
+ }
+ }
+}
+
+#[derive(Clone, Copy, Eq, PartialEq, ToShmem)]
+pub enum AttrSelectorOperator {
+ Equal,
+ Includes,
+ DashMatch,
+ Prefix,
+ Substring,
+ Suffix,
+}
+
+impl ToCss for AttrSelectorOperator {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ // https://drafts.csswg.org/cssom/#serializing-selectors
+ // See "attribute selector".
+ dest.write_str(match *self {
+ AttrSelectorOperator::Equal => "=",
+ AttrSelectorOperator::Includes => "~=",
+ AttrSelectorOperator::DashMatch => "|=",
+ AttrSelectorOperator::Prefix => "^=",
+ AttrSelectorOperator::Substring => "*=",
+ AttrSelectorOperator::Suffix => "$=",
+ })
+ }
+}
+
+impl AttrSelectorOperator {
+ pub fn eval_str(
+ self,
+ element_attr_value: &str,
+ attr_selector_value: &str,
+ case_sensitivity: CaseSensitivity,
+ ) -> bool {
+ let e = element_attr_value.as_bytes();
+ let s = attr_selector_value.as_bytes();
+ let case = case_sensitivity;
+ match self {
+ AttrSelectorOperator::Equal => case.eq(e, s),
+ AttrSelectorOperator::Prefix => e.len() >= s.len() && case.eq(&e[..s.len()], s),
+ AttrSelectorOperator::Suffix => {
+ e.len() >= s.len() && case.eq(&e[(e.len() - s.len())..], s)
+ },
+ AttrSelectorOperator::Substring => {
+ case.contains(element_attr_value, attr_selector_value)
+ },
+ AttrSelectorOperator::Includes => element_attr_value
+ .split(SELECTOR_WHITESPACE)
+ .any(|part| case.eq(part.as_bytes(), s)),
+ AttrSelectorOperator::DashMatch => {
+ case.eq(e, s) || (e.get(s.len()) == Some(&b'-') && case.eq(&e[..s.len()], s))
+ },
+ }
+ }
+}
+
+/// The definition of whitespace per CSS Selectors Level 3 § 4.
+pub static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C'];
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)]
+pub enum ParsedCaseSensitivity {
+ /// 's' was specified.
+ ExplicitCaseSensitive,
+ /// 'i' was specified.
+ AsciiCaseInsensitive,
+ /// No flags were specified and HTML says this is a case-sensitive attribute.
+ CaseSensitive,
+ /// No flags were specified and HTML says this is a case-insensitive attribute.
+ AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum CaseSensitivity {
+ CaseSensitive,
+ AsciiCaseInsensitive,
+}
+
+impl CaseSensitivity {
+ pub fn eq(self, a: &[u8], b: &[u8]) -> bool {
+ match self {
+ CaseSensitivity::CaseSensitive => a == b,
+ CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b),
+ }
+ }
+
+ pub fn contains(self, haystack: &str, needle: &str) -> bool {
+ match self {
+ CaseSensitivity::CaseSensitive => haystack.contains(needle),
+ CaseSensitivity::AsciiCaseInsensitive => {
+ if let Some((&n_first_byte, n_rest)) = needle.as_bytes().split_first() {
+ haystack.bytes().enumerate().any(|(i, byte)| {
+ if !byte.eq_ignore_ascii_case(&n_first_byte) {
+ return false;
+ }
+ let after_this_byte = &haystack.as_bytes()[i + 1..];
+ match after_this_byte.get(..n_rest.len()) {
+ None => false,
+ Some(haystack_slice) => haystack_slice.eq_ignore_ascii_case(n_rest),
+ }
+ })
+ } else {
+ // any_str.contains("") == true,
+ // though these cases should be handled with *NeverMatches and never go here.
+ true
+ }
+ },
+ }
+ }
+}
diff --git a/servo/components/selectors/bloom.rs b/servo/components/selectors/bloom.rs
new file mode 100644
index 0000000000..98461d1ba2
--- /dev/null
+++ b/servo/components/selectors/bloom.rs
@@ -0,0 +1,422 @@
+/* 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/. */
+
+//! Counting and non-counting Bloom filters tuned for use as ancestor filters
+//! for selector matching.
+
+use std::fmt::{self, Debug};
+
+// The top 8 bits of the 32-bit hash value are not used by the bloom filter.
+// Consumers may rely on this to pack hashes more efficiently.
+pub const BLOOM_HASH_MASK: u32 = 0x00ffffff;
+const KEY_SIZE: usize = 12;
+
+const ARRAY_SIZE: usize = 1 << KEY_SIZE;
+const KEY_MASK: u32 = (1 << KEY_SIZE) - 1;
+
+/// A counting Bloom filter with 8-bit counters.
+pub type BloomFilter = CountingBloomFilter<BloomStorageU8>;
+
+/// A counting Bloom filter with parameterized storage to handle
+/// counters of different sizes. For now we assume that having two hash
+/// functions is enough, but we may revisit that decision later.
+///
+/// The filter uses an array with 2**KeySize entries.
+///
+/// Assuming a well-distributed hash function, a Bloom filter with
+/// array size M containing N elements and
+/// using k hash function has expected false positive rate exactly
+///
+/// $ (1 - (1 - 1/M)^{kN})^k $
+///
+/// because each array slot has a
+///
+/// $ (1 - 1/M)^{kN} $
+///
+/// chance of being 0, and the expected false positive rate is the
+/// probability that all of the k hash functions will hit a nonzero
+/// slot.
+///
+/// For reasonable assumptions (M large, kN large, which should both
+/// hold if we're worried about false positives) about M and kN this
+/// becomes approximately
+///
+/// $$ (1 - \exp(-kN/M))^k $$
+///
+/// For our special case of k == 2, that's $(1 - \exp(-2N/M))^2$,
+/// or in other words
+///
+/// $$ N/M = -0.5 * \ln(1 - \sqrt(r)) $$
+///
+/// where r is the false positive rate. This can be used to compute
+/// the desired KeySize for a given load N and false positive rate r.
+///
+/// If N/M is assumed small, then the false positive rate can
+/// further be approximated as 4*N^2/M^2. So increasing KeySize by
+/// 1, which doubles M, reduces the false positive rate by about a
+/// factor of 4, and a false positive rate of 1% corresponds to
+/// about M/N == 20.
+///
+/// What this means in practice is that for a few hundred keys using a
+/// KeySize of 12 gives false positive rates on the order of 0.25-4%.
+///
+/// Similarly, using a KeySize of 10 would lead to a 4% false
+/// positive rate for N == 100 and to quite bad false positive
+/// rates for larger N.
+#[derive(Clone, Default)]
+pub struct CountingBloomFilter<S>
+where
+ S: BloomStorage,
+{
+ storage: S,
+}
+
+impl<S> CountingBloomFilter<S>
+where
+ S: BloomStorage,
+{
+ /// Creates a new bloom filter.
+ #[inline]
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ #[inline]
+ pub fn clear(&mut self) {
+ self.storage = Default::default();
+ }
+
+ // Slow linear accessor to make sure the bloom filter is zeroed. This should
+ // never be used in release builds.
+ #[cfg(debug_assertions)]
+ pub fn is_zeroed(&self) -> bool {
+ self.storage.is_zeroed()
+ }
+
+ #[cfg(not(debug_assertions))]
+ pub fn is_zeroed(&self) -> bool {
+ unreachable!()
+ }
+
+ /// Inserts an item with a particular hash into the bloom filter.
+ #[inline]
+ pub fn insert_hash(&mut self, hash: u32) {
+ self.storage.adjust_first_slot(hash, true);
+ self.storage.adjust_second_slot(hash, true);
+ }
+
+ /// Removes an item with a particular hash from the bloom filter.
+ #[inline]
+ pub fn remove_hash(&mut self, hash: u32) {
+ self.storage.adjust_first_slot(hash, false);
+ self.storage.adjust_second_slot(hash, false);
+ }
+
+ /// Check whether the filter might contain an item with the given hash.
+ /// This can sometimes return true even if the item is not in the filter,
+ /// but will never return false for items that are actually in the filter.
+ #[inline]
+ pub fn might_contain_hash(&self, hash: u32) -> bool {
+ !self.storage.first_slot_is_empty(hash) && !self.storage.second_slot_is_empty(hash)
+ }
+}
+
+impl<S> Debug for CountingBloomFilter<S>
+where
+ S: BloomStorage,
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut slots_used = 0;
+ for i in 0..ARRAY_SIZE {
+ if !self.storage.slot_is_empty(i) {
+ slots_used += 1;
+ }
+ }
+ write!(f, "BloomFilter({}/{})", slots_used, ARRAY_SIZE)
+ }
+}
+
+pub trait BloomStorage: Clone + Default {
+ fn slot_is_empty(&self, index: usize) -> bool;
+ fn adjust_slot(&mut self, index: usize, increment: bool);
+ fn is_zeroed(&self) -> bool;
+
+ #[inline]
+ fn first_slot_is_empty(&self, hash: u32) -> bool {
+ self.slot_is_empty(Self::first_slot_index(hash))
+ }
+
+ #[inline]
+ fn second_slot_is_empty(&self, hash: u32) -> bool {
+ self.slot_is_empty(Self::second_slot_index(hash))
+ }
+
+ #[inline]
+ fn adjust_first_slot(&mut self, hash: u32, increment: bool) {
+ self.adjust_slot(Self::first_slot_index(hash), increment)
+ }
+
+ #[inline]
+ fn adjust_second_slot(&mut self, hash: u32, increment: bool) {
+ self.adjust_slot(Self::second_slot_index(hash), increment)
+ }
+
+ #[inline]
+ fn first_slot_index(hash: u32) -> usize {
+ hash1(hash) as usize
+ }
+
+ #[inline]
+ fn second_slot_index(hash: u32) -> usize {
+ hash2(hash) as usize
+ }
+}
+
+/// Storage class for a CountingBloomFilter that has 8-bit counters.
+pub struct BloomStorageU8 {
+ counters: [u8; ARRAY_SIZE],
+}
+
+impl BloomStorage for BloomStorageU8 {
+ #[inline]
+ fn adjust_slot(&mut self, index: usize, increment: bool) {
+ let slot = &mut self.counters[index];
+ if *slot != 0xff {
+ // full
+ if increment {
+ *slot += 1;
+ } else {
+ *slot -= 1;
+ }
+ }
+ }
+
+ #[inline]
+ fn slot_is_empty(&self, index: usize) -> bool {
+ self.counters[index] == 0
+ }
+
+ #[inline]
+ fn is_zeroed(&self) -> bool {
+ self.counters.iter().all(|x| *x == 0)
+ }
+}
+
+impl Default for BloomStorageU8 {
+ fn default() -> Self {
+ BloomStorageU8 {
+ counters: [0; ARRAY_SIZE],
+ }
+ }
+}
+
+impl Clone for BloomStorageU8 {
+ fn clone(&self) -> Self {
+ BloomStorageU8 {
+ counters: self.counters,
+ }
+ }
+}
+
+/// Storage class for a CountingBloomFilter that has 1-bit counters.
+pub struct BloomStorageBool {
+ counters: [u8; ARRAY_SIZE / 8],
+}
+
+impl BloomStorage for BloomStorageBool {
+ #[inline]
+ fn adjust_slot(&mut self, index: usize, increment: bool) {
+ let bit = 1 << (index % 8);
+ let byte = &mut self.counters[index / 8];
+
+ // Since we have only one bit for storage, decrementing it
+ // should never do anything. Assert against an accidental
+ // decrementing of a bit that was never set.
+ assert!(
+ increment || (*byte & bit) != 0,
+ "should not decrement if slot is already false"
+ );
+
+ if increment {
+ *byte |= bit;
+ }
+ }
+
+ #[inline]
+ fn slot_is_empty(&self, index: usize) -> bool {
+ let bit = 1 << (index % 8);
+ (self.counters[index / 8] & bit) == 0
+ }
+
+ #[inline]
+ fn is_zeroed(&self) -> bool {
+ self.counters.iter().all(|x| *x == 0)
+ }
+}
+
+impl Default for BloomStorageBool {
+ fn default() -> Self {
+ BloomStorageBool {
+ counters: [0; ARRAY_SIZE / 8],
+ }
+ }
+}
+
+impl Clone for BloomStorageBool {
+ fn clone(&self) -> Self {
+ BloomStorageBool {
+ counters: self.counters,
+ }
+ }
+}
+
+#[inline]
+fn hash1(hash: u32) -> u32 {
+ hash & KEY_MASK
+}
+
+#[inline]
+fn hash2(hash: u32) -> u32 {
+ (hash >> KEY_SIZE) & KEY_MASK
+}
+
+#[test]
+fn create_and_insert_some_stuff() {
+ use fxhash::FxHasher;
+ use std::hash::{Hash, Hasher};
+ use std::mem::transmute;
+
+ fn hash_as_str(i: usize) -> u32 {
+ let mut hasher = FxHasher::default();
+ let s = i.to_string();
+ s.hash(&mut hasher);
+ let hash: u64 = hasher.finish();
+ (hash >> 32) as u32 ^ (hash as u32)
+ }
+
+ let mut bf = BloomFilter::new();
+
+ // Statically assert that ARRAY_SIZE is a multiple of 8, which
+ // BloomStorageBool relies on.
+ unsafe {
+ transmute::<[u8; ARRAY_SIZE % 8], [u8; 0]>([]);
+ }
+
+ for i in 0_usize..1000 {
+ bf.insert_hash(hash_as_str(i));
+ }
+
+ for i in 0_usize..1000 {
+ assert!(bf.might_contain_hash(hash_as_str(i)));
+ }
+
+ let false_positives = (1001_usize..2000)
+ .filter(|i| bf.might_contain_hash(hash_as_str(*i)))
+ .count();
+
+ assert!(false_positives < 190, "{} is not < 190", false_positives); // 19%.
+
+ for i in 0_usize..100 {
+ bf.remove_hash(hash_as_str(i));
+ }
+
+ for i in 100_usize..1000 {
+ assert!(bf.might_contain_hash(hash_as_str(i)));
+ }
+
+ let false_positives = (0_usize..100)
+ .filter(|i| bf.might_contain_hash(hash_as_str(*i)))
+ .count();
+
+ assert!(false_positives < 20, "{} is not < 20", false_positives); // 20%.
+
+ bf.clear();
+
+ for i in 0_usize..2000 {
+ assert!(!bf.might_contain_hash(hash_as_str(i)));
+ }
+}
+
+#[cfg(feature = "bench")]
+#[cfg(test)]
+mod bench {
+ extern crate test;
+ use super::BloomFilter;
+
+ #[derive(Default)]
+ struct HashGenerator(u32);
+
+ impl HashGenerator {
+ fn next(&mut self) -> u32 {
+ // Each hash is split into two twelve-bit segments, which are used
+ // as an index into an array. We increment each by 64 so that we
+ // hit the next cache line, and then another 1 so that our wrapping
+ // behavior leads us to different entries each time.
+ //
+ // Trying to simulate cold caches is rather difficult with the cargo
+ // benchmarking setup, so it may all be moot depending on the number
+ // of iterations that end up being run. But we might as well.
+ self.0 += (65) + (65 << super::KEY_SIZE);
+ self.0
+ }
+ }
+
+ #[bench]
+ fn create_insert_1000_remove_100_lookup_100(b: &mut test::Bencher) {
+ b.iter(|| {
+ let mut gen1 = HashGenerator::default();
+ let mut gen2 = HashGenerator::default();
+ let mut bf = BloomFilter::new();
+ for _ in 0_usize..1000 {
+ bf.insert_hash(gen1.next());
+ }
+ for _ in 0_usize..100 {
+ bf.remove_hash(gen2.next());
+ }
+ for _ in 100_usize..200 {
+ test::black_box(bf.might_contain_hash(gen2.next()));
+ }
+ });
+ }
+
+ #[bench]
+ fn might_contain_10(b: &mut test::Bencher) {
+ let bf = BloomFilter::new();
+ let mut gen = HashGenerator::default();
+ b.iter(|| {
+ for _ in 0..10 {
+ test::black_box(bf.might_contain_hash(gen.next()));
+ }
+ });
+ }
+
+ #[bench]
+ fn clear(b: &mut test::Bencher) {
+ let mut bf = Box::new(BloomFilter::new());
+ b.iter(|| test::black_box(&mut bf).clear());
+ }
+
+ #[bench]
+ fn insert_10(b: &mut test::Bencher) {
+ let mut bf = BloomFilter::new();
+ let mut gen = HashGenerator::default();
+ b.iter(|| {
+ for _ in 0..10 {
+ test::black_box(bf.insert_hash(gen.next()));
+ }
+ });
+ }
+
+ #[bench]
+ fn remove_10(b: &mut test::Bencher) {
+ let mut bf = BloomFilter::new();
+ let mut gen = HashGenerator::default();
+ // Note: this will underflow, and that's ok.
+ b.iter(|| {
+ for _ in 0..10 {
+ bf.remove_hash(gen.next())
+ }
+ });
+ }
+}
diff --git a/servo/components/selectors/build.rs b/servo/components/selectors/build.rs
new file mode 100644
index 0000000000..c5c3803991
--- /dev/null
+++ b/servo/components/selectors/build.rs
@@ -0,0 +1,77 @@
+/* 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/. */
+
+extern crate phf_codegen;
+
+use std::env;
+use std::fs::File;
+use std::io::{BufWriter, Write};
+use std::path::Path;
+
+fn main() {
+ let path = Path::new(&env::var_os("OUT_DIR").unwrap())
+ .join("ascii_case_insensitive_html_attributes.rs");
+ let mut file = BufWriter::new(File::create(&path).unwrap());
+
+ let mut set = phf_codegen::Set::new();
+ for name in ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES.split_whitespace() {
+ set.entry(name);
+ }
+ write!(
+ &mut file,
+ "{{ static SET: ::phf::Set<&'static str> = {}; &SET }}",
+ set.build(),
+ )
+ .unwrap();
+}
+
+/// <https://html.spec.whatwg.org/multipage/#selectors>
+static ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES: &str = r#"
+ accept
+ accept-charset
+ align
+ alink
+ axis
+ bgcolor
+ charset
+ checked
+ clear
+ codetype
+ color
+ compact
+ declare
+ defer
+ dir
+ direction
+ disabled
+ enctype
+ face
+ frame
+ hreflang
+ http-equiv
+ lang
+ language
+ link
+ media
+ method
+ multiple
+ nohref
+ noresize
+ noshade
+ nowrap
+ readonly
+ rel
+ rev
+ rules
+ scope
+ scrolling
+ selected
+ shape
+ target
+ text
+ type
+ valign
+ valuetype
+ vlink
+"#;
diff --git a/servo/components/selectors/builder.rs b/servo/components/selectors/builder.rs
new file mode 100644
index 0000000000..2406cd84f6
--- /dev/null
+++ b/servo/components/selectors/builder.rs
@@ -0,0 +1,391 @@
+/* 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/. */
+
+//! Helper module to build up a selector safely and efficiently.
+//!
+//! Our selector representation is designed to optimize matching, and has
+//! several requirements:
+//! * All simple selectors and combinators are stored inline in the same buffer
+//! as Component instances.
+//! * We store the top-level compound selectors from right to left, i.e. in
+//! matching order.
+//! * We store the simple selectors for each combinator from left to right, so
+//! that we match the cheaper simple selectors first.
+//!
+//! Meeting all these constraints without extra memmove traffic during parsing
+//! is non-trivial. This module encapsulates those details and presents an
+//! easy-to-use API for the parser.
+
+use crate::parser::{Combinator, Component, RelativeSelector, Selector, SelectorImpl};
+use crate::sink::Push;
+use servo_arc::{Arc, ThinArc};
+use smallvec::{self, SmallVec};
+use std::cmp;
+use std::iter;
+use std::ptr;
+use std::slice;
+
+/// Top-level SelectorBuilder struct. This should be stack-allocated by the
+/// consumer and never moved (because it contains a lot of inline data that
+/// would be slow to memmov).
+///
+/// After instantation, callers may call the push_simple_selector() and
+/// push_combinator() methods to append selector data as it is encountered
+/// (from left to right). Once the process is complete, callers should invoke
+/// build(), which transforms the contents of the SelectorBuilder into a heap-
+/// allocated Selector and leaves the builder in a drained state.
+#[derive(Debug)]
+pub struct SelectorBuilder<Impl: SelectorImpl> {
+ /// The entire sequence of simple selectors, from left to right, without combinators.
+ ///
+ /// We make this large because the result of parsing a selector is fed into a new
+ /// Arc-ed allocation, so any spilled vec would be a wasted allocation. Also,
+ /// Components are large enough that we don't have much cache locality benefit
+ /// from reserving stack space for fewer of them.
+ simple_selectors: SmallVec<[Component<Impl>; 32]>,
+ /// The combinators, and the length of the compound selector to their left.
+ combinators: SmallVec<[(Combinator, usize); 16]>,
+ /// The length of the current compount selector.
+ current_len: usize,
+}
+
+impl<Impl: SelectorImpl> Default for SelectorBuilder<Impl> {
+ #[inline(always)]
+ fn default() -> Self {
+ SelectorBuilder {
+ simple_selectors: SmallVec::new(),
+ combinators: SmallVec::new(),
+ current_len: 0,
+ }
+ }
+}
+
+impl<Impl: SelectorImpl> Push<Component<Impl>> for SelectorBuilder<Impl> {
+ fn push(&mut self, value: Component<Impl>) {
+ self.push_simple_selector(value);
+ }
+}
+
+impl<Impl: SelectorImpl> SelectorBuilder<Impl> {
+ /// Pushes a simple selector onto the current compound selector.
+ #[inline(always)]
+ pub fn push_simple_selector(&mut self, ss: Component<Impl>) {
+ assert!(!ss.is_combinator());
+ self.simple_selectors.push(ss);
+ self.current_len += 1;
+ }
+
+ /// Completes the current compound selector and starts a new one, delimited
+ /// by the given combinator.
+ #[inline(always)]
+ pub fn push_combinator(&mut self, c: Combinator) {
+ self.combinators.push((c, self.current_len));
+ self.current_len = 0;
+ }
+
+ /// Returns true if combinators have ever been pushed to this builder.
+ #[inline(always)]
+ pub fn has_combinators(&self) -> bool {
+ !self.combinators.is_empty()
+ }
+
+ /// Consumes the builder, producing a Selector.
+ #[inline(always)]
+ pub fn build(&mut self) -> ThinArc<SpecificityAndFlags, Component<Impl>> {
+ // Compute the specificity and flags.
+ let sf = specificity_and_flags(self.simple_selectors.iter());
+ self.build_with_specificity_and_flags(sf)
+ }
+
+ /// Builds with an explicit SpecificityAndFlags. This is separated from build() so
+ /// that unit tests can pass an explicit specificity.
+ #[inline(always)]
+ pub(crate) fn build_with_specificity_and_flags(
+ &mut self,
+ spec: SpecificityAndFlags,
+ ) -> ThinArc<SpecificityAndFlags, Component<Impl>> {
+ // Create the Arc using an iterator that drains our buffers.
+ // Panic-safety: if SelectorBuilderIter is not iterated to the end, some simple selectors
+ // will safely leak.
+ let raw_simple_selectors = unsafe {
+ let simple_selectors_len = self.simple_selectors.len();
+ self.simple_selectors.set_len(0);
+ std::slice::from_raw_parts(self.simple_selectors.as_ptr(), simple_selectors_len)
+ };
+ let (rest, current) = split_from_end(raw_simple_selectors, self.current_len);
+ let iter = SelectorBuilderIter {
+ current_simple_selectors: current.iter(),
+ rest_of_simple_selectors: rest,
+ combinators: self.combinators.drain(..).rev(),
+ };
+
+ Arc::from_header_and_iter(spec, iter)
+ }
+}
+
+struct SelectorBuilderIter<'a, Impl: SelectorImpl> {
+ current_simple_selectors: slice::Iter<'a, Component<Impl>>,
+ rest_of_simple_selectors: &'a [Component<Impl>],
+ combinators: iter::Rev<smallvec::Drain<'a, [(Combinator, usize); 16]>>,
+}
+
+impl<'a, Impl: SelectorImpl> ExactSizeIterator for SelectorBuilderIter<'a, Impl> {
+ fn len(&self) -> usize {
+ self.current_simple_selectors.len() +
+ self.rest_of_simple_selectors.len() +
+ self.combinators.len()
+ }
+}
+
+impl<'a, Impl: SelectorImpl> Iterator for SelectorBuilderIter<'a, Impl> {
+ type Item = Component<Impl>;
+ #[inline(always)]
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some(simple_selector_ref) = self.current_simple_selectors.next() {
+ // Move a simple selector out of this slice iterator.
+ // This is safe because we’ve called SmallVec::set_len(0) above,
+ // so SmallVec::drop won’t drop this simple selector.
+ unsafe { Some(ptr::read(simple_selector_ref)) }
+ } else {
+ self.combinators.next().map(|(combinator, len)| {
+ let (rest, current) = split_from_end(self.rest_of_simple_selectors, len);
+ self.rest_of_simple_selectors = rest;
+ self.current_simple_selectors = current.iter();
+ Component::Combinator(combinator)
+ })
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (self.len(), Some(self.len()))
+ }
+}
+
+fn split_from_end<T>(s: &[T], at: usize) -> (&[T], &[T]) {
+ s.split_at(s.len() - at)
+}
+
+/// Flags that indicate at which point of parsing a selector are we.
+#[derive(Clone, Copy, Default, Eq, PartialEq, ToShmem)]
+pub(crate) struct SelectorFlags(u8);
+
+bitflags! {
+ impl SelectorFlags: u8 {
+ const HAS_PSEUDO = 1 << 0;
+ const HAS_SLOTTED = 1 << 1;
+ const HAS_PART = 1 << 2;
+ const HAS_PARENT = 1 << 3;
+ const HAS_NON_FEATURELESS_COMPONENT = 1 << 4;
+ const HAS_HOST = 1 << 5;
+ }
+}
+
+impl core::fmt::Debug for SelectorFlags {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+ if self.is_empty() {
+ write!(f, "{:#x}", Self::empty().bits())
+ } else {
+ bitflags::parser::to_writer(self, f)
+ }
+ }
+}
+
+impl SelectorFlags {
+ /// When you nest a pseudo-element with something like:
+ ///
+ /// ::before { & { .. } }
+ ///
+ /// It is not supposed to work, because :is(::before) is invalid. We can't propagate the
+ /// pseudo-flags from inner to outer selectors, to avoid breaking our invariants.
+ pub(crate) fn for_nesting() -> Self {
+ Self::all() - (Self::HAS_PSEUDO | Self::HAS_SLOTTED | Self::HAS_PART)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ToShmem)]
+pub struct SpecificityAndFlags {
+ /// There are two free bits here, since we use ten bits for each specificity
+ /// kind (id, class, element).
+ pub(crate) specificity: u32,
+ /// There's padding after this field due to the size of the flags.
+ pub(crate) flags: SelectorFlags,
+}
+
+const MAX_10BIT: u32 = (1u32 << 10) - 1;
+
+#[derive(Add, AddAssign, Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
+pub(crate) struct Specificity {
+ id_selectors: u32,
+ class_like_selectors: u32,
+ element_selectors: u32,
+}
+
+impl From<u32> for Specificity {
+ #[inline]
+ fn from(value: u32) -> Specificity {
+ assert!(value <= MAX_10BIT << 20 | MAX_10BIT << 10 | MAX_10BIT);
+ Specificity {
+ id_selectors: value >> 20,
+ class_like_selectors: (value >> 10) & MAX_10BIT,
+ element_selectors: value & MAX_10BIT,
+ }
+ }
+}
+
+impl From<Specificity> for u32 {
+ #[inline]
+ fn from(specificity: Specificity) -> u32 {
+ cmp::min(specificity.id_selectors, MAX_10BIT) << 20 |
+ cmp::min(specificity.class_like_selectors, MAX_10BIT) << 10 |
+ cmp::min(specificity.element_selectors, MAX_10BIT)
+ }
+}
+
+pub(crate) fn specificity_and_flags<Impl>(iter: slice::Iter<Component<Impl>>) -> SpecificityAndFlags
+where
+ Impl: SelectorImpl,
+{
+ complex_selector_specificity_and_flags(iter).into()
+}
+
+fn complex_selector_specificity_and_flags<Impl>(
+ iter: slice::Iter<Component<Impl>>,
+) -> SpecificityAndFlags
+where
+ Impl: SelectorImpl,
+{
+ fn component_specificity<Impl>(
+ simple_selector: &Component<Impl>,
+ specificity: &mut Specificity,
+ flags: &mut SelectorFlags,
+ ) where
+ Impl: SelectorImpl,
+ {
+ match *simple_selector {
+ Component::Combinator(..) => {},
+ Component::ParentSelector => flags.insert(SelectorFlags::HAS_PARENT),
+ Component::Part(..) => {
+ flags.insert(SelectorFlags::HAS_PART);
+ specificity.element_selectors += 1
+ },
+ Component::PseudoElement(..) => {
+ flags.insert(SelectorFlags::HAS_PSEUDO);
+ specificity.element_selectors += 1
+ },
+ Component::LocalName(..) => {
+ flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
+ specificity.element_selectors += 1
+ },
+ Component::Slotted(ref selector) => {
+ flags.insert(
+ SelectorFlags::HAS_SLOTTED | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ );
+ specificity.element_selectors += 1;
+ // Note that due to the way ::slotted works we only compete with
+ // other ::slotted rules, so the above rule doesn't really
+ // matter, but we do it still for consistency with other
+ // pseudo-elements.
+ //
+ // See: https://github.com/w3c/csswg-drafts/issues/1915
+ *specificity += Specificity::from(selector.specificity());
+ flags.insert(selector.flags());
+ },
+ Component::Host(ref selector) => {
+ flags.insert(SelectorFlags::HAS_HOST);
+ specificity.class_like_selectors += 1;
+ if let Some(ref selector) = *selector {
+ // See: https://github.com/w3c/csswg-drafts/issues/1915
+ *specificity += Specificity::from(selector.specificity());
+ flags.insert(selector.flags() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
+ }
+ },
+ Component::ID(..) => {
+ flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
+ specificity.id_selectors += 1;
+ },
+ Component::Class(..) |
+ Component::AttributeInNoNamespace { .. } |
+ Component::AttributeInNoNamespaceExists { .. } |
+ Component::AttributeOther(..) |
+ Component::Root |
+ Component::Empty |
+ Component::Scope |
+ Component::Nth(..) |
+ Component::NonTSPseudoClass(..) => {
+ flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
+ specificity.class_like_selectors += 1;
+ },
+ Component::NthOf(ref nth_of_data) => {
+ // https://drafts.csswg.org/selectors/#specificity-rules:
+ //
+ // The specificity of the :nth-last-child() pseudo-class,
+ // like the :nth-child() pseudo-class, combines the
+ // specificity of a regular pseudo-class with that of its
+ // selector argument S.
+ specificity.class_like_selectors += 1;
+ let sf = selector_list_specificity_and_flags(nth_of_data.selectors().iter());
+ *specificity += Specificity::from(sf.specificity);
+ flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
+ },
+ // https://drafts.csswg.org/selectors/#specificity-rules:
+ //
+ // The specificity of an :is(), :not(), or :has() pseudo-class
+ // is replaced by the specificity of the most specific complex
+ // selector in its selector list argument.
+ Component::Where(ref list) |
+ Component::Negation(ref list) |
+ Component::Is(ref list) => {
+ let sf = selector_list_specificity_and_flags(list.slice().iter());
+ if !matches!(*simple_selector, Component::Where(..)) {
+ *specificity += Specificity::from(sf.specificity);
+ }
+ flags.insert(sf.flags);
+ },
+ Component::Has(ref relative_selectors) => {
+ let sf = relative_selector_list_specificity_and_flags(relative_selectors);
+ *specificity += Specificity::from(sf.specificity);
+ flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
+ },
+ Component::ExplicitUniversalType |
+ Component::ExplicitAnyNamespace |
+ Component::ExplicitNoNamespace |
+ Component::DefaultNamespace(..) |
+ Component::Namespace(..) |
+ Component::RelativeSelectorAnchor |
+ Component::Invalid(..) => {
+ // Does not affect specificity
+ flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT);
+ },
+ }
+ }
+
+ let mut specificity = Default::default();
+ let mut flags = Default::default();
+ for simple_selector in iter {
+ component_specificity(&simple_selector, &mut specificity, &mut flags);
+ }
+ SpecificityAndFlags {
+ specificity: specificity.into(),
+ flags,
+ }
+}
+
+/// Finds the maximum specificity of elements in the list and returns it.
+pub(crate) fn selector_list_specificity_and_flags<'a, Impl: SelectorImpl>(
+ itr: impl Iterator<Item = &'a Selector<Impl>>,
+) -> SpecificityAndFlags {
+ let mut specificity = 0;
+ let mut flags = SelectorFlags::empty();
+ for selector in itr {
+ specificity = std::cmp::max(specificity, selector.specificity());
+ flags.insert(selector.flags());
+ }
+ SpecificityAndFlags { specificity, flags }
+}
+
+pub(crate) fn relative_selector_list_specificity_and_flags<Impl: SelectorImpl>(
+ list: &[RelativeSelector<Impl>],
+) -> SpecificityAndFlags {
+ selector_list_specificity_and_flags(list.iter().map(|rel| &rel.selector))
+}
diff --git a/servo/components/selectors/context.rs b/servo/components/selectors/context.rs
new file mode 100644
index 0000000000..84ee262dfe
--- /dev/null
+++ b/servo/components/selectors/context.rs
@@ -0,0 +1,418 @@
+/* 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/. */
+
+use crate::attr::CaseSensitivity;
+use crate::bloom::BloomFilter;
+use crate::nth_index_cache::{NthIndexCache, NthIndexCacheInner};
+use crate::parser::{Selector, SelectorImpl};
+use crate::relative_selector::cache::RelativeSelectorCache;
+use crate::relative_selector::filter::RelativeSelectorFilterMap;
+use crate::tree::{Element, OpaqueElement};
+
+/// What kind of selector matching mode we should use.
+///
+/// There are two modes of selector matching. The difference is only noticeable
+/// in presence of pseudo-elements.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum MatchingMode {
+ /// Don't ignore any pseudo-element selectors.
+ Normal,
+
+ /// Ignores any stateless pseudo-element selectors in the rightmost sequence
+ /// of simple selectors.
+ ///
+ /// This is useful, for example, to match against ::before when you aren't a
+ /// pseudo-element yourself.
+ ///
+ /// For example, in presence of `::before:hover`, it would never match, but
+ /// `::before` would be ignored as in "matching".
+ ///
+ /// It's required for all the selectors you match using this mode to have a
+ /// pseudo-element.
+ ForStatelessPseudoElement,
+}
+
+/// The mode to use when matching unvisited and visited links.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum VisitedHandlingMode {
+ /// All links are matched as if they are unvisted.
+ AllLinksUnvisited,
+ /// All links are matched as if they are visited and unvisited (both :link
+ /// and :visited match).
+ ///
+ /// This is intended to be used from invalidation code, to be conservative
+ /// about whether we need to restyle a link.
+ AllLinksVisitedAndUnvisited,
+ /// A element's "relevant link" is the element being matched if it is a link
+ /// or the nearest ancestor link. The relevant link is matched as though it
+ /// is visited, and all other links are matched as if they are unvisited.
+ RelevantLinkVisited,
+}
+
+impl VisitedHandlingMode {
+ #[inline]
+ pub fn matches_visited(&self) -> bool {
+ matches!(
+ *self,
+ VisitedHandlingMode::RelevantLinkVisited |
+ VisitedHandlingMode::AllLinksVisitedAndUnvisited
+ )
+ }
+
+ #[inline]
+ pub fn matches_unvisited(&self) -> bool {
+ matches!(
+ *self,
+ VisitedHandlingMode::AllLinksUnvisited |
+ VisitedHandlingMode::AllLinksVisitedAndUnvisited
+ )
+ }
+}
+
+/// Whether we need to set selector invalidation flags on elements for this
+/// match request.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum NeedsSelectorFlags {
+ No,
+ Yes,
+}
+
+/// Whether we're matching in the contect of invalidation.
+#[derive(PartialEq)]
+pub enum MatchingForInvalidation {
+ No,
+ Yes,
+}
+
+/// Which quirks mode is this document in.
+///
+/// See: https://quirks.spec.whatwg.org/
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub enum QuirksMode {
+ /// Quirks mode.
+ Quirks,
+ /// Limited quirks mode.
+ LimitedQuirks,
+ /// No quirks mode.
+ NoQuirks,
+}
+
+impl QuirksMode {
+ #[inline]
+ pub fn classes_and_ids_case_sensitivity(self) -> CaseSensitivity {
+ match self {
+ QuirksMode::NoQuirks | QuirksMode::LimitedQuirks => CaseSensitivity::CaseSensitive,
+ QuirksMode::Quirks => CaseSensitivity::AsciiCaseInsensitive,
+ }
+ }
+}
+
+/// Whether or not this matching considered relative selector.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum RelativeSelectorMatchingState {
+ /// Was not considered for any relative selector.
+ None,
+ /// Relative selector was considered for a match, but the element to be
+ /// under matching would not anchor the relative selector. i.e. The
+ /// relative selector was not part of the first compound selector (in match
+ /// order).
+ Considered,
+ /// Same as above, but the relative selector was part of the first compound
+ /// selector (in match order).
+ ConsideredAnchor,
+}
+
+impl RelativeSelectorMatchingState {
+ /// Update the matching state to indicate that the relative selector matching
+ /// happened in the subject position.
+ pub fn considered_anchor(&mut self) {
+ *self = Self::ConsideredAnchor;
+ }
+
+ /// Update the matching state to indicate that the relative selector matching
+ /// happened in a non-subject position.
+ pub fn considered(&mut self) {
+ // Being considered an anchor is stronger (e.g. `:has(.a):is(:has(.b) .c)`).
+ if *self == Self::ConsideredAnchor {
+ *self = Self::ConsideredAnchor;
+ } else {
+ *self = Self::Considered;
+ }
+ }
+}
+
+/// Set of caches (And cache-likes) that speed up expensive selector matches.
+#[derive(Default)]
+pub struct SelectorCaches {
+ /// A cache to speed up nth-index-like selectors.
+ pub nth_index: NthIndexCache,
+ /// A cache to speed up relative selector matches. See module documentation.
+ pub relative_selector: RelativeSelectorCache,
+ /// A map of bloom filters to fast-reject relative selector matches.
+ pub relative_selector_filter_map: RelativeSelectorFilterMap,
+}
+
+/// Data associated with the matching process for a element. This context is
+/// used across many selectors for an element, so it's not appropriate for
+/// transient data that applies to only a single selector.
+pub struct MatchingContext<'a, Impl>
+where
+ Impl: SelectorImpl,
+{
+ /// Input with the matching mode we should use when matching selectors.
+ matching_mode: MatchingMode,
+ /// Input with the bloom filter used to fast-reject selectors.
+ pub bloom_filter: Option<&'a BloomFilter>,
+ /// The element which is going to match :scope pseudo-class. It can be
+ /// either one :scope element, or the scoping element.
+ ///
+ /// Note that, although in theory there can be multiple :scope elements,
+ /// in current specs, at most one is specified, and when there is one,
+ /// scoping element is not relevant anymore, so we use a single field for
+ /// them.
+ ///
+ /// When this is None, :scope will match the root element.
+ ///
+ /// See https://drafts.csswg.org/selectors-4/#scope-pseudo
+ pub scope_element: Option<OpaqueElement>,
+
+ /// The current shadow host we're collecting :host rules for.
+ pub current_host: Option<OpaqueElement>,
+
+ /// Controls how matching for links is handled.
+ visited_handling: VisitedHandlingMode,
+
+ /// The current nesting level of selectors that we're matching.
+ nesting_level: usize,
+
+ /// Whether we're inside a negation or not.
+ in_negation: bool,
+
+ /// An optional hook function for checking whether a pseudo-element
+ /// should match when matching_mode is ForStatelessPseudoElement.
+ pub pseudo_element_matching_fn: Option<&'a dyn Fn(&Impl::PseudoElement) -> bool>,
+
+ /// Extra implementation-dependent matching data.
+ pub extra_data: Impl::ExtraMatchingData<'a>,
+
+ /// The current element we're anchoring on for evaluating the relative selector.
+ current_relative_selector_anchor: Option<OpaqueElement>,
+ pub considered_relative_selector: RelativeSelectorMatchingState,
+
+ quirks_mode: QuirksMode,
+ needs_selector_flags: NeedsSelectorFlags,
+
+ /// Whether we're matching in the contect of invalidation.
+ matching_for_invalidation: MatchingForInvalidation,
+
+ /// Caches to speed up expensive selector matches.
+ pub selector_caches: &'a mut SelectorCaches,
+
+ classes_and_ids_case_sensitivity: CaseSensitivity,
+ _impl: ::std::marker::PhantomData<Impl>,
+}
+
+impl<'a, Impl> MatchingContext<'a, Impl>
+where
+ Impl: SelectorImpl,
+{
+ /// Constructs a new `MatchingContext`.
+ pub fn new(
+ matching_mode: MatchingMode,
+ bloom_filter: Option<&'a BloomFilter>,
+ selector_caches: &'a mut SelectorCaches,
+ quirks_mode: QuirksMode,
+ needs_selector_flags: NeedsSelectorFlags,
+ matching_for_invalidation: MatchingForInvalidation,
+ ) -> Self {
+ Self::new_for_visited(
+ matching_mode,
+ bloom_filter,
+ selector_caches,
+ VisitedHandlingMode::AllLinksUnvisited,
+ quirks_mode,
+ needs_selector_flags,
+ matching_for_invalidation,
+ )
+ }
+
+ /// Constructs a new `MatchingContext` for use in visited matching.
+ pub fn new_for_visited(
+ matching_mode: MatchingMode,
+ bloom_filter: Option<&'a BloomFilter>,
+ selector_caches: &'a mut SelectorCaches,
+ visited_handling: VisitedHandlingMode,
+ quirks_mode: QuirksMode,
+ needs_selector_flags: NeedsSelectorFlags,
+ matching_for_invalidation: MatchingForInvalidation,
+ ) -> Self {
+ Self {
+ matching_mode,
+ bloom_filter,
+ visited_handling,
+ quirks_mode,
+ classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(),
+ needs_selector_flags,
+ matching_for_invalidation,
+ scope_element: None,
+ current_host: None,
+ nesting_level: 0,
+ in_negation: false,
+ pseudo_element_matching_fn: None,
+ extra_data: Default::default(),
+ current_relative_selector_anchor: None,
+ considered_relative_selector: RelativeSelectorMatchingState::None,
+ selector_caches,
+ _impl: ::std::marker::PhantomData,
+ }
+ }
+
+ // Grab a reference to the appropriate cache.
+ #[inline]
+ pub fn nth_index_cache(
+ &mut self,
+ is_of_type: bool,
+ is_from_end: bool,
+ selectors: &[Selector<Impl>],
+ ) -> &mut NthIndexCacheInner {
+ self.selector_caches
+ .nth_index
+ .get(is_of_type, is_from_end, selectors)
+ }
+
+ /// Whether we're matching a nested selector.
+ #[inline]
+ pub fn is_nested(&self) -> bool {
+ self.nesting_level != 0
+ }
+
+ /// Whether we're matching inside a :not(..) selector.
+ #[inline]
+ pub fn in_negation(&self) -> bool {
+ self.in_negation
+ }
+
+ /// The quirks mode of the document.
+ #[inline]
+ pub fn quirks_mode(&self) -> QuirksMode {
+ self.quirks_mode
+ }
+
+ /// The matching-mode for this selector-matching operation.
+ #[inline]
+ pub fn matching_mode(&self) -> MatchingMode {
+ self.matching_mode
+ }
+
+ /// Whether we need to set selector flags.
+ #[inline]
+ pub fn needs_selector_flags(&self) -> bool {
+ self.needs_selector_flags == NeedsSelectorFlags::Yes
+ }
+
+ /// Whether or not we're matching to invalidate.
+ #[inline]
+ pub fn matching_for_invalidation(&self) -> bool {
+ self.matching_for_invalidation == MatchingForInvalidation::Yes
+ }
+
+ /// The case-sensitivity for class and ID selectors
+ #[inline]
+ pub fn classes_and_ids_case_sensitivity(&self) -> CaseSensitivity {
+ self.classes_and_ids_case_sensitivity
+ }
+
+ /// Runs F with a deeper nesting level.
+ #[inline]
+ pub fn nest<F, R>(&mut self, f: F) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ self.nesting_level += 1;
+ let result = f(self);
+ self.nesting_level -= 1;
+ result
+ }
+
+ /// Runs F with a deeper nesting level, and marking ourselves in a negation,
+ /// for a :not(..) selector, for example.
+ #[inline]
+ pub fn nest_for_negation<F, R>(&mut self, f: F) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ let old_in_negation = self.in_negation;
+ self.in_negation = true;
+ let result = self.nest(f);
+ self.in_negation = old_in_negation;
+ result
+ }
+
+ #[inline]
+ pub fn visited_handling(&self) -> VisitedHandlingMode {
+ self.visited_handling
+ }
+
+ /// Runs F with a different VisitedHandlingMode.
+ #[inline]
+ pub fn with_visited_handling_mode<F, R>(
+ &mut self,
+ handling_mode: VisitedHandlingMode,
+ f: F,
+ ) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ let original_handling_mode = self.visited_handling;
+ self.visited_handling = handling_mode;
+ let result = f(self);
+ self.visited_handling = original_handling_mode;
+ result
+ }
+
+ /// Runs F with a given shadow host which is the root of the tree whose
+ /// rules we're matching.
+ #[inline]
+ pub fn with_shadow_host<F, E, R>(&mut self, host: Option<E>, f: F) -> R
+ where
+ E: Element,
+ F: FnOnce(&mut Self) -> R,
+ {
+ let original_host = self.current_host.take();
+ self.current_host = host.map(|h| h.opaque());
+ let result = f(self);
+ self.current_host = original_host;
+ result
+ }
+
+ /// Returns the current shadow host whose shadow root we're matching rules
+ /// against.
+ #[inline]
+ pub fn shadow_host(&self) -> Option<OpaqueElement> {
+ self.current_host
+ }
+
+ /// Runs F with a deeper nesting level, with the given element as the anchor,
+ /// for a :has(...) selector, for example.
+ #[inline]
+ pub fn nest_for_relative_selector<F, R>(&mut self, anchor: OpaqueElement, f: F) -> R
+ where
+ F: FnOnce(&mut Self) -> R,
+ {
+ debug_assert!(
+ self.current_relative_selector_anchor.is_none(),
+ "Nesting should've been rejected at parse time"
+ );
+ self.current_relative_selector_anchor = Some(anchor);
+ let result = self.nest(f);
+ self.current_relative_selector_anchor = None;
+ result
+ }
+
+ /// Returns the current anchor element to evaluate the relative selector against.
+ #[inline]
+ pub fn relative_selector_anchor(&self) -> Option<OpaqueElement> {
+ self.current_relative_selector_anchor
+ }
+}
diff --git a/servo/components/selectors/lib.rs b/servo/components/selectors/lib.rs
new file mode 100644
index 0000000000..d909059ccf
--- /dev/null
+++ b/servo/components/selectors/lib.rs
@@ -0,0 +1,41 @@
+/* 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/. */
+
+// Make |cargo bench| work.
+#![cfg_attr(feature = "bench", feature(test))]
+
+#[macro_use]
+extern crate bitflags;
+#[macro_use]
+extern crate cssparser;
+#[macro_use]
+extern crate debug_unreachable;
+#[macro_use]
+extern crate derive_more;
+extern crate fxhash;
+#[macro_use]
+extern crate log;
+extern crate phf;
+extern crate precomputed_hash;
+extern crate servo_arc;
+extern crate smallvec;
+extern crate to_shmem;
+#[macro_use]
+extern crate to_shmem_derive;
+
+pub mod attr;
+pub mod bloom;
+mod builder;
+pub mod context;
+pub mod matching;
+mod nth_index_cache;
+pub mod parser;
+pub mod relative_selector;
+pub mod sink;
+mod tree;
+pub mod visitor;
+
+pub use crate::nth_index_cache::NthIndexCache;
+pub use crate::parser::{Parser, SelectorImpl, SelectorList};
+pub use crate::tree::{Element, OpaqueElement};
diff --git a/servo/components/selectors/matching.rs b/servo/components/selectors/matching.rs
new file mode 100644
index 0000000000..763f65d547
--- /dev/null
+++ b/servo/components/selectors/matching.rs
@@ -0,0 +1,1370 @@
+/* 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/. */
+
+use crate::attr::{
+ AttrSelectorOperation, AttrSelectorWithOptionalNamespace, CaseSensitivity, NamespaceConstraint,
+ ParsedAttrSelectorOperation, ParsedCaseSensitivity,
+};
+use crate::bloom::{BloomFilter, BLOOM_HASH_MASK};
+use crate::parser::{
+ AncestorHashes, Combinator, Component, LocalName, NthSelectorData, RelativeSelectorMatchHint,
+};
+use crate::parser::{
+ NonTSPseudoClass, RelativeSelector, Selector, SelectorImpl, SelectorIter, SelectorList,
+};
+use crate::relative_selector::cache::RelativeSelectorCachedMatch;
+use crate::tree::Element;
+use smallvec::SmallVec;
+use std::borrow::Borrow;
+
+pub use crate::context::*;
+
+// The bloom filter for descendant CSS selectors will have a <1% false
+// positive rate until it has this many selectors in it, then it will
+// rapidly increase.
+pub static RECOMMENDED_SELECTOR_BLOOM_FILTER_SIZE: usize = 4096;
+
+bitflags! {
+ /// Set of flags that are set on either the element or its parent (depending
+ /// on the flag) if the element could potentially match a selector.
+ #[derive(Clone, Copy)]
+ pub struct ElementSelectorFlags: usize {
+ /// When a child is added or removed from the parent, all the children
+ /// must be restyled, because they may match :nth-last-child,
+ /// :last-of-type, :nth-last-of-type, or :only-of-type.
+ const HAS_SLOW_SELECTOR = 1 << 0;
+
+ /// When a child is added or removed from the parent, any later
+ /// children must be restyled, because they may match :nth-child,
+ /// :first-of-type, or :nth-of-type.
+ const HAS_SLOW_SELECTOR_LATER_SIBLINGS = 1 << 1;
+
+ /// HAS_SLOW_SELECTOR* was set by the presence of :nth (But not of).
+ const HAS_SLOW_SELECTOR_NTH = 1 << 2;
+
+ /// When a DOM mutation occurs on a child that might be matched by
+ /// :nth-last-child(.. of <selector list>), earlier children must be
+ /// restyled, and HAS_SLOW_SELECTOR will be set (which normally
+ /// indicates that all children will be restyled).
+ ///
+ /// Similarly, when a DOM mutation occurs on a child that might be
+ /// matched by :nth-child(.. of <selector list>), later children must be
+ /// restyled, and HAS_SLOW_SELECTOR_LATER_SIBLINGS will be set.
+ const HAS_SLOW_SELECTOR_NTH_OF = 1 << 3;
+
+ /// When a child is added or removed from the parent, the first and
+ /// last children must be restyled, because they may match :first-child,
+ /// :last-child, or :only-child.
+ const HAS_EDGE_CHILD_SELECTOR = 1 << 4;
+
+ /// The element has an empty selector, so when a child is appended we
+ /// might need to restyle the parent completely.
+ const HAS_EMPTY_SELECTOR = 1 << 5;
+
+ /// The element may anchor a relative selector.
+ const ANCHORS_RELATIVE_SELECTOR = 1 << 6;
+
+ /// The element may anchor a relative selector that is not the subject
+ /// of the whole selector.
+ const ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT = 1 << 7;
+
+ /// The element is reached by a relative selector search in the sibling direction.
+ const RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING = 1 << 8;
+
+ /// The element is reached by a relative selector search in the ancestor direction.
+ const RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR = 1 << 9;
+
+ // The element is reached by a relative selector search in both sibling and ancestor directions.
+ const RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING =
+ Self::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING.bits() |
+ Self::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR.bits();
+ }
+}
+
+impl ElementSelectorFlags {
+ /// Returns the subset of flags that apply to the element.
+ pub fn for_self(self) -> ElementSelectorFlags {
+ self & (ElementSelectorFlags::HAS_EMPTY_SELECTOR |
+ ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR |
+ ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT |
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING |
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR)
+ }
+
+ /// Returns the subset of flags that apply to the parent.
+ pub fn for_parent(self) -> ElementSelectorFlags {
+ self & (ElementSelectorFlags::HAS_SLOW_SELECTOR |
+ ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS |
+ ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH |
+ ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF |
+ ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR)
+ }
+}
+
+/// Holds per-compound-selector data.
+struct LocalMatchingContext<'a, 'b: 'a, Impl: SelectorImpl> {
+ shared: &'a mut MatchingContext<'b, Impl>,
+ rightmost: SubjectOrPseudoElement,
+ quirks_data: Option<SelectorIter<'a, Impl>>,
+}
+
+#[inline(always)]
+pub fn matches_selector_list<E>(
+ selector_list: &SelectorList<E::Impl>,
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+) -> bool
+where
+ E: Element,
+{
+ // This is pretty much any(..) but manually inlined because the compiler
+ // refuses to do so from querySelector / querySelectorAll.
+ for selector in selector_list.slice() {
+ let matches = matches_selector(selector, 0, None, element, context);
+ if matches {
+ return true;
+ }
+ }
+
+ false
+}
+
+#[inline(always)]
+fn may_match(hashes: &AncestorHashes, bf: &BloomFilter) -> bool {
+ // Check the first three hashes. Note that we can check for zero before
+ // masking off the high bits, since if any of the first three hashes is
+ // zero the fourth will be as well. We also take care to avoid the
+ // special-case complexity of the fourth hash until we actually reach it,
+ // because we usually don't.
+ //
+ // To be clear: this is all extremely hot.
+ for i in 0..3 {
+ let packed = hashes.packed_hashes[i];
+ if packed == 0 {
+ // No more hashes left - unable to fast-reject.
+ return true;
+ }
+
+ if !bf.might_contain_hash(packed & BLOOM_HASH_MASK) {
+ // Hooray! We fast-rejected on this hash.
+ return false;
+ }
+ }
+
+ // Now do the slighty-more-complex work of synthesizing the fourth hash,
+ // and check it against the filter if it exists.
+ let fourth = hashes.fourth_hash();
+ fourth == 0 || bf.might_contain_hash(fourth)
+}
+
+/// A result of selector matching, includes 3 failure types,
+///
+/// NotMatchedAndRestartFromClosestLaterSibling
+/// NotMatchedAndRestartFromClosestDescendant
+/// NotMatchedGlobally
+///
+/// When NotMatchedGlobally appears, stop selector matching completely since
+/// the succeeding selectors never matches.
+/// It is raised when
+/// Child combinator cannot find the candidate element.
+/// Descendant combinator cannot find the candidate element.
+///
+/// When NotMatchedAndRestartFromClosestDescendant appears, the selector
+/// matching does backtracking and restarts from the closest Descendant
+/// combinator.
+/// It is raised when
+/// NextSibling combinator cannot find the candidate element.
+/// LaterSibling combinator cannot find the candidate element.
+/// Child combinator doesn't match on the found element.
+///
+/// When NotMatchedAndRestartFromClosestLaterSibling appears, the selector
+/// matching does backtracking and restarts from the closest LaterSibling
+/// combinator.
+/// It is raised when
+/// NextSibling combinator doesn't match on the found element.
+///
+/// For example, when the selector "d1 d2 a" is provided and we cannot *find*
+/// an appropriate ancestor element for "d1", this selector matching raises
+/// NotMatchedGlobally since even if "d2" is moved to more upper element, the
+/// candidates for "d1" becomes less than before and d1 .
+///
+/// The next example is siblings. When the selector "b1 + b2 ~ d1 a" is
+/// provided and we cannot *find* an appropriate brother element for b1,
+/// the selector matching raises NotMatchedAndRestartFromClosestDescendant.
+/// The selectors ("b1 + b2 ~") doesn't match and matching restart from "d1".
+///
+/// The additional example is child and sibling. When the selector
+/// "b1 + c1 > b2 ~ d1 a" is provided and the selector "b1" doesn't match on
+/// the element, this "b1" raises NotMatchedAndRestartFromClosestLaterSibling.
+/// However since the selector "c1" raises
+/// NotMatchedAndRestartFromClosestDescendant. So the selector
+/// "b1 + c1 > b2 ~ " doesn't match and restart matching from "d1".
+#[derive(Clone, Copy, Eq, PartialEq)]
+enum SelectorMatchingResult {
+ Matched,
+ NotMatchedAndRestartFromClosestLaterSibling,
+ NotMatchedAndRestartFromClosestDescendant,
+ NotMatchedGlobally,
+}
+
+/// Matches a selector, fast-rejecting against a bloom filter.
+///
+/// We accept an offset to allow consumers to represent and match against
+/// partial selectors (indexed from the right). We use this API design, rather
+/// than having the callers pass a SelectorIter, because creating a SelectorIter
+/// requires dereferencing the selector to get the length, which adds an
+/// unncessary cache miss for cases when we can fast-reject with AncestorHashes
+/// (which the caller can store inline with the selector pointer).
+#[inline(always)]
+pub fn matches_selector<E>(
+ selector: &Selector<E::Impl>,
+ offset: usize,
+ hashes: Option<&AncestorHashes>,
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+) -> bool
+where
+ E: Element,
+{
+ // Use the bloom filter to fast-reject.
+ if let Some(hashes) = hashes {
+ if let Some(filter) = context.bloom_filter {
+ if !may_match(hashes, filter) {
+ return false;
+ }
+ }
+ }
+ matches_complex_selector(
+ selector.iter_from(offset),
+ element,
+ context,
+ if selector.is_rightmost(offset) {
+ SubjectOrPseudoElement::Yes
+ } else {
+ SubjectOrPseudoElement::No
+ },
+ )
+}
+
+/// Whether a compound selector matched, and whether it was the rightmost
+/// selector inside the complex selector.
+pub enum CompoundSelectorMatchingResult {
+ /// The selector was fully matched.
+ FullyMatched,
+ /// The compound selector matched, and the next combinator offset is
+ /// `next_combinator_offset`.
+ Matched { next_combinator_offset: usize },
+ /// The selector didn't match.
+ NotMatched,
+}
+
+/// Matches a compound selector belonging to `selector`, starting at offset
+/// `from_offset`, matching left to right.
+///
+/// Requires that `from_offset` points to a `Combinator`.
+///
+/// NOTE(emilio): This doesn't allow to match in the leftmost sequence of the
+/// complex selector, but it happens to be the case we don't need it.
+pub fn matches_compound_selector_from<E>(
+ selector: &Selector<E::Impl>,
+ mut from_offset: usize,
+ context: &mut MatchingContext<E::Impl>,
+ element: &E,
+) -> CompoundSelectorMatchingResult
+where
+ E: Element,
+{
+ if cfg!(debug_assertions) && from_offset != 0 {
+ selector.combinator_at_parse_order(from_offset - 1); // This asserts.
+ }
+
+ let mut local_context = LocalMatchingContext {
+ shared: context,
+ // We have no info if this is an outer selector. This function is called in
+ // an invalidation context, which only calls this for non-subject (i.e.
+ // Non-rightmost) positions.
+ rightmost: SubjectOrPseudoElement::No,
+ quirks_data: None,
+ };
+
+ // Find the end of the selector or the next combinator, then match
+ // backwards, so that we match in the same order as
+ // matches_complex_selector, which is usually faster.
+ let start_offset = from_offset;
+ for component in selector.iter_raw_parse_order_from(from_offset) {
+ if matches!(*component, Component::Combinator(..)) {
+ debug_assert_ne!(from_offset, 0, "Selector started with a combinator?");
+ break;
+ }
+
+ from_offset += 1;
+ }
+
+ debug_assert!(from_offset >= 1);
+ debug_assert!(from_offset <= selector.len());
+
+ let iter = selector.iter_from(selector.len() - from_offset);
+ debug_assert!(
+ iter.clone().next().is_some() ||
+ (from_offset != selector.len() &&
+ matches!(
+ selector.combinator_at_parse_order(from_offset),
+ Combinator::SlotAssignment | Combinator::PseudoElement
+ )),
+ "Got the math wrong: {:?} | {:?} | {} {}",
+ selector,
+ selector.iter_raw_match_order().as_slice(),
+ from_offset,
+ start_offset
+ );
+
+ for component in iter {
+ if !matches_simple_selector(component, element, &mut local_context) {
+ return CompoundSelectorMatchingResult::NotMatched;
+ }
+ }
+
+ if from_offset != selector.len() {
+ return CompoundSelectorMatchingResult::Matched {
+ next_combinator_offset: from_offset,
+ };
+ }
+
+ CompoundSelectorMatchingResult::FullyMatched
+}
+
+/// Matches a complex selector.
+#[inline(always)]
+fn matches_complex_selector<E>(
+ mut iter: SelectorIter<E::Impl>,
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool
+where
+ E: Element,
+{
+ // If this is the special pseudo-element mode, consume the ::pseudo-element
+ // before proceeding, since the caller has already handled that part.
+ if context.matching_mode() == MatchingMode::ForStatelessPseudoElement && !context.is_nested() {
+ // Consume the pseudo.
+ match *iter.next().unwrap() {
+ Component::PseudoElement(ref pseudo) => {
+ if let Some(ref f) = context.pseudo_element_matching_fn {
+ if !f(pseudo) {
+ return false;
+ }
+ }
+ },
+ ref other => {
+ debug_assert!(
+ false,
+ "Used MatchingMode::ForStatelessPseudoElement \
+ in a non-pseudo selector {:?}",
+ other
+ );
+ return false;
+ },
+ }
+
+ if !iter.matches_for_stateless_pseudo_element() {
+ return false;
+ }
+
+ // Advance to the non-pseudo-element part of the selector.
+ let next_sequence = iter.next_sequence().unwrap();
+ debug_assert_eq!(next_sequence, Combinator::PseudoElement);
+ }
+
+ let result = matches_complex_selector_internal(iter, element, context, rightmost);
+
+ matches!(result, SelectorMatchingResult::Matched)
+}
+
+/// Matches each selector of a list as a complex selector
+fn matches_complex_selector_list<E: Element>(
+ list: &[Selector<E::Impl>],
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool {
+ for selector in list {
+ if matches_complex_selector(selector.iter(), element, context, rightmost) {
+ return true;
+ }
+ }
+ false
+}
+
+fn matches_relative_selector<E: Element>(
+ relative_selector: &RelativeSelector<E::Impl>,
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool {
+ // Overall, we want to mark the path that we've traversed so that when an element
+ // is invalidated, we early-reject unnecessary relative selector invalidations.
+ if relative_selector.match_hint.is_descendant_direction() {
+ if context.needs_selector_flags() {
+ element.apply_selector_flags(
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR,
+ );
+ }
+ let mut next_element = element.first_element_child();
+ while let Some(el) = next_element {
+ if context.needs_selector_flags() {
+ el.apply_selector_flags(
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR,
+ );
+ }
+ let mut matched = matches_complex_selector(
+ relative_selector.selector.iter(),
+ &el,
+ context,
+ rightmost,
+ );
+ if !matched && relative_selector.match_hint.is_subtree() {
+ matched = matches_relative_selector_subtree(
+ &relative_selector.selector,
+ &el,
+ context,
+ rightmost,
+ );
+ }
+ if matched {
+ return true;
+ }
+ next_element = el.next_sibling_element();
+ }
+ } else {
+ debug_assert!(
+ matches!(
+ relative_selector.match_hint,
+ RelativeSelectorMatchHint::InNextSibling |
+ RelativeSelectorMatchHint::InNextSiblingSubtree |
+ RelativeSelectorMatchHint::InSibling |
+ RelativeSelectorMatchHint::InSiblingSubtree
+ ),
+ "Not descendant direction, but also not sibling direction?"
+ );
+ if context.needs_selector_flags() {
+ element.apply_selector_flags(
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING,
+ );
+ }
+ let sibling_flag = if relative_selector.match_hint.is_subtree() {
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING
+ } else {
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING
+ };
+ let mut next_element = element.next_sibling_element();
+ while let Some(el) = next_element {
+ if context.needs_selector_flags() {
+ el.apply_selector_flags(sibling_flag);
+ }
+ let matched = if relative_selector.match_hint.is_subtree() {
+ matches_relative_selector_subtree(
+ &relative_selector.selector,
+ &el,
+ context,
+ rightmost,
+ )
+ } else {
+ matches_complex_selector(relative_selector.selector.iter(), &el, context, rightmost)
+ };
+ if matched {
+ return true;
+ }
+ if relative_selector.match_hint.is_next_sibling() {
+ break;
+ }
+ next_element = el.next_sibling_element();
+ }
+ }
+ return false;
+}
+
+fn relative_selector_match_early<E: Element>(
+ selector: &RelativeSelector<E::Impl>,
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+) -> Option<bool> {
+ if context.matching_for_invalidation() {
+ // In the context of invalidation, we can't use caching/filtering due to
+ // now/then matches. DOM structure also may have changed, so just pretend
+ // that we always match.
+ return Some(!context.in_negation());
+ }
+ // See if we can return a cached result.
+ if let Some(cached) = context
+ .selector_caches
+ .relative_selector
+ .lookup(element.opaque(), selector)
+ {
+ return Some(cached.matched());
+ }
+ // See if we can fast-reject.
+ if context
+ .selector_caches
+ .relative_selector_filter_map
+ .fast_reject(element, selector, context.quirks_mode())
+ {
+ // Alright, add as unmatched to cache.
+ context.selector_caches.relative_selector.add(
+ element.opaque(),
+ selector,
+ RelativeSelectorCachedMatch::NotMatched,
+ );
+ return Some(false);
+ }
+ None
+}
+
+fn match_relative_selectors<E: Element>(
+ selectors: &[RelativeSelector<E::Impl>],
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool {
+ if context.relative_selector_anchor().is_some() {
+ // FIXME(emilio): This currently can happen with nesting, and it's not fully
+ // correct, arguably. But the ideal solution isn't super-clear either. For now,
+ // cope with it and explicitly reject it at match time. See [1] for discussion.
+ //
+ // [1]: https://github.com/w3c/csswg-drafts/issues/9600
+ return false;
+ }
+ context.nest_for_relative_selector(element.opaque(), |context| {
+ do_match_relative_selectors(selectors, element, context, rightmost)
+ })
+}
+
+/// Matches a relative selector in a list of relative selectors.
+fn do_match_relative_selectors<E: Element>(
+ selectors: &[RelativeSelector<E::Impl>],
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool {
+ // Due to style sharing implications (See style sharing code), we mark the current styling context
+ // to mark elements considered for :has matching. Additionally, we want to mark the elements themselves,
+ // since we don't want to indiscriminately invalidate every element as a potential anchor.
+ if rightmost == SubjectOrPseudoElement::Yes {
+ context.considered_relative_selector.considered_anchor();
+ if context.needs_selector_flags() {
+ element.apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR);
+ }
+ } else {
+ context.considered_relative_selector.considered();
+ if context.needs_selector_flags() {
+ element
+ .apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT);
+ }
+ }
+
+ for relative_selector in selectors.iter() {
+ if let Some(result) = relative_selector_match_early(relative_selector, element, context) {
+ if result {
+ return true;
+ }
+ // Early return indicates no match, continue to next selector.
+ continue;
+ }
+
+ let matched = matches_relative_selector(relative_selector, element, context, rightmost);
+ context.selector_caches.relative_selector.add(
+ element.opaque(),
+ relative_selector,
+ if matched {
+ RelativeSelectorCachedMatch::Matched
+ } else {
+ RelativeSelectorCachedMatch::NotMatched
+ },
+ );
+ if matched {
+ return true;
+ }
+ }
+
+ false
+}
+
+fn matches_relative_selector_subtree<E: Element>(
+ selector: &Selector<E::Impl>,
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool {
+ let mut current = element.first_element_child();
+
+ while let Some(el) = current {
+ if context.needs_selector_flags() {
+ el.apply_selector_flags(
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR,
+ );
+ }
+ if matches_complex_selector(selector.iter(), &el, context, rightmost) {
+ return true;
+ }
+
+ if matches_relative_selector_subtree(selector, &el, context, rightmost) {
+ return true;
+ }
+
+ current = el.next_sibling_element();
+ }
+
+ false
+}
+
+/// Whether the :hover and :active quirk applies.
+///
+/// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk
+fn hover_and_active_quirk_applies<Impl: SelectorImpl>(
+ selector_iter: &SelectorIter<Impl>,
+ context: &MatchingContext<Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool {
+ if context.quirks_mode() != QuirksMode::Quirks {
+ return false;
+ }
+
+ if context.is_nested() {
+ return false;
+ }
+
+ // This compound selector had a pseudo-element to the right that we
+ // intentionally skipped.
+ if rightmost == SubjectOrPseudoElement::Yes &&
+ context.matching_mode() == MatchingMode::ForStatelessPseudoElement
+ {
+ return false;
+ }
+
+ selector_iter.clone().all(|simple| match *simple {
+ Component::LocalName(_) |
+ Component::AttributeInNoNamespaceExists { .. } |
+ Component::AttributeInNoNamespace { .. } |
+ Component::AttributeOther(_) |
+ Component::ID(_) |
+ Component::Class(_) |
+ Component::PseudoElement(_) |
+ Component::Negation(_) |
+ Component::Empty |
+ Component::Nth(_) |
+ Component::NthOf(_) => false,
+ Component::NonTSPseudoClass(ref pseudo_class) => pseudo_class.is_active_or_hover(),
+ _ => true,
+ })
+}
+
+#[derive(Clone, Copy, PartialEq)]
+enum SubjectOrPseudoElement {
+ Yes,
+ No,
+}
+
+fn host_for_part<E>(element: &E, context: &MatchingContext<E::Impl>) -> Option<E>
+where
+ E: Element,
+{
+ let scope = context.current_host;
+ let mut curr = element.containing_shadow_host()?;
+ if scope == Some(curr.opaque()) {
+ return Some(curr);
+ }
+ loop {
+ let parent = curr.containing_shadow_host();
+ if parent.as_ref().map(|h| h.opaque()) == scope {
+ return Some(curr);
+ }
+ curr = parent?;
+ }
+}
+
+fn assigned_slot<E>(element: &E, context: &MatchingContext<E::Impl>) -> Option<E>
+where
+ E: Element,
+{
+ debug_assert!(element
+ .assigned_slot()
+ .map_or(true, |s| s.is_html_slot_element()));
+ let scope = context.current_host?;
+ let mut current_slot = element.assigned_slot()?;
+ while current_slot.containing_shadow_host().unwrap().opaque() != scope {
+ current_slot = current_slot.assigned_slot()?;
+ }
+ Some(current_slot)
+}
+
+#[inline(always)]
+fn next_element_for_combinator<E>(
+ element: &E,
+ combinator: Combinator,
+ selector: &SelectorIter<E::Impl>,
+ context: &MatchingContext<E::Impl>,
+) -> Option<E>
+where
+ E: Element,
+{
+ match combinator {
+ Combinator::NextSibling | Combinator::LaterSibling => element.prev_sibling_element(),
+ Combinator::Child | Combinator::Descendant => {
+ match element.parent_element() {
+ Some(e) => return Some(e),
+ None => {},
+ }
+
+ if !element.parent_node_is_shadow_root() {
+ return None;
+ }
+
+ // https://drafts.csswg.org/css-scoping/#host-element-in-tree:
+ //
+ // For the purpose of Selectors, a shadow host also appears in
+ // its shadow tree, with the contents of the shadow tree treated
+ // as its children. (In other words, the shadow host is treated as
+ // replacing the shadow root node.)
+ //
+ // and also:
+ //
+ // When considered within its own shadow trees, the shadow host is
+ // featureless. Only the :host, :host(), and :host-context()
+ // pseudo-classes are allowed to match it.
+ //
+ // Since we know that the parent is a shadow root, we necessarily
+ // are in a shadow tree of the host, and the next selector will only
+ // match if the selector is a featureless :host selector.
+ if !selector.clone().is_featureless_host_selector() {
+ return None;
+ }
+
+ element.containing_shadow_host()
+ },
+ Combinator::Part => host_for_part(element, context),
+ Combinator::SlotAssignment => assigned_slot(element, context),
+ Combinator::PseudoElement => element.pseudo_element_originating_element(),
+ }
+}
+
+fn matches_complex_selector_internal<E>(
+ mut selector_iter: SelectorIter<E::Impl>,
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> SelectorMatchingResult
+where
+ E: Element,
+{
+ debug!(
+ "Matching complex selector {:?} for {:?}",
+ selector_iter, element
+ );
+
+ let matches_compound_selector =
+ matches_compound_selector(&mut selector_iter, element, context, rightmost);
+
+ let combinator = selector_iter.next_sequence();
+ if combinator.map_or(false, |c| c.is_sibling()) {
+ if context.needs_selector_flags() {
+ element.apply_selector_flags(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS);
+ }
+ }
+
+ if !matches_compound_selector {
+ return SelectorMatchingResult::NotMatchedAndRestartFromClosestLaterSibling;
+ }
+
+ let combinator = match combinator {
+ None => return SelectorMatchingResult::Matched,
+ Some(c) => c,
+ };
+
+ let (candidate_not_found, mut rightmost) = match combinator {
+ Combinator::NextSibling | Combinator::LaterSibling => {
+ (SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant, SubjectOrPseudoElement::No)
+ },
+ Combinator::Child |
+ Combinator::Descendant |
+ Combinator::SlotAssignment |
+ Combinator::Part => (SelectorMatchingResult::NotMatchedGlobally, SubjectOrPseudoElement::No),
+ Combinator::PseudoElement => (SelectorMatchingResult::NotMatchedGlobally, rightmost),
+ };
+
+ // Stop matching :visited as soon as we find a link, or a combinator for
+ // something that isn't an ancestor.
+ let mut visited_handling = if combinator.is_sibling() {
+ VisitedHandlingMode::AllLinksUnvisited
+ } else {
+ context.visited_handling()
+ };
+
+ let mut element = element.clone();
+ loop {
+ if element.is_link() {
+ visited_handling = VisitedHandlingMode::AllLinksUnvisited;
+ }
+
+ element = match next_element_for_combinator(&element, combinator, &selector_iter, &context)
+ {
+ None => return candidate_not_found,
+ Some(next_element) => next_element,
+ };
+
+ let result = context.with_visited_handling_mode(visited_handling, |context| {
+ matches_complex_selector_internal(
+ selector_iter.clone(),
+ &element,
+ context,
+ rightmost,
+ )
+ });
+
+ if !matches!(combinator, Combinator::PseudoElement) {
+ rightmost = SubjectOrPseudoElement::No;
+ }
+
+ match (result, combinator) {
+ // Return the status immediately.
+ (SelectorMatchingResult::Matched, _) |
+ (SelectorMatchingResult::NotMatchedGlobally, _) |
+ (_, Combinator::NextSibling) => {
+ return result;
+ },
+
+ // Upgrade the failure status to
+ // NotMatchedAndRestartFromClosestDescendant.
+ (_, Combinator::PseudoElement) | (_, Combinator::Child) => {
+ return SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant;
+ },
+
+ // If the failure status is
+ // NotMatchedAndRestartFromClosestDescendant and combinator is
+ // Combinator::LaterSibling, give up this Combinator::LaterSibling
+ // matching and restart from the closest descendant combinator.
+ (
+ SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant,
+ Combinator::LaterSibling,
+ ) => {
+ return result;
+ },
+
+ // The Combinator::Descendant combinator and the status is
+ // NotMatchedAndRestartFromClosestLaterSibling or
+ // NotMatchedAndRestartFromClosestDescendant, or the
+ // Combinator::LaterSibling combinator and the status is
+ // NotMatchedAndRestartFromClosestDescendant, we can continue to
+ // matching on the next candidate element.
+ _ => {},
+ }
+ }
+}
+
+#[inline]
+fn matches_local_name<E>(element: &E, local_name: &LocalName<E::Impl>) -> bool
+where
+ E: Element,
+{
+ let name = select_name(element, &local_name.name, &local_name.lower_name).borrow();
+ element.has_local_name(name)
+}
+
+fn matches_part<E>(
+ element: &E,
+ parts: &[<E::Impl as SelectorImpl>::Identifier],
+ context: &mut MatchingContext<E::Impl>,
+) -> bool
+where
+ E: Element,
+{
+ let mut hosts = SmallVec::<[E; 4]>::new();
+
+ let mut host = match element.containing_shadow_host() {
+ Some(h) => h,
+ None => return false,
+ };
+
+ let current_host = context.current_host;
+ if current_host != Some(host.opaque()) {
+ loop {
+ let outer_host = host.containing_shadow_host();
+ if outer_host.as_ref().map(|h| h.opaque()) == current_host {
+ break;
+ }
+ let outer_host = match outer_host {
+ Some(h) => h,
+ None => return false,
+ };
+ // TODO(emilio): if worth it, we could early return if
+ // host doesn't have the exportparts attribute.
+ hosts.push(host);
+ host = outer_host;
+ }
+ }
+
+ // Translate the part into the right scope.
+ parts.iter().all(|part| {
+ let mut part = part.clone();
+ for host in hosts.iter().rev() {
+ part = match host.imported_part(&part) {
+ Some(p) => p,
+ None => return false,
+ };
+ }
+ element.is_part(&part)
+ })
+}
+
+fn matches_host<E>(
+ element: &E,
+ selector: Option<&Selector<E::Impl>>,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool
+where
+ E: Element,
+{
+ let host = match context.shadow_host() {
+ Some(h) => h,
+ None => return false,
+ };
+ if host != element.opaque() {
+ return false;
+ }
+ selector.map_or(true, |selector| {
+ context
+ .nest(|context| matches_complex_selector(selector.iter(), element, context, rightmost))
+ })
+}
+
+fn matches_slotted<E>(
+ element: &E,
+ selector: &Selector<E::Impl>,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool
+where
+ E: Element,
+{
+ // <slots> are never flattened tree slottables.
+ if element.is_html_slot_element() {
+ return false;
+ }
+ context.nest(|context| matches_complex_selector(selector.iter(), element, context, rightmost))
+}
+
+fn matches_rare_attribute_selector<E>(
+ element: &E,
+ attr_sel: &AttrSelectorWithOptionalNamespace<E::Impl>,
+) -> bool
+where
+ E: Element,
+{
+ let empty_string;
+ let namespace = match attr_sel.namespace() {
+ Some(ns) => ns,
+ None => {
+ empty_string = crate::parser::namespace_empty_string::<E::Impl>();
+ NamespaceConstraint::Specific(&empty_string)
+ },
+ };
+ element.attr_matches(
+ &namespace,
+ select_name(element, &attr_sel.local_name, &attr_sel.local_name_lower),
+ &match attr_sel.operation {
+ ParsedAttrSelectorOperation::Exists => AttrSelectorOperation::Exists,
+ ParsedAttrSelectorOperation::WithValue {
+ operator,
+ case_sensitivity,
+ ref value,
+ } => AttrSelectorOperation::WithValue {
+ operator,
+ case_sensitivity: to_unconditional_case_sensitivity(case_sensitivity, element),
+ value,
+ },
+ },
+ )
+}
+
+/// Determines whether the given element matches the given compound selector.
+#[inline]
+fn matches_compound_selector<E>(
+ selector_iter: &mut SelectorIter<E::Impl>,
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ rightmost: SubjectOrPseudoElement,
+) -> bool
+where
+ E: Element,
+{
+ let quirks_data = if context.quirks_mode() == QuirksMode::Quirks {
+ Some(selector_iter.clone())
+ } else {
+ None
+ };
+ let mut local_context = LocalMatchingContext {
+ shared: context,
+ rightmost,
+ quirks_data,
+ };
+ selector_iter.all(|simple| matches_simple_selector(simple, element, &mut local_context))
+}
+
+/// Determines whether the given element matches the given single selector.
+fn matches_simple_selector<E>(
+ selector: &Component<E::Impl>,
+ element: &E,
+ context: &mut LocalMatchingContext<E::Impl>,
+) -> bool
+where
+ E: Element,
+{
+ debug_assert!(context.shared.is_nested() || !context.shared.in_negation());
+ let rightmost = context.rightmost;
+ match *selector {
+ Component::ID(ref id) => {
+ element.has_id(id, context.shared.classes_and_ids_case_sensitivity())
+ },
+ Component::Class(ref class) => {
+ element.has_class(class, context.shared.classes_and_ids_case_sensitivity())
+ },
+ Component::LocalName(ref local_name) => matches_local_name(element, local_name),
+ Component::AttributeInNoNamespaceExists {
+ ref local_name,
+ ref local_name_lower,
+ } => element.has_attr_in_no_namespace(select_name(element, local_name, local_name_lower)),
+ Component::AttributeInNoNamespace {
+ ref local_name,
+ ref value,
+ operator,
+ case_sensitivity,
+ } => element.attr_matches(
+ &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::<E::Impl>()),
+ local_name,
+ &AttrSelectorOperation::WithValue {
+ operator,
+ case_sensitivity: to_unconditional_case_sensitivity(case_sensitivity, element),
+ value,
+ },
+ ),
+ Component::AttributeOther(ref attr_sel) => {
+ matches_rare_attribute_selector(element, attr_sel)
+ },
+ Component::Part(ref parts) => matches_part(element, parts, &mut context.shared),
+ Component::Slotted(ref selector) => {
+ matches_slotted(element, selector, &mut context.shared, rightmost)
+ },
+ Component::PseudoElement(ref pseudo) => {
+ element.match_pseudo_element(pseudo, context.shared)
+ },
+ Component::ExplicitUniversalType | Component::ExplicitAnyNamespace => true,
+ Component::Namespace(_, ref url) | Component::DefaultNamespace(ref url) => {
+ element.has_namespace(&url.borrow())
+ },
+ Component::ExplicitNoNamespace => {
+ let ns = crate::parser::namespace_empty_string::<E::Impl>();
+ element.has_namespace(&ns.borrow())
+ },
+ Component::NonTSPseudoClass(ref pc) => {
+ if let Some(ref iter) = context.quirks_data {
+ if pc.is_active_or_hover() &&
+ !element.is_link() &&
+ hover_and_active_quirk_applies(iter, context.shared, context.rightmost)
+ {
+ return false;
+ }
+ }
+ element.match_non_ts_pseudo_class(pc, &mut context.shared)
+ },
+ Component::Root => element.is_root(),
+ Component::Empty => {
+ if context.shared.needs_selector_flags() {
+ element.apply_selector_flags(ElementSelectorFlags::HAS_EMPTY_SELECTOR);
+ }
+ element.is_empty()
+ },
+ Component::Host(ref selector) => {
+ matches_host(element, selector.as_ref(), &mut context.shared, rightmost)
+ },
+ Component::ParentSelector | Component::Scope => match context.shared.scope_element {
+ Some(ref scope_element) => element.opaque() == *scope_element,
+ None => element.is_root(),
+ },
+ Component::Nth(ref nth_data) => {
+ matches_generic_nth_child(element, context.shared, nth_data, &[], rightmost)
+ },
+ Component::NthOf(ref nth_of_data) => context.shared.nest(|context| {
+ matches_generic_nth_child(
+ element,
+ context,
+ nth_of_data.nth_data(),
+ nth_of_data.selectors(),
+ rightmost,
+ )
+ }),
+ Component::Is(ref list) | Component::Where(ref list) => context.shared.nest(|context| {
+ matches_complex_selector_list(list.slice(), element, context, rightmost)
+ }),
+ Component::Negation(ref list) => context.shared.nest_for_negation(|context| {
+ !matches_complex_selector_list(list.slice(), element, context, rightmost)
+ }),
+ Component::Has(ref relative_selectors) => {
+ match_relative_selectors(relative_selectors, element, context.shared, rightmost)
+ },
+ Component::Combinator(_) => unsafe {
+ debug_unreachable!("Shouldn't try to selector-match combinators")
+ },
+ Component::RelativeSelectorAnchor => {
+ let anchor = context.shared.relative_selector_anchor();
+ // We may match inner relative selectors, in which case we want to always match.
+ anchor.map_or(true, |a| a == element.opaque())
+ },
+ Component::Invalid(..) => false,
+ }
+}
+
+#[inline(always)]
+pub fn select_name<'a, E: Element, T: PartialEq>(
+ element: &E,
+ local_name: &'a T,
+ local_name_lower: &'a T,
+) -> &'a T {
+ if local_name == local_name_lower || element.is_html_element_in_html_document() {
+ local_name_lower
+ } else {
+ local_name
+ }
+}
+
+#[inline(always)]
+pub fn to_unconditional_case_sensitivity<'a, E: Element>(
+ parsed: ParsedCaseSensitivity,
+ element: &E,
+) -> CaseSensitivity {
+ match parsed {
+ ParsedCaseSensitivity::CaseSensitive | ParsedCaseSensitivity::ExplicitCaseSensitive => {
+ CaseSensitivity::CaseSensitive
+ },
+ ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive,
+ ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {
+ if element.is_html_element_in_html_document() {
+ CaseSensitivity::AsciiCaseInsensitive
+ } else {
+ CaseSensitivity::CaseSensitive
+ }
+ },
+ }
+}
+
+fn matches_generic_nth_child<E>(
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ nth_data: &NthSelectorData,
+ selectors: &[Selector<E::Impl>],
+ rightmost: SubjectOrPseudoElement,
+) -> bool
+where
+ E: Element,
+{
+ if element.ignores_nth_child_selectors() {
+ return false;
+ }
+ let has_selectors = !selectors.is_empty();
+ let selectors_match =
+ !has_selectors || matches_complex_selector_list(selectors, element, context, rightmost);
+ if context.matching_for_invalidation() {
+ // Skip expensive indexing math in invalidation.
+ return selectors_match && !context.in_negation();
+ }
+
+ let NthSelectorData { ty, a, b, .. } = *nth_data;
+ let is_of_type = ty.is_of_type();
+ if ty.is_only() {
+ debug_assert!(
+ !has_selectors,
+ ":only-child and :only-of-type cannot have a selector list!"
+ );
+ return matches_generic_nth_child(
+ element,
+ context,
+ &NthSelectorData::first(is_of_type),
+ selectors,
+ rightmost,
+ ) && matches_generic_nth_child(
+ element,
+ context,
+ &NthSelectorData::last(is_of_type),
+ selectors,
+ rightmost,
+ );
+ }
+
+ let is_from_end = ty.is_from_end();
+
+ // It's useful to know whether this can only select the first/last element
+ // child for optimization purposes, see the `HAS_EDGE_CHILD_SELECTOR` flag.
+ let is_edge_child_selector = nth_data.is_simple_edge() && !has_selectors;
+
+ if context.needs_selector_flags() {
+ let mut flags = if is_edge_child_selector {
+ ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR
+ } else if is_from_end {
+ ElementSelectorFlags::HAS_SLOW_SELECTOR
+ } else {
+ ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS
+ };
+ flags |= if has_selectors {
+ ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF
+ } else {
+ ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH
+ };
+ element.apply_selector_flags(flags);
+ }
+
+ if !selectors_match {
+ return false;
+ }
+
+ // :first/last-child are rather trivial to match, don't bother with the
+ // cache.
+ if is_edge_child_selector {
+ return if is_from_end {
+ element.next_sibling_element()
+ } else {
+ element.prev_sibling_element()
+ }
+ .is_none();
+ }
+
+ // Lookup or compute the index.
+ let index = if let Some(i) = context
+ .nth_index_cache(is_of_type, is_from_end, selectors)
+ .lookup(element.opaque())
+ {
+ i
+ } else {
+ let i = nth_child_index(
+ element,
+ context,
+ selectors,
+ is_of_type,
+ is_from_end,
+ /* check_cache = */ true,
+ rightmost,
+ );
+ context
+ .nth_index_cache(is_of_type, is_from_end, selectors)
+ .insert(element.opaque(), i);
+ i
+ };
+ debug_assert_eq!(
+ index,
+ nth_child_index(
+ element,
+ context,
+ selectors,
+ is_of_type,
+ is_from_end,
+ /* check_cache = */ false,
+ rightmost,
+ ),
+ "invalid cache"
+ );
+
+ // Is there a non-negative integer n such that An+B=index?
+ match index.checked_sub(b) {
+ None => false,
+ Some(an) => match an.checked_div(a) {
+ Some(n) => n >= 0 && a * n == an,
+ None /* a == 0 */ => an == 0,
+ },
+ }
+}
+
+#[inline]
+fn nth_child_index<E>(
+ element: &E,
+ context: &mut MatchingContext<E::Impl>,
+ selectors: &[Selector<E::Impl>],
+ is_of_type: bool,
+ is_from_end: bool,
+ check_cache: bool,
+ rightmost: SubjectOrPseudoElement,
+) -> i32
+where
+ E: Element,
+{
+ // The traversal mostly processes siblings left to right. So when we walk
+ // siblings to the right when computing NthLast/NthLastOfType we're unlikely
+ // to get cache hits along the way. As such, we take the hit of walking the
+ // siblings to the left checking the cache in the is_from_end case (this
+ // matches what Gecko does). The indices-from-the-left is handled during the
+ // regular look further below.
+ if check_cache &&
+ is_from_end &&
+ !context
+ .nth_index_cache(is_of_type, is_from_end, selectors)
+ .is_empty()
+ {
+ let mut index: i32 = 1;
+ let mut curr = element.clone();
+ while let Some(e) = curr.prev_sibling_element() {
+ curr = e;
+ let matches = if is_of_type {
+ element.is_same_type(&curr)
+ } else if !selectors.is_empty() {
+ matches_complex_selector_list(selectors, &curr, context, rightmost)
+ } else {
+ true
+ };
+ if !matches {
+ continue;
+ }
+ if let Some(i) = context
+ .nth_index_cache(is_of_type, is_from_end, selectors)
+ .lookup(curr.opaque())
+ {
+ return i - index;
+ }
+ index += 1;
+ }
+ }
+
+ let mut index: i32 = 1;
+ let mut curr = element.clone();
+ let next = |e: E| {
+ if is_from_end {
+ e.next_sibling_element()
+ } else {
+ e.prev_sibling_element()
+ }
+ };
+ while let Some(e) = next(curr) {
+ curr = e;
+ let matches = if is_of_type {
+ element.is_same_type(&curr)
+ } else if !selectors.is_empty() {
+ matches_complex_selector_list(selectors, &curr, context, rightmost)
+ } else {
+ true
+ };
+ if !matches {
+ continue;
+ }
+ // If we're computing indices from the left, check each element in the
+ // cache. We handle the indices-from-the-right case at the top of this
+ // function.
+ if !is_from_end && check_cache {
+ if let Some(i) = context
+ .nth_index_cache(is_of_type, is_from_end, selectors)
+ .lookup(curr.opaque())
+ {
+ return i + index;
+ }
+ }
+ index += 1;
+ }
+
+ index
+}
diff --git a/servo/components/selectors/nth_index_cache.rs b/servo/components/selectors/nth_index_cache.rs
new file mode 100644
index 0000000000..b4b41578d0
--- /dev/null
+++ b/servo/components/selectors/nth_index_cache.rs
@@ -0,0 +1,102 @@
+/* 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/. */
+
+use std::hash::Hash;
+
+use crate::{parser::Selector, tree::OpaqueElement, SelectorImpl};
+use fxhash::FxHashMap;
+
+/// A cache to speed up matching of nth-index-like selectors.
+///
+/// See [1] for some discussion around the design tradeoffs.
+///
+/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1401855#c3
+#[derive(Default)]
+pub struct NthIndexCache {
+ nth: NthIndexCacheInner,
+ nth_of_selectors: NthIndexOfSelectorsCaches,
+ nth_last: NthIndexCacheInner,
+ nth_last_of_selectors: NthIndexOfSelectorsCaches,
+ nth_of_type: NthIndexCacheInner,
+ nth_last_of_type: NthIndexCacheInner,
+}
+
+impl NthIndexCache {
+ /// Gets the appropriate cache for the given parameters.
+ pub fn get<Impl: SelectorImpl>(
+ &mut self,
+ is_of_type: bool,
+ is_from_end: bool,
+ selectors: &[Selector<Impl>],
+ ) -> &mut NthIndexCacheInner {
+ if is_of_type {
+ return if is_from_end {
+ &mut self.nth_last_of_type
+ } else {
+ &mut self.nth_of_type
+ };
+ }
+ if !selectors.is_empty() {
+ return if is_from_end {
+ self.nth_last_of_selectors.lookup(selectors)
+ } else {
+ self.nth_of_selectors.lookup(selectors)
+ };
+ }
+ if is_from_end {
+ &mut self.nth_last
+ } else {
+ &mut self.nth
+ }
+ }
+}
+
+#[derive(Hash, Eq, PartialEq)]
+struct SelectorListCacheKey(usize);
+
+/// Use the selector list's pointer as the cache key
+impl SelectorListCacheKey {
+ // :nth-child of selectors are reference-counted with `ThinArc`, so we know their pointers are stable.
+ fn new<Impl: SelectorImpl>(selectors: &[Selector<Impl>]) -> Self {
+ Self(selectors.as_ptr() as usize)
+ }
+}
+
+/// Use a different map of cached indices per :nth-child's or :nth-last-child's selector list
+#[derive(Default)]
+pub struct NthIndexOfSelectorsCaches(FxHashMap<SelectorListCacheKey, NthIndexCacheInner>);
+
+/// Get or insert a map of cached incides for the selector list of this
+/// particular :nth-child or :nth-last-child pseudoclass
+impl NthIndexOfSelectorsCaches {
+ pub fn lookup<Impl: SelectorImpl>(
+ &mut self,
+ selectors: &[Selector<Impl>],
+ ) -> &mut NthIndexCacheInner {
+ self.0
+ .entry(SelectorListCacheKey::new(selectors))
+ .or_default()
+ }
+}
+
+/// The concrete per-pseudo-class cache.
+#[derive(Default)]
+pub struct NthIndexCacheInner(FxHashMap<OpaqueElement, i32>);
+
+impl NthIndexCacheInner {
+ /// Does a lookup for a given element in the cache.
+ pub fn lookup(&mut self, el: OpaqueElement) -> Option<i32> {
+ self.0.get(&el).copied()
+ }
+
+ /// Inserts an entry into the cache.
+ pub fn insert(&mut self, element: OpaqueElement, index: i32) {
+ self.0.insert(element, index);
+ }
+
+ /// Returns whether the cache is empty.
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
diff --git a/servo/components/selectors/parser.rs b/servo/components/selectors/parser.rs
new file mode 100644
index 0000000000..792d4eb8bc
--- /dev/null
+++ b/servo/components/selectors/parser.rs
@@ -0,0 +1,4483 @@
+/* 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/. */
+
+use crate::attr::{AttrSelectorOperator, AttrSelectorWithOptionalNamespace};
+use crate::attr::{NamespaceConstraint, ParsedAttrSelectorOperation, ParsedCaseSensitivity};
+use crate::bloom::BLOOM_HASH_MASK;
+use crate::builder::{
+ relative_selector_list_specificity_and_flags, selector_list_specificity_and_flags,
+ SelectorBuilder, SelectorFlags, Specificity, SpecificityAndFlags,
+};
+use crate::context::QuirksMode;
+use crate::sink::Push;
+use crate::visitor::SelectorListKind;
+pub use crate::visitor::SelectorVisitor;
+use cssparser::parse_nth;
+use cssparser::{BasicParseError, BasicParseErrorKind, ParseError, ParseErrorKind};
+use cssparser::{CowRcStr, Delimiter, SourceLocation};
+use cssparser::{Parser as CssParser, ToCss, Token};
+use precomputed_hash::PrecomputedHash;
+use servo_arc::{Arc, ArcUnionBorrow, ThinArc, ThinArcUnion, UniqueArc};
+use smallvec::SmallVec;
+use std::borrow::{Borrow, Cow};
+use std::fmt::{self, Debug};
+use std::iter::Rev;
+use std::slice;
+
+/// A trait that represents a pseudo-element.
+pub trait PseudoElement: Sized + ToCss {
+ /// The `SelectorImpl` this pseudo-element is used for.
+ type Impl: SelectorImpl;
+
+ /// Whether the pseudo-element supports a given state selector to the right
+ /// of it.
+ fn accepts_state_pseudo_classes(&self) -> bool {
+ false
+ }
+
+ /// Whether this pseudo-element is valid after a ::slotted(..) pseudo.
+ fn valid_after_slotted(&self) -> bool {
+ false
+ }
+}
+
+/// A trait that represents a pseudo-class.
+pub trait NonTSPseudoClass: Sized + ToCss {
+ /// The `SelectorImpl` this pseudo-element is used for.
+ type Impl: SelectorImpl;
+
+ /// Whether this pseudo-class is :active or :hover.
+ fn is_active_or_hover(&self) -> bool;
+
+ /// Whether this pseudo-class belongs to:
+ ///
+ /// https://drafts.csswg.org/selectors-4/#useraction-pseudos
+ fn is_user_action_state(&self) -> bool;
+
+ fn visit<V>(&self, _visitor: &mut V) -> bool
+ where
+ V: SelectorVisitor<Impl = Self::Impl>,
+ {
+ true
+ }
+}
+
+/// Returns a Cow::Borrowed if `s` is already ASCII lowercase, and a
+/// Cow::Owned if `s` had to be converted into ASCII lowercase.
+fn to_ascii_lowercase(s: &str) -> Cow<str> {
+ if let Some(first_uppercase) = s.bytes().position(|byte| byte >= b'A' && byte <= b'Z') {
+ let mut string = s.to_owned();
+ string[first_uppercase..].make_ascii_lowercase();
+ string.into()
+ } else {
+ s.into()
+ }
+}
+
+bitflags! {
+ /// Flags that indicate at which point of parsing a selector are we.
+ #[derive(Copy, Clone)]
+ struct SelectorParsingState: u8 {
+ /// Whether we should avoid adding default namespaces to selectors that
+ /// aren't type or universal selectors.
+ const SKIP_DEFAULT_NAMESPACE = 1 << 0;
+
+ /// Whether we've parsed a ::slotted() pseudo-element already.
+ ///
+ /// If so, then we can only parse a subset of pseudo-elements, and
+ /// whatever comes after them if so.
+ const AFTER_SLOTTED = 1 << 1;
+ /// Whether we've parsed a ::part() pseudo-element already.
+ ///
+ /// If so, then we can only parse a subset of pseudo-elements, and
+ /// whatever comes after them if so.
+ const AFTER_PART = 1 << 2;
+ /// Whether we've parsed a pseudo-element (as in, an
+ /// `Impl::PseudoElement` thus not accounting for `::slotted` or
+ /// `::part`) already.
+ ///
+ /// If so, then other pseudo-elements and most other selectors are
+ /// disallowed.
+ const AFTER_PSEUDO_ELEMENT = 1 << 3;
+ /// Whether we've parsed a non-stateful pseudo-element (again, as-in
+ /// `Impl::PseudoElement`) already. If so, then other pseudo-classes are
+ /// disallowed. If this flag is set, `AFTER_PSEUDO_ELEMENT` must be set
+ /// as well.
+ const AFTER_NON_STATEFUL_PSEUDO_ELEMENT = 1 << 4;
+
+ /// Whether we are after any of the pseudo-like things.
+ const AFTER_PSEUDO = Self::AFTER_PART.bits() | Self::AFTER_SLOTTED.bits() | Self::AFTER_PSEUDO_ELEMENT.bits();
+
+ /// Whether we explicitly disallow combinators.
+ const DISALLOW_COMBINATORS = 1 << 5;
+
+ /// Whether we explicitly disallow pseudo-element-like things.
+ const DISALLOW_PSEUDOS = 1 << 6;
+
+ /// Whether we explicitly disallow relative selectors (i.e. `:has()`).
+ const DISALLOW_RELATIVE_SELECTOR = 1 << 7;
+ }
+}
+
+impl SelectorParsingState {
+ #[inline]
+ fn allows_pseudos(self) -> bool {
+ // NOTE(emilio): We allow pseudos after ::part and such.
+ !self.intersects(Self::AFTER_PSEUDO_ELEMENT | Self::DISALLOW_PSEUDOS)
+ }
+
+ #[inline]
+ fn allows_slotted(self) -> bool {
+ !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS)
+ }
+
+ #[inline]
+ fn allows_part(self) -> bool {
+ !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS)
+ }
+
+ #[inline]
+ fn allows_non_functional_pseudo_classes(self) -> bool {
+ !self.intersects(Self::AFTER_SLOTTED | Self::AFTER_NON_STATEFUL_PSEUDO_ELEMENT)
+ }
+
+ #[inline]
+ fn allows_tree_structural_pseudo_classes(self) -> bool {
+ !self.intersects(Self::AFTER_PSEUDO)
+ }
+
+ #[inline]
+ fn allows_combinators(self) -> bool {
+ !self.intersects(Self::DISALLOW_COMBINATORS)
+ }
+}
+
+pub type SelectorParseError<'i> = ParseError<'i, SelectorParseErrorKind<'i>>;
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum SelectorParseErrorKind<'i> {
+ NoQualifiedNameInAttributeSelector(Token<'i>),
+ EmptySelector,
+ DanglingCombinator,
+ NonCompoundSelector,
+ NonPseudoElementAfterSlotted,
+ InvalidPseudoElementAfterSlotted,
+ InvalidPseudoElementInsideWhere,
+ InvalidState,
+ UnexpectedTokenInAttributeSelector(Token<'i>),
+ PseudoElementExpectedColon(Token<'i>),
+ PseudoElementExpectedIdent(Token<'i>),
+ NoIdentForPseudo(Token<'i>),
+ UnsupportedPseudoClassOrElement(CowRcStr<'i>),
+ UnexpectedIdent(CowRcStr<'i>),
+ ExpectedNamespace(CowRcStr<'i>),
+ ExpectedBarInAttr(Token<'i>),
+ BadValueInAttr(Token<'i>),
+ InvalidQualNameInAttr(Token<'i>),
+ ExplicitNamespaceUnexpectedToken(Token<'i>),
+ ClassNeedsIdent(Token<'i>),
+}
+
+macro_rules! with_all_bounds {
+ (
+ [ $( $InSelector: tt )* ]
+ [ $( $CommonBounds: tt )* ]
+ [ $( $FromStr: tt )* ]
+ ) => {
+ /// This trait allows to define the parser implementation in regards
+ /// of pseudo-classes/elements
+ ///
+ /// NB: We need Clone so that we can derive(Clone) on struct with that
+ /// are parameterized on SelectorImpl. See
+ /// <https://github.com/rust-lang/rust/issues/26925>
+ pub trait SelectorImpl: Clone + Debug + Sized + 'static {
+ type ExtraMatchingData<'a>: Sized + Default;
+ type AttrValue: $($InSelector)*;
+ type Identifier: $($InSelector)* + PrecomputedHash;
+ type LocalName: $($InSelector)* + Borrow<Self::BorrowedLocalName> + PrecomputedHash;
+ type NamespaceUrl: $($CommonBounds)* + Default + Borrow<Self::BorrowedNamespaceUrl> + PrecomputedHash;
+ type NamespacePrefix: $($InSelector)* + Default;
+ type BorrowedNamespaceUrl: ?Sized + Eq;
+ type BorrowedLocalName: ?Sized + Eq;
+
+ /// non tree-structural pseudo-classes
+ /// (see: https://drafts.csswg.org/selectors/#structural-pseudos)
+ type NonTSPseudoClass: $($CommonBounds)* + NonTSPseudoClass<Impl = Self>;
+
+ /// pseudo-elements
+ type PseudoElement: $($CommonBounds)* + PseudoElement<Impl = Self>;
+
+ /// Whether attribute hashes should be collected for filtering
+ /// purposes.
+ fn should_collect_attr_hash(_name: &Self::LocalName) -> bool {
+ false
+ }
+ }
+ }
+}
+
+macro_rules! with_bounds {
+ ( [ $( $CommonBounds: tt )* ] [ $( $FromStr: tt )* ]) => {
+ with_all_bounds! {
+ [$($CommonBounds)* + $($FromStr)* + ToCss]
+ [$($CommonBounds)*]
+ [$($FromStr)*]
+ }
+ }
+}
+
+with_bounds! {
+ [Clone + Eq]
+ [for<'a> From<&'a str>]
+}
+
+pub trait Parser<'i> {
+ type Impl: SelectorImpl;
+ type Error: 'i + From<SelectorParseErrorKind<'i>>;
+
+ /// Whether to parse the `::slotted()` pseudo-element.
+ fn parse_slotted(&self) -> bool {
+ false
+ }
+
+ /// Whether to parse the `::part()` pseudo-element.
+ fn parse_part(&self) -> bool {
+ false
+ }
+
+ /// Whether to parse the selector list of nth-child() or nth-last-child().
+ fn parse_nth_child_of(&self) -> bool {
+ false
+ }
+
+ /// Whether to parse the `:where` pseudo-class.
+ fn parse_is_and_where(&self) -> bool {
+ false
+ }
+
+ /// Whether to parse the :has pseudo-class.
+ fn parse_has(&self) -> bool {
+ false
+ }
+
+ /// Whether to parse the '&' delimiter as a parent selector.
+ fn parse_parent_selector(&self) -> bool {
+ false
+ }
+
+ /// Whether the given function name is an alias for the `:is()` function.
+ fn is_is_alias(&self, _name: &str) -> bool {
+ false
+ }
+
+ /// Whether to parse the `:host` pseudo-class.
+ fn parse_host(&self) -> bool {
+ false
+ }
+
+ /// Whether to allow forgiving selector-list parsing.
+ fn allow_forgiving_selectors(&self) -> bool {
+ true
+ }
+
+ /// This function can return an "Err" pseudo-element in order to support CSS2.1
+ /// pseudo-elements.
+ fn parse_non_ts_pseudo_class(
+ &self,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ ) -> Result<<Self::Impl as SelectorImpl>::NonTSPseudoClass, ParseError<'i, Self::Error>> {
+ Err(
+ location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn parse_non_ts_functional_pseudo_class<'t>(
+ &self,
+ name: CowRcStr<'i>,
+ parser: &mut CssParser<'i, 't>,
+ _after_part: bool,
+ ) -> Result<<Self::Impl as SelectorImpl>::NonTSPseudoClass, ParseError<'i, Self::Error>> {
+ Err(
+ parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn parse_pseudo_element(
+ &self,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ ) -> Result<<Self::Impl as SelectorImpl>::PseudoElement, ParseError<'i, Self::Error>> {
+ Err(
+ location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn parse_functional_pseudo_element<'t>(
+ &self,
+ name: CowRcStr<'i>,
+ arguments: &mut CssParser<'i, 't>,
+ ) -> Result<<Self::Impl as SelectorImpl>::PseudoElement, ParseError<'i, Self::Error>> {
+ Err(
+ arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn default_namespace(&self) -> Option<<Self::Impl as SelectorImpl>::NamespaceUrl> {
+ None
+ }
+
+ fn namespace_for_prefix(
+ &self,
+ _prefix: &<Self::Impl as SelectorImpl>::NamespacePrefix,
+ ) -> Option<<Self::Impl as SelectorImpl>::NamespaceUrl> {
+ None
+ }
+}
+
+/// A selector list is a tagged pointer with either a single selector, or a ThinArc<()> of multiple
+/// selectors.
+#[derive(Clone, Eq, Debug, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+pub struct SelectorList<Impl: SelectorImpl>(
+ #[shmem(field_bound)] ThinArcUnion<SpecificityAndFlags, Component<Impl>, (), Selector<Impl>>,
+);
+
+impl<Impl: SelectorImpl> SelectorList<Impl> {
+ pub fn from_one(selector: Selector<Impl>) -> Self {
+ #[cfg(debug_assertions)]
+ let selector_repr = unsafe { *(&selector as *const _ as *const usize) };
+ let list = Self(ThinArcUnion::from_first(selector.into_data()));
+ #[cfg(debug_assertions)]
+ debug_assert_eq!(
+ selector_repr,
+ unsafe { *(&list as *const _ as *const usize) },
+ "We rely on the same bit representation for the single selector variant"
+ );
+ list
+ }
+
+ pub fn from_iter(mut iter: impl ExactSizeIterator<Item = Selector<Impl>>) -> Self {
+ if iter.len() == 1 {
+ Self::from_one(iter.next().unwrap())
+ } else {
+ Self(ThinArcUnion::from_second(ThinArc::from_header_and_iter(
+ (),
+ iter,
+ )))
+ }
+ }
+
+ #[inline]
+ pub fn slice(&self) -> &[Selector<Impl>] {
+ match self.0.borrow() {
+ ArcUnionBorrow::First(..) => {
+ // SAFETY: see from_one.
+ let selector: &Selector<Impl> = unsafe { std::mem::transmute(self) };
+ std::slice::from_ref(selector)
+ },
+ ArcUnionBorrow::Second(list) => list.get().slice(),
+ }
+ }
+
+ #[inline]
+ pub fn len(&self) -> usize {
+ match self.0.borrow() {
+ ArcUnionBorrow::First(..) => 1,
+ ArcUnionBorrow::Second(list) => list.len(),
+ }
+ }
+
+ /// Returns the address on the heap of the ThinArc for memory reporting.
+ pub fn thin_arc_heap_ptr(&self) -> *const ::std::os::raw::c_void {
+ match self.0.borrow() {
+ ArcUnionBorrow::First(s) => s.with_arc(|a| a.heap_ptr()),
+ ArcUnionBorrow::Second(s) => s.with_arc(|a| a.heap_ptr()),
+ }
+ }
+}
+
+/// Uniquely identify a selector based on its components, which is behind ThinArc and
+/// is therefore stable.
+#[derive(Clone, Copy, Hash, Eq, PartialEq)]
+pub struct SelectorKey(usize);
+
+impl SelectorKey {
+ /// Create a new key based on the given selector.
+ pub fn new<Impl: SelectorImpl>(selector: &Selector<Impl>) -> Self {
+ Self(selector.0.slice().as_ptr() as usize)
+ }
+}
+
+/// Whether or not we're using forgiving parsing mode
+#[derive(PartialEq)]
+enum ForgivingParsing {
+ /// Discard the entire selector list upon encountering any invalid selector.
+ /// This is the default behavior for almost all of CSS.
+ No,
+ /// Ignore invalid selectors, potentially creating an empty selector list.
+ ///
+ /// This is the error recovery mode of :is() and :where()
+ Yes,
+}
+
+/// Flag indicating if we're parsing relative selectors.
+#[derive(Copy, Clone, PartialEq)]
+pub enum ParseRelative {
+ /// Expect selectors to start with a combinator, assuming descendant combinator if not present.
+ ForHas,
+ /// Allow selectors to start with a combinator, prepending a parent selector if so. Do nothing
+ /// otherwise
+ ForNesting,
+ /// Treat as parse error if any selector begins with a combinator.
+ No,
+}
+
+impl<Impl: SelectorImpl> SelectorList<Impl> {
+ /// Returns a selector list with a single `&`
+ pub fn ampersand() -> Self {
+ Self::from_one(Selector::ampersand())
+ }
+
+ /// Parse a comma-separated list of Selectors.
+ /// <https://drafts.csswg.org/selectors/#grouping>
+ ///
+ /// Return the Selectors or Err if there is an invalid selector.
+ pub fn parse<'i, 't, P>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ parse_relative: ParseRelative,
+ ) -> Result<Self, ParseError<'i, P::Error>>
+ where
+ P: Parser<'i, Impl = Impl>,
+ {
+ Self::parse_with_state(
+ parser,
+ input,
+ SelectorParsingState::empty(),
+ ForgivingParsing::No,
+ parse_relative,
+ )
+ }
+
+ #[inline]
+ fn parse_with_state<'i, 't, P>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ state: SelectorParsingState,
+ recovery: ForgivingParsing,
+ parse_relative: ParseRelative,
+ ) -> Result<Self, ParseError<'i, P::Error>>
+ where
+ P: Parser<'i, Impl = Impl>,
+ {
+ let mut values = SmallVec::<[_; 4]>::new();
+ let forgiving = recovery == ForgivingParsing::Yes && parser.allow_forgiving_selectors();
+ loop {
+ let selector = input.parse_until_before(Delimiter::Comma, |input| {
+ let start = input.position();
+ let mut selector = parse_selector(parser, input, state, parse_relative);
+ if forgiving && (selector.is_err() || input.expect_exhausted().is_err()) {
+ input.expect_no_error_token()?;
+ selector = Ok(Selector::new_invalid(input.slice_from(start)));
+ }
+ selector
+ })?;
+
+ values.push(selector);
+
+ match input.next() {
+ Ok(&Token::Comma) => {},
+ Ok(_) => unreachable!(),
+ Err(_) => break,
+ }
+ }
+ Ok(Self::from_iter(values.into_iter()))
+ }
+
+ /// Replaces the parent selector in all the items of the selector list.
+ pub fn replace_parent_selector(&self, parent: &SelectorList<Impl>) -> Self {
+ Self::from_iter(
+ self.slice()
+ .iter()
+ .map(|selector| selector.replace_parent_selector(parent)),
+ )
+ }
+
+ /// Creates a SelectorList from a Vec of selectors. Used in tests.
+ #[allow(dead_code)]
+ pub(crate) fn from_vec(v: Vec<Selector<Impl>>) -> Self {
+ SelectorList::from_iter(v.into_iter())
+ }
+}
+
+/// Parses one compound selector suitable for nested stuff like :-moz-any, etc.
+fn parse_inner_compound_selector<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ state: SelectorParsingState,
+) -> Result<Selector<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ parse_selector(
+ parser,
+ input,
+ state | SelectorParsingState::DISALLOW_PSEUDOS | SelectorParsingState::DISALLOW_COMBINATORS,
+ ParseRelative::No,
+ )
+}
+
+/// Ancestor hashes for the bloom filter. We precompute these and store them
+/// inline with selectors to optimize cache performance during matching.
+/// This matters a lot.
+///
+/// We use 4 hashes, which is copied from Gecko, who copied it from WebKit.
+/// Note that increasing the number of hashes here will adversely affect the
+/// cache hit when fast-rejecting long lists of Rules with inline hashes.
+///
+/// Because the bloom filter only uses the bottom 24 bits of the hash, we pack
+/// the fourth hash into the upper bits of the first three hashes in order to
+/// shrink Rule (whose size matters a lot). This scheme minimizes the runtime
+/// overhead of the packing for the first three hashes (we just need to mask
+/// off the upper bits) at the expense of making the fourth somewhat more
+/// complicated to assemble, because we often bail out before checking all the
+/// hashes.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AncestorHashes {
+ pub packed_hashes: [u32; 3],
+}
+
+pub(crate) fn collect_selector_hashes<'a, Impl: SelectorImpl, Iter>(
+ iter: Iter,
+ quirks_mode: QuirksMode,
+ hashes: &mut [u32; 4],
+ len: &mut usize,
+ create_inner_iterator: fn(&'a Selector<Impl>) -> Iter,
+) -> bool
+where
+ Iter: Iterator<Item = &'a Component<Impl>>,
+{
+ for component in iter {
+ let hash = match *component {
+ Component::LocalName(LocalName {
+ ref name,
+ ref lower_name,
+ }) => {
+ // Only insert the local-name into the filter if it's all
+ // lowercase. Otherwise we would need to test both hashes, and
+ // our data structures aren't really set up for that.
+ if name != lower_name {
+ continue;
+ }
+ name.precomputed_hash()
+ },
+ Component::DefaultNamespace(ref url) | Component::Namespace(_, ref url) => {
+ url.precomputed_hash()
+ },
+ // In quirks mode, class and id selectors should match
+ // case-insensitively, so just avoid inserting them into the filter.
+ Component::ID(ref id) if quirks_mode != QuirksMode::Quirks => id.precomputed_hash(),
+ Component::Class(ref class) if quirks_mode != QuirksMode::Quirks => {
+ class.precomputed_hash()
+ },
+ Component::AttributeInNoNamespace { ref local_name, .. }
+ if Impl::should_collect_attr_hash(local_name) =>
+ {
+ // AttributeInNoNamespace is only used when local_name ==
+ // local_name_lower.
+ local_name.precomputed_hash()
+ },
+ Component::AttributeInNoNamespaceExists {
+ ref local_name,
+ ref local_name_lower,
+ ..
+ } => {
+ // Only insert the local-name into the filter if it's all
+ // lowercase. Otherwise we would need to test both hashes, and
+ // our data structures aren't really set up for that.
+ if local_name != local_name_lower || !Impl::should_collect_attr_hash(local_name) {
+ continue;
+ }
+ local_name.precomputed_hash()
+ },
+ Component::AttributeOther(ref selector) => {
+ if selector.local_name != selector.local_name_lower ||
+ !Impl::should_collect_attr_hash(&selector.local_name)
+ {
+ continue;
+ }
+ selector.local_name.precomputed_hash()
+ },
+ Component::Is(ref list) | Component::Where(ref list) => {
+ // :where and :is OR their selectors, so we can't put any hash
+ // in the filter if there's more than one selector, as that'd
+ // exclude elements that may match one of the other selectors.
+ let slice = list.slice();
+ if slice.len() == 1 &&
+ !collect_selector_hashes(
+ create_inner_iterator(&slice[0]),
+ quirks_mode,
+ hashes,
+ len,
+ create_inner_iterator,
+ )
+ {
+ return false;
+ }
+ continue;
+ },
+ _ => continue,
+ };
+
+ hashes[*len] = hash & BLOOM_HASH_MASK;
+ *len += 1;
+ if *len == hashes.len() {
+ return false;
+ }
+ }
+ true
+}
+
+fn collect_ancestor_hashes<Impl: SelectorImpl>(
+ iter: SelectorIter<Impl>,
+ quirks_mode: QuirksMode,
+ hashes: &mut [u32; 4],
+ len: &mut usize,
+) {
+ collect_selector_hashes(AncestorIter::new(iter), quirks_mode, hashes, len, |s| {
+ AncestorIter(s.iter())
+ });
+}
+
+impl AncestorHashes {
+ pub fn new<Impl: SelectorImpl>(selector: &Selector<Impl>, quirks_mode: QuirksMode) -> Self {
+ // Compute ancestor hashes for the bloom filter.
+ let mut hashes = [0u32; 4];
+ let mut len = 0;
+ collect_ancestor_hashes(selector.iter(), quirks_mode, &mut hashes, &mut len);
+ debug_assert!(len <= 4);
+
+ // Now, pack the fourth hash (if it exists) into the upper byte of each of
+ // the other three hashes.
+ if len == 4 {
+ let fourth = hashes[3];
+ hashes[0] |= (fourth & 0x000000ff) << 24;
+ hashes[1] |= (fourth & 0x0000ff00) << 16;
+ hashes[2] |= (fourth & 0x00ff0000) << 8;
+ }
+
+ AncestorHashes {
+ packed_hashes: [hashes[0], hashes[1], hashes[2]],
+ }
+ }
+
+ /// Returns the fourth hash, reassembled from parts.
+ pub fn fourth_hash(&self) -> u32 {
+ ((self.packed_hashes[0] & 0xff000000) >> 24) |
+ ((self.packed_hashes[1] & 0xff000000) >> 16) |
+ ((self.packed_hashes[2] & 0xff000000) >> 8)
+ }
+}
+
+#[inline]
+pub fn namespace_empty_string<Impl: SelectorImpl>() -> Impl::NamespaceUrl {
+ // Rust type’s default, not default namespace
+ Impl::NamespaceUrl::default()
+}
+
+type SelectorData<Impl> = ThinArc<SpecificityAndFlags, Component<Impl>>;
+
+/// A Selector stores a sequence of simple selectors and combinators. The
+/// iterator classes allow callers to iterate at either the raw sequence level or
+/// at the level of sequences of simple selectors separated by combinators. Most
+/// callers want the higher-level iterator.
+///
+/// We store compound selectors internally right-to-left (in matching order).
+/// Additionally, we invert the order of top-level compound selectors so that
+/// each one matches left-to-right. This is because matching namespace, local name,
+/// id, and class are all relatively cheap, whereas matching pseudo-classes might
+/// be expensive (depending on the pseudo-class). Since authors tend to put the
+/// pseudo-classes on the right, it's faster to start matching on the left.
+///
+/// This reordering doesn't change the semantics of selector matching, and we
+/// handle it in to_css to make it invisible to serialization.
+#[derive(Clone, Eq, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+#[repr(transparent)]
+pub struct Selector<Impl: SelectorImpl>(#[shmem(field_bound)] SelectorData<Impl>);
+
+impl<Impl: SelectorImpl> Selector<Impl> {
+ /// See Arc::mark_as_intentionally_leaked
+ pub fn mark_as_intentionally_leaked(&self) {
+ self.0.mark_as_intentionally_leaked()
+ }
+
+ fn ampersand() -> Self {
+ Self(ThinArc::from_header_and_iter(
+ SpecificityAndFlags {
+ specificity: 0,
+ flags: SelectorFlags::HAS_PARENT,
+ },
+ std::iter::once(Component::ParentSelector),
+ ))
+ }
+
+ #[inline]
+ pub fn specificity(&self) -> u32 {
+ self.0.header.specificity
+ }
+
+ #[inline]
+ pub(crate) fn flags(&self) -> SelectorFlags {
+ self.0.header.flags
+ }
+
+ #[inline]
+ pub fn has_pseudo_element(&self) -> bool {
+ self.flags().intersects(SelectorFlags::HAS_PSEUDO)
+ }
+
+ #[inline]
+ pub fn has_parent_selector(&self) -> bool {
+ self.flags().intersects(SelectorFlags::HAS_PARENT)
+ }
+
+ #[inline]
+ pub fn is_slotted(&self) -> bool {
+ self.flags().intersects(SelectorFlags::HAS_SLOTTED)
+ }
+
+ #[inline]
+ pub fn is_part(&self) -> bool {
+ self.flags().intersects(SelectorFlags::HAS_PART)
+ }
+
+ #[inline]
+ pub fn parts(&self) -> Option<&[Impl::Identifier]> {
+ if !self.is_part() {
+ return None;
+ }
+
+ let mut iter = self.iter();
+ if self.has_pseudo_element() {
+ // Skip the pseudo-element.
+ for _ in &mut iter {}
+
+ let combinator = iter.next_sequence()?;
+ debug_assert_eq!(combinator, Combinator::PseudoElement);
+ }
+
+ for component in iter {
+ if let Component::Part(ref part) = *component {
+ return Some(part);
+ }
+ }
+
+ debug_assert!(false, "is_part() lied somehow?");
+ None
+ }
+
+ #[inline]
+ pub fn pseudo_element(&self) -> Option<&Impl::PseudoElement> {
+ if !self.has_pseudo_element() {
+ return None;
+ }
+
+ for component in self.iter() {
+ if let Component::PseudoElement(ref pseudo) = *component {
+ return Some(pseudo);
+ }
+ }
+
+ debug_assert!(false, "has_pseudo_element lied!");
+ None
+ }
+
+ /// Whether this selector (pseudo-element part excluded) matches every element.
+ ///
+ /// Used for "pre-computed" pseudo-elements in components/style/stylist.rs
+ #[inline]
+ pub fn is_universal(&self) -> bool {
+ self.iter_raw_match_order().all(|c| {
+ matches!(
+ *c,
+ Component::ExplicitUniversalType |
+ Component::ExplicitAnyNamespace |
+ Component::Combinator(Combinator::PseudoElement) |
+ Component::PseudoElement(..)
+ )
+ })
+ }
+
+ /// Returns an iterator over this selector in matching order (right-to-left).
+ /// When a combinator is reached, the iterator will return None, and
+ /// next_sequence() may be called to continue to the next sequence.
+ #[inline]
+ pub fn iter(&self) -> SelectorIter<Impl> {
+ SelectorIter {
+ iter: self.iter_raw_match_order(),
+ next_combinator: None,
+ }
+ }
+
+ /// Same as `iter()`, but skips `RelativeSelectorAnchor` and its associated combinator.
+ #[inline]
+ pub fn iter_skip_relative_selector_anchor(&self) -> SelectorIter<Impl> {
+ if cfg!(debug_assertions) {
+ let mut selector_iter = self.iter_raw_parse_order_from(0);
+ assert!(
+ matches!(
+ selector_iter.next().unwrap(),
+ Component::RelativeSelectorAnchor
+ ),
+ "Relative selector does not start with RelativeSelectorAnchor"
+ );
+ assert!(
+ selector_iter.next().unwrap().is_combinator(),
+ "Relative combinator does not exist"
+ );
+ }
+
+ SelectorIter {
+ iter: self.0.slice()[..self.len() - 2].iter(),
+ next_combinator: None,
+ }
+ }
+
+ /// Whether this selector is a featureless :host selector, with no combinators to the left, and
+ /// optionally has a pseudo-element to the right.
+ #[inline]
+ pub fn is_featureless_host_selector_or_pseudo_element(&self) -> bool {
+ let flags = self.flags();
+ flags.intersects(SelectorFlags::HAS_HOST) &&
+ !flags.intersects(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT)
+ }
+
+ /// Returns an iterator over this selector in matching order (right-to-left),
+ /// skipping the rightmost |offset| Components.
+ #[inline]
+ pub fn iter_from(&self, offset: usize) -> SelectorIter<Impl> {
+ let iter = self.0.slice()[offset..].iter();
+ SelectorIter {
+ iter,
+ next_combinator: None,
+ }
+ }
+
+ /// Returns the combinator at index `index` (zero-indexed from the right),
+ /// or panics if the component is not a combinator.
+ #[inline]
+ pub fn combinator_at_match_order(&self, index: usize) -> Combinator {
+ match self.0.slice()[index] {
+ Component::Combinator(c) => c,
+ ref other => panic!(
+ "Not a combinator: {:?}, {:?}, index: {}",
+ other, self, index
+ ),
+ }
+ }
+
+ /// Returns an iterator over the entire sequence of simple selectors and
+ /// combinators, in matching order (from right to left).
+ #[inline]
+ pub fn iter_raw_match_order(&self) -> slice::Iter<Component<Impl>> {
+ self.0.slice().iter()
+ }
+
+ /// Returns the combinator at index `index` (zero-indexed from the left),
+ /// or panics if the component is not a combinator.
+ #[inline]
+ pub fn combinator_at_parse_order(&self, index: usize) -> Combinator {
+ match self.0.slice()[self.len() - index - 1] {
+ Component::Combinator(c) => c,
+ ref other => panic!(
+ "Not a combinator: {:?}, {:?}, index: {}",
+ other, self, index
+ ),
+ }
+ }
+
+ /// Returns an iterator over the sequence of simple selectors and
+ /// combinators, in parse order (from left to right), starting from
+ /// `offset`.
+ #[inline]
+ pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev<slice::Iter<Component<Impl>>> {
+ self.0.slice()[..self.len() - offset].iter().rev()
+ }
+
+ /// Creates a Selector from a vec of Components, specified in parse order. Used in tests.
+ #[allow(dead_code)]
+ pub(crate) fn from_vec(
+ vec: Vec<Component<Impl>>,
+ specificity: u32,
+ flags: SelectorFlags,
+ ) -> Self {
+ let mut builder = SelectorBuilder::default();
+ for component in vec.into_iter() {
+ if let Some(combinator) = component.as_combinator() {
+ builder.push_combinator(combinator);
+ } else {
+ builder.push_simple_selector(component);
+ }
+ }
+ let spec = SpecificityAndFlags { specificity, flags };
+ Selector(builder.build_with_specificity_and_flags(spec))
+ }
+
+ #[inline]
+ fn into_data(self) -> SelectorData<Impl> {
+ self.0
+ }
+
+ pub fn replace_parent_selector(&self, parent: &SelectorList<Impl>) -> Self {
+ let parent_specificity_and_flags =
+ selector_list_specificity_and_flags(parent.slice().iter());
+
+ let mut specificity = Specificity::from(self.specificity());
+ let mut flags = self.flags() - SelectorFlags::HAS_PARENT;
+
+ fn replace_parent_on_selector_list<Impl: SelectorImpl>(
+ orig: &[Selector<Impl>],
+ parent: &SelectorList<Impl>,
+ specificity: &mut Specificity,
+ flags: &mut SelectorFlags,
+ propagate_specificity: bool,
+ flags_to_propagate: SelectorFlags,
+ ) -> Option<SelectorList<Impl>> {
+ if !orig.iter().any(|s| s.has_parent_selector()) {
+ return None;
+ }
+
+ let result = SelectorList::from_iter(orig.iter().map(|s| {
+ if !s.has_parent_selector() {
+ return s.clone();
+ }
+ s.replace_parent_selector(parent)
+ }));
+
+ let result_specificity_and_flags =
+ selector_list_specificity_and_flags(result.slice().iter());
+ if propagate_specificity {
+ *specificity += Specificity::from(
+ result_specificity_and_flags.specificity -
+ selector_list_specificity_and_flags(orig.iter()).specificity,
+ );
+ }
+ flags.insert(
+ result_specificity_and_flags
+ .flags
+ .intersection(flags_to_propagate),
+ );
+ Some(result)
+ }
+
+ fn replace_parent_on_relative_selector_list<Impl: SelectorImpl>(
+ orig: &[RelativeSelector<Impl>],
+ parent: &SelectorList<Impl>,
+ specificity: &mut Specificity,
+ flags: &mut SelectorFlags,
+ flags_to_propagate: SelectorFlags,
+ ) -> Vec<RelativeSelector<Impl>> {
+ let mut any = false;
+
+ let result = orig
+ .iter()
+ .map(|s| {
+ if !s.selector.has_parent_selector() {
+ return s.clone();
+ }
+ any = true;
+ RelativeSelector {
+ match_hint: s.match_hint,
+ selector: s.selector.replace_parent_selector(parent),
+ }
+ })
+ .collect();
+
+ if !any {
+ return result;
+ }
+
+ let result_specificity_and_flags =
+ relative_selector_list_specificity_and_flags(&result);
+ flags.insert(
+ result_specificity_and_flags
+ .flags
+ .intersection(flags_to_propagate),
+ );
+ *specificity += Specificity::from(
+ result_specificity_and_flags.specificity -
+ relative_selector_list_specificity_and_flags(orig).specificity,
+ );
+ result
+ }
+
+ fn replace_parent_on_selector<Impl: SelectorImpl>(
+ orig: &Selector<Impl>,
+ parent: &SelectorList<Impl>,
+ specificity: &mut Specificity,
+ flags: &mut SelectorFlags,
+ flags_to_propagate: SelectorFlags,
+ ) -> Selector<Impl> {
+ if !orig.has_parent_selector() {
+ return orig.clone();
+ }
+ let new_selector = orig.replace_parent_selector(parent);
+ *specificity += Specificity::from(new_selector.specificity() - orig.specificity());
+ flags.insert(new_selector.flags().intersection(flags_to_propagate));
+ new_selector
+ }
+
+ let mut items = if !self.has_parent_selector() {
+ // Implicit `&` plus descendant combinator.
+ let iter = self.iter_raw_match_order();
+ let len = iter.len() + 2;
+ specificity += Specificity::from(parent_specificity_and_flags.specificity);
+ flags.insert(
+ parent_specificity_and_flags
+ .flags
+ .intersection(SelectorFlags::for_nesting()),
+ );
+ let iter = iter
+ .cloned()
+ .chain(std::iter::once(Component::Combinator(
+ Combinator::Descendant,
+ )))
+ .chain(std::iter::once(Component::Is(parent.clone())));
+ UniqueArc::from_header_and_iter_with_size(Default::default(), iter, len)
+ } else {
+ let iter = self.iter_raw_match_order().map(|component| {
+ use self::Component::*;
+ match *component {
+ LocalName(..) |
+ ID(..) |
+ Class(..) |
+ AttributeInNoNamespaceExists { .. } |
+ AttributeInNoNamespace { .. } |
+ AttributeOther(..) |
+ ExplicitUniversalType |
+ ExplicitAnyNamespace |
+ ExplicitNoNamespace |
+ DefaultNamespace(..) |
+ Namespace(..) |
+ Root |
+ Empty |
+ Scope |
+ Nth(..) |
+ NonTSPseudoClass(..) |
+ PseudoElement(..) |
+ Combinator(..) |
+ Host(None) |
+ Part(..) |
+ Invalid(..) |
+ RelativeSelectorAnchor => component.clone(),
+ ParentSelector => {
+ specificity += Specificity::from(parent_specificity_and_flags.specificity);
+ flags.insert(
+ parent_specificity_and_flags
+ .flags
+ .intersection(SelectorFlags::for_nesting()),
+ );
+ Is(parent.clone())
+ },
+ Negation(ref selectors) => {
+ Negation(
+ replace_parent_on_selector_list(
+ selectors.slice(),
+ parent,
+ &mut specificity,
+ &mut flags,
+ /* propagate_specificity = */ true,
+ SelectorFlags::for_nesting(),
+ )
+ .unwrap_or_else(|| selectors.clone()),
+ )
+ },
+ Is(ref selectors) => {
+ Is(replace_parent_on_selector_list(
+ selectors.slice(),
+ parent,
+ &mut specificity,
+ &mut flags,
+ /* propagate_specificity = */ true,
+ SelectorFlags::for_nesting(),
+ )
+ .unwrap_or_else(|| selectors.clone()))
+ },
+ Where(ref selectors) => {
+ Where(
+ replace_parent_on_selector_list(
+ selectors.slice(),
+ parent,
+ &mut specificity,
+ &mut flags,
+ /* propagate_specificity = */ false,
+ SelectorFlags::for_nesting(),
+ )
+ .unwrap_or_else(|| selectors.clone()),
+ )
+ },
+ Has(ref selectors) => Has(replace_parent_on_relative_selector_list(
+ selectors,
+ parent,
+ &mut specificity,
+ &mut flags,
+ SelectorFlags::for_nesting(),
+ )
+ .into_boxed_slice()),
+
+ Host(Some(ref selector)) => Host(Some(replace_parent_on_selector(
+ selector,
+ parent,
+ &mut specificity,
+ &mut flags,
+ SelectorFlags::for_nesting() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ ))),
+ NthOf(ref data) => {
+ let selectors = replace_parent_on_selector_list(
+ data.selectors(),
+ parent,
+ &mut specificity,
+ &mut flags,
+ /* propagate_specificity = */ true,
+ SelectorFlags::for_nesting(),
+ );
+ NthOf(match selectors {
+ Some(s) => {
+ NthOfSelectorData::new(data.nth_data(), s.slice().iter().cloned())
+ },
+ None => data.clone(),
+ })
+ },
+ Slotted(ref selector) => Slotted(replace_parent_on_selector(
+ selector,
+ parent,
+ &mut specificity,
+ &mut flags,
+ SelectorFlags::for_nesting(),
+ )),
+ }
+ });
+ UniqueArc::from_header_and_iter(Default::default(), iter)
+ };
+ *items.header_mut() = SpecificityAndFlags {
+ specificity: specificity.into(),
+ flags,
+ };
+ Selector(items.shareable())
+ }
+
+ /// Returns count of simple selectors and combinators in the Selector.
+ #[inline]
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
+ /// Returns the address on the heap of the ThinArc for memory reporting.
+ pub fn thin_arc_heap_ptr(&self) -> *const ::std::os::raw::c_void {
+ self.0.heap_ptr()
+ }
+
+ /// Traverse selector components inside `self`.
+ ///
+ /// Implementations of this method should call `SelectorVisitor` methods
+ /// or other impls of `Visit` as appropriate based on the fields of `Self`.
+ ///
+ /// A return value of `false` indicates terminating the traversal.
+ /// It should be propagated with an early return.
+ /// On the contrary, `true` indicates that all fields of `self` have been traversed:
+ ///
+ /// ```rust,ignore
+ /// if !visitor.visit_simple_selector(&self.some_simple_selector) {
+ /// return false;
+ /// }
+ /// if !self.some_component.visit(visitor) {
+ /// return false;
+ /// }
+ /// true
+ /// ```
+ pub fn visit<V>(&self, visitor: &mut V) -> bool
+ where
+ V: SelectorVisitor<Impl = Impl>,
+ {
+ let mut current = self.iter();
+ let mut combinator = None;
+ loop {
+ if !visitor.visit_complex_selector(combinator) {
+ return false;
+ }
+
+ for selector in &mut current {
+ if !selector.visit(visitor) {
+ return false;
+ }
+ }
+
+ combinator = current.next_sequence();
+ if combinator.is_none() {
+ break;
+ }
+ }
+
+ true
+ }
+
+ /// Parse a selector, without any pseudo-element.
+ #[inline]
+ pub fn parse<'i, 't, P>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ ) -> Result<Self, ParseError<'i, P::Error>>
+ where
+ P: Parser<'i, Impl = Impl>,
+ {
+ parse_selector(
+ parser,
+ input,
+ SelectorParsingState::empty(),
+ ParseRelative::No,
+ )
+ }
+
+ pub fn new_invalid(s: &str) -> Self {
+ fn check_for_parent(input: &mut CssParser, has_parent: &mut bool) {
+ while let Ok(t) = input.next() {
+ match *t {
+ Token::Function(_) |
+ Token::ParenthesisBlock |
+ Token::CurlyBracketBlock |
+ Token::SquareBracketBlock => {
+ let _ = input.parse_nested_block(
+ |i| -> Result<(), ParseError<'_, BasicParseError>> {
+ check_for_parent(i, has_parent);
+ Ok(())
+ },
+ );
+ },
+ Token::Delim('&') => {
+ *has_parent = true;
+ },
+ _ => {},
+ }
+ if *has_parent {
+ break;
+ }
+ }
+ }
+ let mut has_parent = false;
+ {
+ let mut parser = cssparser::ParserInput::new(s);
+ let mut parser = CssParser::new(&mut parser);
+ check_for_parent(&mut parser, &mut has_parent);
+ }
+ Self(ThinArc::from_header_and_iter(
+ SpecificityAndFlags {
+ specificity: 0,
+ flags: if has_parent {
+ SelectorFlags::HAS_PARENT
+ } else {
+ SelectorFlags::empty()
+ },
+ },
+ std::iter::once(Component::Invalid(Arc::new(String::from(s.trim())))),
+ ))
+ }
+
+ /// Is the compound starting at the offset the subject compound, or referring to its pseudo-element?
+ pub fn is_rightmost(&self, offset: usize) -> bool {
+ // There can really be only one pseudo-element, and it's not really valid for anything else to
+ // follow it.
+ offset == 0 || matches!(self.combinator_at_match_order(offset - 1), Combinator::PseudoElement)
+ }
+}
+
+#[derive(Clone)]
+pub struct SelectorIter<'a, Impl: 'a + SelectorImpl> {
+ iter: slice::Iter<'a, Component<Impl>>,
+ next_combinator: Option<Combinator>,
+}
+
+impl<'a, Impl: 'a + SelectorImpl> SelectorIter<'a, Impl> {
+ /// Prepares this iterator to point to the next sequence to the left,
+ /// returning the combinator if the sequence was found.
+ #[inline]
+ pub fn next_sequence(&mut self) -> Option<Combinator> {
+ self.next_combinator.take()
+ }
+
+ /// Whether this selector is a featureless host selector, with no
+ /// combinators to the left.
+ #[inline]
+ pub(crate) fn is_featureless_host_selector(&mut self) -> bool {
+ self.selector_length() > 0 &&
+ self.all(|component| component.matches_featureless_host()) &&
+ self.next_sequence().is_none()
+ }
+
+ #[inline]
+ pub(crate) fn matches_for_stateless_pseudo_element(&mut self) -> bool {
+ let first = match self.next() {
+ Some(c) => c,
+ // Note that this is the common path that we keep inline: the
+ // pseudo-element not having anything to its right.
+ None => return true,
+ };
+ self.matches_for_stateless_pseudo_element_internal(first)
+ }
+
+ #[inline(never)]
+ fn matches_for_stateless_pseudo_element_internal(&mut self, first: &Component<Impl>) -> bool {
+ if !first.matches_for_stateless_pseudo_element() {
+ return false;
+ }
+ for component in self {
+ // The only other parser-allowed Components in this sequence are
+ // state pseudo-classes, or one of the other things that can contain
+ // them.
+ if !component.matches_for_stateless_pseudo_element() {
+ return false;
+ }
+ }
+ true
+ }
+
+ /// Returns remaining count of the simple selectors and combinators in the Selector.
+ #[inline]
+ pub fn selector_length(&self) -> usize {
+ self.iter.len()
+ }
+}
+
+impl<'a, Impl: SelectorImpl> Iterator for SelectorIter<'a, Impl> {
+ type Item = &'a Component<Impl>;
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ debug_assert!(
+ self.next_combinator.is_none(),
+ "You should call next_sequence!"
+ );
+ match *self.iter.next()? {
+ Component::Combinator(c) => {
+ self.next_combinator = Some(c);
+ None
+ },
+ ref x => Some(x),
+ }
+ }
+}
+
+impl<'a, Impl: SelectorImpl> fmt::Debug for SelectorIter<'a, Impl> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let iter = self.iter.clone().rev();
+ for component in iter {
+ component.to_css(f)?
+ }
+ Ok(())
+ }
+}
+
+/// An iterator over all combinators in a selector. Does not traverse selectors within psuedoclasses.
+struct CombinatorIter<'a, Impl: 'a + SelectorImpl>(SelectorIter<'a, Impl>);
+impl<'a, Impl: 'a + SelectorImpl> CombinatorIter<'a, Impl> {
+ fn new(inner: SelectorIter<'a, Impl>) -> Self {
+ let mut result = CombinatorIter(inner);
+ result.consume_non_combinators();
+ result
+ }
+
+ fn consume_non_combinators(&mut self) {
+ while self.0.next().is_some() {}
+ }
+}
+
+impl<'a, Impl: SelectorImpl> Iterator for CombinatorIter<'a, Impl> {
+ type Item = Combinator;
+ fn next(&mut self) -> Option<Self::Item> {
+ let result = self.0.next_sequence();
+ self.consume_non_combinators();
+ result
+ }
+}
+
+/// An iterator over all simple selectors belonging to ancestors.
+struct AncestorIter<'a, Impl: 'a + SelectorImpl>(SelectorIter<'a, Impl>);
+impl<'a, Impl: 'a + SelectorImpl> AncestorIter<'a, Impl> {
+ /// Creates an AncestorIter. The passed-in iterator is assumed to point to
+ /// the beginning of the child sequence, which will be skipped.
+ fn new(inner: SelectorIter<'a, Impl>) -> Self {
+ let mut result = AncestorIter(inner);
+ result.skip_until_ancestor();
+ result
+ }
+
+ /// Skips a sequence of simple selectors and all subsequent sequences until
+ /// a non-pseudo-element ancestor combinator is reached.
+ fn skip_until_ancestor(&mut self) {
+ loop {
+ while self.0.next().is_some() {}
+ // If this is ever changed to stop at the "pseudo-element"
+ // combinator, we will need to fix the way we compute hashes for
+ // revalidation selectors.
+ if self.0.next_sequence().map_or(true, |x| {
+ matches!(x, Combinator::Child | Combinator::Descendant)
+ }) {
+ break;
+ }
+ }
+ }
+}
+
+impl<'a, Impl: SelectorImpl> Iterator for AncestorIter<'a, Impl> {
+ type Item = &'a Component<Impl>;
+ fn next(&mut self) -> Option<Self::Item> {
+ // Grab the next simple selector in the sequence if available.
+ let next = self.0.next();
+ if next.is_some() {
+ return next;
+ }
+
+ // See if there are more sequences. If so, skip any non-ancestor sequences.
+ if let Some(combinator) = self.0.next_sequence() {
+ if !matches!(combinator, Combinator::Child | Combinator::Descendant) {
+ self.skip_until_ancestor();
+ }
+ }
+
+ self.0.next()
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)]
+pub enum Combinator {
+ Child, // >
+ Descendant, // space
+ NextSibling, // +
+ LaterSibling, // ~
+ /// A dummy combinator we use to the left of pseudo-elements.
+ ///
+ /// It serializes as the empty string, and acts effectively as a child
+ /// combinator in most cases. If we ever actually start using a child
+ /// combinator for this, we will need to fix up the way hashes are computed
+ /// for revalidation selectors.
+ PseudoElement,
+ /// Another combinator used for ::slotted(), which represent the jump from
+ /// a node to its assigned slot.
+ SlotAssignment,
+ /// Another combinator used for `::part()`, which represents the jump from
+ /// the part to the containing shadow host.
+ Part,
+}
+
+impl Combinator {
+ /// Returns true if this combinator is a child or descendant combinator.
+ #[inline]
+ pub fn is_ancestor(&self) -> bool {
+ matches!(
+ *self,
+ Combinator::Child |
+ Combinator::Descendant |
+ Combinator::PseudoElement |
+ Combinator::SlotAssignment
+ )
+ }
+
+ /// Returns true if this combinator is a pseudo-element combinator.
+ #[inline]
+ pub fn is_pseudo_element(&self) -> bool {
+ matches!(*self, Combinator::PseudoElement)
+ }
+
+ /// Returns true if this combinator is a next- or later-sibling combinator.
+ #[inline]
+ pub fn is_sibling(&self) -> bool {
+ matches!(*self, Combinator::NextSibling | Combinator::LaterSibling)
+ }
+}
+
+/// An enum for the different types of :nth- pseudoclasses
+#[derive(Copy, Clone, Eq, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+pub enum NthType {
+ Child,
+ LastChild,
+ OnlyChild,
+ OfType,
+ LastOfType,
+ OnlyOfType,
+}
+
+impl NthType {
+ pub fn is_only(self) -> bool {
+ self == Self::OnlyChild || self == Self::OnlyOfType
+ }
+
+ pub fn is_of_type(self) -> bool {
+ self == Self::OfType || self == Self::LastOfType || self == Self::OnlyOfType
+ }
+
+ pub fn is_from_end(self) -> bool {
+ self == Self::LastChild || self == Self::LastOfType
+ }
+}
+
+/// The properties that comprise an :nth- pseudoclass as of Selectors 3 (e.g.,
+/// nth-child(An+B)).
+/// https://www.w3.org/TR/selectors-3/#nth-child-pseudo
+#[derive(Copy, Clone, Eq, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+pub struct NthSelectorData {
+ pub ty: NthType,
+ pub is_function: bool,
+ pub a: i32,
+ pub b: i32,
+}
+
+impl NthSelectorData {
+ /// Returns selector data for :only-{child,of-type}
+ #[inline]
+ pub const fn only(of_type: bool) -> Self {
+ Self {
+ ty: if of_type {
+ NthType::OnlyOfType
+ } else {
+ NthType::OnlyChild
+ },
+ is_function: false,
+ a: 0,
+ b: 1,
+ }
+ }
+
+ /// Returns selector data for :first-{child,of-type}
+ #[inline]
+ pub const fn first(of_type: bool) -> Self {
+ Self {
+ ty: if of_type {
+ NthType::OfType
+ } else {
+ NthType::Child
+ },
+ is_function: false,
+ a: 0,
+ b: 1,
+ }
+ }
+
+ /// Returns selector data for :last-{child,of-type}
+ #[inline]
+ pub const fn last(of_type: bool) -> Self {
+ Self {
+ ty: if of_type {
+ NthType::LastOfType
+ } else {
+ NthType::LastChild
+ },
+ is_function: false,
+ a: 0,
+ b: 1,
+ }
+ }
+
+ /// Returns true if this is an edge selector that is not `:*-of-type``
+ #[inline]
+ pub fn is_simple_edge(&self) -> bool {
+ self.a == 0 && self.b == 1 && !self.ty.is_of_type()
+ }
+
+ /// Writes the beginning of the selector.
+ #[inline]
+ fn write_start<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
+ dest.write_str(match self.ty {
+ NthType::Child if self.is_function => ":nth-child(",
+ NthType::Child => ":first-child",
+ NthType::LastChild if self.is_function => ":nth-last-child(",
+ NthType::LastChild => ":last-child",
+ NthType::OfType if self.is_function => ":nth-of-type(",
+ NthType::OfType => ":first-of-type",
+ NthType::LastOfType if self.is_function => ":nth-last-of-type(",
+ NthType::LastOfType => ":last-of-type",
+ NthType::OnlyChild => ":only-child",
+ NthType::OnlyOfType => ":only-of-type",
+ })
+ }
+
+ /// Serialize <an+b> (part of the CSS Syntax spec, but currently only used here).
+ /// <https://drafts.csswg.org/css-syntax-3/#serialize-an-anb-value>
+ #[inline]
+ fn write_affine<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
+ match (self.a, self.b) {
+ (0, 0) => dest.write_char('0'),
+
+ (1, 0) => dest.write_char('n'),
+ (-1, 0) => dest.write_str("-n"),
+ (_, 0) => write!(dest, "{}n", self.a),
+
+ (0, _) => write!(dest, "{}", self.b),
+ (1, _) => write!(dest, "n{:+}", self.b),
+ (-1, _) => write!(dest, "-n{:+}", self.b),
+ (_, _) => write!(dest, "{}n{:+}", self.a, self.b),
+ }
+ }
+}
+
+/// The properties that comprise an :nth- pseudoclass as of Selectors 4 (e.g.,
+/// nth-child(An+B [of S]?)).
+/// https://www.w3.org/TR/selectors-4/#nth-child-pseudo
+#[derive(Clone, Eq, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+pub struct NthOfSelectorData<Impl: SelectorImpl>(
+ #[shmem(field_bound)] ThinArc<NthSelectorData, Selector<Impl>>,
+);
+
+impl<Impl: SelectorImpl> NthOfSelectorData<Impl> {
+ /// Returns selector data for :nth-{,last-}{child,of-type}(An+B [of S])
+ #[inline]
+ pub fn new<I>(nth_data: &NthSelectorData, selectors: I) -> Self
+ where
+ I: Iterator<Item = Selector<Impl>> + ExactSizeIterator,
+ {
+ Self(ThinArc::from_header_and_iter(*nth_data, selectors))
+ }
+
+ /// Returns the An+B part of the selector
+ #[inline]
+ pub fn nth_data(&self) -> &NthSelectorData {
+ &self.0.header
+ }
+
+ /// Returns the selector list part of the selector
+ #[inline]
+ pub fn selectors(&self) -> &[Selector<Impl>] {
+ self.0.slice()
+ }
+}
+
+/// Flag indicating where a given relative selector's match would be contained.
+#[derive(Clone, Copy, Eq, PartialEq, ToShmem)]
+pub enum RelativeSelectorMatchHint {
+ /// Within this element's subtree.
+ InSubtree,
+ /// Within this element's direct children.
+ InChild,
+ /// This element's next sibling.
+ InNextSibling,
+ /// Within this element's next sibling's subtree.
+ InNextSiblingSubtree,
+ /// Within this element's subsequent siblings.
+ InSibling,
+ /// Across this element's subsequent siblings and their subtrees.
+ InSiblingSubtree,
+}
+
+impl RelativeSelectorMatchHint {
+ /// Create a new relative selector match hint based on its composition.
+ pub fn new(
+ relative_combinator: Combinator,
+ has_child_or_descendants: bool,
+ has_adjacent_or_next_siblings: bool,
+ ) -> Self {
+ match relative_combinator {
+ Combinator::Descendant => RelativeSelectorMatchHint::InSubtree,
+ Combinator::Child => {
+ if !has_child_or_descendants {
+ RelativeSelectorMatchHint::InChild
+ } else {
+ // Technically, for any composition that consists of child combinators only,
+ // the search space is depth-constrained, but it's probably not worth optimizing for.
+ RelativeSelectorMatchHint::InSubtree
+ }
+ },
+ Combinator::NextSibling => {
+ if !has_child_or_descendants && !has_adjacent_or_next_siblings {
+ RelativeSelectorMatchHint::InNextSibling
+ } else if !has_child_or_descendants && has_adjacent_or_next_siblings {
+ RelativeSelectorMatchHint::InSibling
+ } else if has_child_or_descendants && !has_adjacent_or_next_siblings {
+ // Match won't cross multiple siblings.
+ RelativeSelectorMatchHint::InNextSiblingSubtree
+ } else {
+ RelativeSelectorMatchHint::InSiblingSubtree
+ }
+ },
+ Combinator::LaterSibling => {
+ if !has_child_or_descendants {
+ RelativeSelectorMatchHint::InSibling
+ } else {
+ // Even if the match may not cross multiple siblings, we have to look until
+ // we find a match anyway.
+ RelativeSelectorMatchHint::InSiblingSubtree
+ }
+ },
+ Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => {
+ debug_assert!(false, "Unexpected relative combinator");
+ RelativeSelectorMatchHint::InSubtree
+ },
+ }
+ }
+
+ /// Is the match traversal direction towards the descendant of this element (As opposed to siblings)?
+ pub fn is_descendant_direction(&self) -> bool {
+ matches!(*self, Self::InChild | Self::InSubtree)
+ }
+
+ /// Is the match traversal terminated at the next sibling?
+ pub fn is_next_sibling(&self) -> bool {
+ matches!(*self, Self::InNextSibling | Self::InNextSiblingSubtree)
+ }
+
+ /// Does the match involve matching the subtree?
+ pub fn is_subtree(&self) -> bool {
+ matches!(
+ *self,
+ Self::InSubtree | Self::InSiblingSubtree | Self::InNextSiblingSubtree
+ )
+ }
+}
+
+/// Count of combinators in a given relative selector, not traversing selectors of pseudoclasses.
+#[derive(Clone, Copy)]
+pub struct RelativeSelectorCombinatorCount {
+ relative_combinator: Combinator,
+ pub child_or_descendants: usize,
+ pub adjacent_or_next_siblings: usize,
+}
+
+impl RelativeSelectorCombinatorCount {
+ /// Create a new relative selector combinator count from a given relative selector.
+ pub fn new<Impl: SelectorImpl>(relative_selector: &RelativeSelector<Impl>) -> Self {
+ let mut result = RelativeSelectorCombinatorCount {
+ relative_combinator: relative_selector.selector.combinator_at_parse_order(1),
+ child_or_descendants: 0,
+ adjacent_or_next_siblings: 0,
+ };
+
+ for combinator in CombinatorIter::new(
+ relative_selector
+ .selector
+ .iter_skip_relative_selector_anchor(),
+ ) {
+ match combinator {
+ Combinator::Descendant | Combinator::Child => {
+ result.child_or_descendants += 1;
+ },
+ Combinator::NextSibling | Combinator::LaterSibling => {
+ result.adjacent_or_next_siblings += 1;
+ },
+ Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => {
+ continue
+ },
+ };
+ }
+ result
+ }
+
+ /// Get the match hint based on the current combinator count.
+ pub fn get_match_hint(&self) -> RelativeSelectorMatchHint {
+ RelativeSelectorMatchHint::new(
+ self.relative_combinator,
+ self.child_or_descendants != 0,
+ self.adjacent_or_next_siblings != 0,
+ )
+ }
+}
+
+/// Storage for a relative selector.
+#[derive(Clone, Eq, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+pub struct RelativeSelector<Impl: SelectorImpl> {
+ /// Match space constraining hint.
+ pub match_hint: RelativeSelectorMatchHint,
+ /// The selector. Guaranteed to contain `RelativeSelectorAnchor` and the relative combinator in parse order.
+ #[shmem(field_bound)]
+ pub selector: Selector<Impl>,
+}
+
+bitflags! {
+ /// Composition of combinators in a given selector, not traversing selectors of pseudoclasses.
+ #[derive(Clone, Debug, Eq, PartialEq)]
+ struct CombinatorComposition: u8 {
+ const DESCENDANTS = 1 << 0;
+ const SIBLINGS = 1 << 1;
+ }
+}
+
+impl CombinatorComposition {
+ fn for_relative_selector<Impl: SelectorImpl>(inner_selector: &Selector<Impl>) -> Self {
+ let mut result = CombinatorComposition::empty();
+ for combinator in CombinatorIter::new(inner_selector.iter_skip_relative_selector_anchor()) {
+ match combinator {
+ Combinator::Descendant | Combinator::Child => {
+ result.insert(Self::DESCENDANTS);
+ },
+ Combinator::NextSibling | Combinator::LaterSibling => {
+ result.insert(Self::SIBLINGS);
+ },
+ Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => {
+ continue
+ },
+ };
+ if result.is_all() {
+ break;
+ }
+ }
+ return result;
+ }
+}
+
+impl<Impl: SelectorImpl> RelativeSelector<Impl> {
+ fn from_selector_list(selector_list: SelectorList<Impl>) -> Box<[Self]> {
+ let vec: Vec<Self> = selector_list
+ .slice()
+ .iter()
+ .map(|selector| {
+ // It's more efficient to keep track of all this during the parse time, but that seems like a lot of special
+ // case handling for what it's worth.
+ if cfg!(debug_assertions) {
+ let relative_selector_anchor = selector.iter_raw_parse_order_from(0).next();
+ debug_assert!(
+ relative_selector_anchor.is_some(),
+ "Relative selector is empty"
+ );
+ debug_assert!(
+ matches!(
+ relative_selector_anchor.unwrap(),
+ Component::RelativeSelectorAnchor
+ ),
+ "Relative selector anchor is missing"
+ );
+ }
+ // Leave a hint for narrowing down the search space when we're matching.
+ let composition = CombinatorComposition::for_relative_selector(&selector);
+ let match_hint = RelativeSelectorMatchHint::new(
+ selector.combinator_at_parse_order(1),
+ composition.intersects(CombinatorComposition::DESCENDANTS),
+ composition.intersects(CombinatorComposition::SIBLINGS),
+ );
+ RelativeSelector {
+ match_hint,
+ selector: selector.clone(),
+ }
+ })
+ .collect();
+ vec.into_boxed_slice()
+ }
+}
+
+/// A CSS simple selector or combinator. We store both in the same enum for
+/// optimal packing and cache performance, see [1].
+///
+/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1357973
+#[derive(Clone, Eq, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+pub enum Component<Impl: SelectorImpl> {
+ LocalName(LocalName<Impl>),
+
+ ID(#[shmem(field_bound)] Impl::Identifier),
+ Class(#[shmem(field_bound)] Impl::Identifier),
+
+ AttributeInNoNamespaceExists {
+ #[shmem(field_bound)]
+ local_name: Impl::LocalName,
+ local_name_lower: Impl::LocalName,
+ },
+ // Used only when local_name is already lowercase.
+ AttributeInNoNamespace {
+ local_name: Impl::LocalName,
+ operator: AttrSelectorOperator,
+ #[shmem(field_bound)]
+ value: Impl::AttrValue,
+ case_sensitivity: ParsedCaseSensitivity,
+ },
+ // Use a Box in the less common cases with more data to keep size_of::<Component>() small.
+ AttributeOther(Box<AttrSelectorWithOptionalNamespace<Impl>>),
+
+ ExplicitUniversalType,
+ ExplicitAnyNamespace,
+
+ ExplicitNoNamespace,
+ DefaultNamespace(#[shmem(field_bound)] Impl::NamespaceUrl),
+ Namespace(
+ #[shmem(field_bound)] Impl::NamespacePrefix,
+ #[shmem(field_bound)] Impl::NamespaceUrl,
+ ),
+
+ /// Pseudo-classes
+ Negation(SelectorList<Impl>),
+ Root,
+ Empty,
+ Scope,
+ ParentSelector,
+ Nth(NthSelectorData),
+ NthOf(NthOfSelectorData<Impl>),
+ NonTSPseudoClass(#[shmem(field_bound)] Impl::NonTSPseudoClass),
+ /// The ::slotted() pseudo-element:
+ ///
+ /// https://drafts.csswg.org/css-scoping/#slotted-pseudo
+ ///
+ /// The selector here is a compound selector, that is, no combinators.
+ ///
+ /// NOTE(emilio): This should support a list of selectors, but as of this
+ /// writing no other browser does, and that allows them to put ::slotted()
+ /// in the rule hash, so we do that too.
+ ///
+ /// See https://github.com/w3c/csswg-drafts/issues/2158
+ Slotted(Selector<Impl>),
+ /// The `::part` pseudo-element.
+ /// https://drafts.csswg.org/css-shadow-parts/#part
+ Part(#[shmem(field_bound)] Box<[Impl::Identifier]>),
+ /// The `:host` pseudo-class:
+ ///
+ /// https://drafts.csswg.org/css-scoping/#host-selector
+ ///
+ /// NOTE(emilio): This should support a list of selectors, but as of this
+ /// writing no other browser does, and that allows them to put :host()
+ /// in the rule hash, so we do that too.
+ ///
+ /// See https://github.com/w3c/csswg-drafts/issues/2158
+ Host(Option<Selector<Impl>>),
+ /// The `:where` pseudo-class.
+ ///
+ /// https://drafts.csswg.org/selectors/#zero-matches
+ ///
+ /// The inner argument is conceptually a SelectorList, but we move the
+ /// selectors to the heap to keep Component small.
+ Where(SelectorList<Impl>),
+ /// The `:is` pseudo-class.
+ ///
+ /// https://drafts.csswg.org/selectors/#matches-pseudo
+ ///
+ /// Same comment as above re. the argument.
+ Is(SelectorList<Impl>),
+ /// The `:has` pseudo-class.
+ ///
+ /// https://drafts.csswg.org/selectors/#has-pseudo
+ ///
+ /// Same comment as above re. the argument.
+ Has(Box<[RelativeSelector<Impl>]>),
+ /// An invalid selector inside :is() / :where().
+ Invalid(Arc<String>),
+ /// An implementation-dependent pseudo-element selector.
+ PseudoElement(#[shmem(field_bound)] Impl::PseudoElement),
+
+ Combinator(Combinator),
+
+ /// Used only for relative selectors, which starts with a combinator
+ /// (With an implied descendant combinator if not specified).
+ ///
+ /// https://drafts.csswg.org/selectors-4/#typedef-relative-selector
+ RelativeSelectorAnchor,
+}
+
+impl<Impl: SelectorImpl> Component<Impl> {
+ /// Returns true if this is a combinator.
+ #[inline]
+ pub fn is_combinator(&self) -> bool {
+ matches!(*self, Component::Combinator(_))
+ }
+
+ /// Returns true if this is a :host() selector.
+ #[inline]
+ pub fn is_host(&self) -> bool {
+ matches!(*self, Component::Host(..))
+ }
+
+ /// Returns true if this is a :host() selector.
+ #[inline]
+ pub fn matches_featureless_host(&self) -> bool {
+ match *self {
+ Component::Host(..) => true,
+ Component::Where(ref l) | Component::Is(ref l) => {
+ // TODO(emilio): For now we use .all() rather than .any(), because not doing so
+ // brings up a fair amount of extra complexity (we can't make the decision on
+ // whether to walk out statically).
+ l.slice()
+ .iter()
+ .all(|i| i.is_featureless_host_selector_or_pseudo_element())
+ },
+ _ => false,
+ }
+ }
+
+ /// Returns the value as a combinator if applicable, None otherwise.
+ pub fn as_combinator(&self) -> Option<Combinator> {
+ match *self {
+ Component::Combinator(c) => Some(c),
+ _ => None,
+ }
+ }
+
+ /// Whether this component is valid after a pseudo-element. Only intended
+ /// for sanity-checking.
+ pub fn maybe_allowed_after_pseudo_element(&self) -> bool {
+ match *self {
+ Component::NonTSPseudoClass(..) => true,
+ Component::Negation(ref selectors) |
+ Component::Is(ref selectors) |
+ Component::Where(ref selectors) => selectors.slice().iter().all(|selector| {
+ selector
+ .iter_raw_match_order()
+ .all(|c| c.maybe_allowed_after_pseudo_element())
+ }),
+ _ => false,
+ }
+ }
+
+ /// Whether a given selector should match for stateless pseudo-elements.
+ ///
+ /// This is a bit subtle: Only selectors that return true in
+ /// `maybe_allowed_after_pseudo_element` should end up here, and
+ /// `NonTSPseudoClass` never matches (as it is a stateless pseudo after
+ /// all).
+ fn matches_for_stateless_pseudo_element(&self) -> bool {
+ debug_assert!(
+ self.maybe_allowed_after_pseudo_element(),
+ "Someone messed up pseudo-element parsing: {:?}",
+ *self
+ );
+ match *self {
+ Component::Negation(ref selectors) => !selectors.slice().iter().all(|selector| {
+ selector
+ .iter_raw_match_order()
+ .all(|c| c.matches_for_stateless_pseudo_element())
+ }),
+ Component::Is(ref selectors) | Component::Where(ref selectors) => {
+ selectors.slice().iter().any(|selector| {
+ selector
+ .iter_raw_match_order()
+ .all(|c| c.matches_for_stateless_pseudo_element())
+ })
+ },
+ _ => false,
+ }
+ }
+
+ pub fn visit<V>(&self, visitor: &mut V) -> bool
+ where
+ V: SelectorVisitor<Impl = Impl>,
+ {
+ use self::Component::*;
+ if !visitor.visit_simple_selector(self) {
+ return false;
+ }
+
+ match *self {
+ Slotted(ref selector) => {
+ if !selector.visit(visitor) {
+ return false;
+ }
+ },
+ Host(Some(ref selector)) => {
+ if !selector.visit(visitor) {
+ return false;
+ }
+ },
+ AttributeInNoNamespaceExists {
+ ref local_name,
+ ref local_name_lower,
+ } => {
+ if !visitor.visit_attribute_selector(
+ &NamespaceConstraint::Specific(&namespace_empty_string::<Impl>()),
+ local_name,
+ local_name_lower,
+ ) {
+ return false;
+ }
+ },
+ AttributeInNoNamespace { ref local_name, .. } => {
+ if !visitor.visit_attribute_selector(
+ &NamespaceConstraint::Specific(&namespace_empty_string::<Impl>()),
+ local_name,
+ local_name,
+ ) {
+ return false;
+ }
+ },
+ AttributeOther(ref attr_selector) => {
+ let empty_string;
+ let namespace = match attr_selector.namespace() {
+ Some(ns) => ns,
+ None => {
+ empty_string = crate::parser::namespace_empty_string::<Impl>();
+ NamespaceConstraint::Specific(&empty_string)
+ },
+ };
+ if !visitor.visit_attribute_selector(
+ &namespace,
+ &attr_selector.local_name,
+ &attr_selector.local_name_lower,
+ ) {
+ return false;
+ }
+ },
+
+ NonTSPseudoClass(ref pseudo_class) => {
+ if !pseudo_class.visit(visitor) {
+ return false;
+ }
+ },
+ Negation(ref list) | Is(ref list) | Where(ref list) => {
+ let list_kind = SelectorListKind::from_component(self);
+ debug_assert!(!list_kind.is_empty());
+ if !visitor.visit_selector_list(list_kind, list.slice()) {
+ return false;
+ }
+ },
+ NthOf(ref nth_of_data) => {
+ if !visitor.visit_selector_list(SelectorListKind::NTH_OF, nth_of_data.selectors()) {
+ return false;
+ }
+ },
+ Has(ref list) => {
+ if !visitor.visit_relative_selector_list(list) {
+ return false;
+ }
+ },
+ _ => {},
+ }
+
+ true
+ }
+
+ // Returns true if this has any selector that requires an index calculation. e.g.
+ // :nth-child, :first-child, etc. For nested selectors, return true only if the
+ // indexed selector is in its subject compound.
+ pub fn has_indexed_selector_in_subject(&self) -> bool {
+ match *self {
+ Component::NthOf(..) | Component::Nth(..) => return true,
+ Component::Is(ref selectors) |
+ Component::Where(ref selectors) |
+ Component::Negation(ref selectors) => {
+ // Check the subject compound.
+ for selector in selectors.slice() {
+ let mut iter = selector.iter();
+ while let Some(c) = iter.next() {
+ if c.has_indexed_selector_in_subject() {
+ return true;
+ }
+ }
+ }
+ },
+ _ => (),
+ };
+ false
+ }
+}
+
+#[derive(Clone, Eq, PartialEq, ToShmem)]
+#[shmem(no_bounds)]
+pub struct LocalName<Impl: SelectorImpl> {
+ #[shmem(field_bound)]
+ pub name: Impl::LocalName,
+ pub lower_name: Impl::LocalName,
+}
+
+impl<Impl: SelectorImpl> Debug for Selector<Impl> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("Selector(")?;
+ self.to_css(f)?;
+ write!(
+ f,
+ ", specificity = {:#x}, flags = {:?})",
+ self.specificity(),
+ self.flags()
+ )
+ }
+}
+
+impl<Impl: SelectorImpl> Debug for Component<Impl> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.to_css(f)
+ }
+}
+impl<Impl: SelectorImpl> Debug for AttrSelectorWithOptionalNamespace<Impl> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.to_css(f)
+ }
+}
+impl<Impl: SelectorImpl> Debug for LocalName<Impl> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.to_css(f)
+ }
+}
+
+fn serialize_selector_list<'a, Impl, I, W>(iter: I, dest: &mut W) -> fmt::Result
+where
+ Impl: SelectorImpl,
+ I: Iterator<Item = &'a Selector<Impl>>,
+ W: fmt::Write,
+{
+ let mut first = true;
+ for selector in iter {
+ if !first {
+ dest.write_str(", ")?;
+ }
+ first = false;
+ selector.to_css(dest)?;
+ }
+ Ok(())
+}
+
+impl<Impl: SelectorImpl> ToCss for SelectorList<Impl> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ serialize_selector_list(self.slice().iter(), dest)
+ }
+}
+
+impl<Impl: SelectorImpl> ToCss for Selector<Impl> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ // Compound selectors invert the order of their contents, so we need to
+ // undo that during serialization.
+ //
+ // This two-iterator strategy involves walking over the selector twice.
+ // We could do something more clever, but selector serialization probably
+ // isn't hot enough to justify it, and the stringification likely
+ // dominates anyway.
+ //
+ // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(),
+ // which we need for |split|. So we split by combinators on a match-order
+ // sequence and then reverse.
+
+ let mut combinators = self
+ .iter_raw_match_order()
+ .rev()
+ .filter_map(|x| x.as_combinator());
+ let compound_selectors = self
+ .iter_raw_match_order()
+ .as_slice()
+ .split(|x| x.is_combinator())
+ .rev();
+
+ let mut combinators_exhausted = false;
+ for compound in compound_selectors {
+ debug_assert!(!combinators_exhausted);
+
+ // https://drafts.csswg.org/cssom/#serializing-selectors
+ if compound.is_empty() {
+ continue;
+ }
+ if let Component::RelativeSelectorAnchor = compound.first().unwrap() {
+ debug_assert!(
+ compound.len() == 1,
+ "RelativeLeft should only be a simple selector"
+ );
+ combinators.next().unwrap().to_css_relative(dest)?;
+ continue;
+ }
+
+ // 1. If there is only one simple selector in the compound selectors
+ // which is a universal selector, append the result of
+ // serializing the universal selector to s.
+ //
+ // Check if `!compound.empty()` first--this can happen if we have
+ // something like `... > ::before`, because we store `>` and `::`
+ // both as combinators internally.
+ //
+ // If we are in this case, after we have serialized the universal
+ // selector, we skip Step 2 and continue with the algorithm.
+ let (can_elide_namespace, first_non_namespace) = match compound[0] {
+ Component::ExplicitAnyNamespace |
+ Component::ExplicitNoNamespace |
+ Component::Namespace(..) => (false, 1),
+ Component::DefaultNamespace(..) => (true, 1),
+ _ => (true, 0),
+ };
+ let mut perform_step_2 = true;
+ let next_combinator = combinators.next();
+ if first_non_namespace == compound.len() - 1 {
+ match (next_combinator, &compound[first_non_namespace]) {
+ // We have to be careful here, because if there is a
+ // pseudo element "combinator" there isn't really just
+ // the one simple selector. Technically this compound
+ // selector contains the pseudo element selector as well
+ // -- Combinator::PseudoElement, just like
+ // Combinator::SlotAssignment, don't exist in the
+ // spec.
+ (Some(Combinator::PseudoElement), _) |
+ (Some(Combinator::SlotAssignment), _) => (),
+ (_, &Component::ExplicitUniversalType) => {
+ // Iterate over everything so we serialize the namespace
+ // too.
+ for simple in compound.iter() {
+ simple.to_css(dest)?;
+ }
+ // Skip step 2, which is an "otherwise".
+ perform_step_2 = false;
+ },
+ _ => (),
+ }
+ }
+
+ // 2. Otherwise, for each simple selector in the compound selectors
+ // that is not a universal selector of which the namespace prefix
+ // maps to a namespace that is not the default namespace
+ // serialize the simple selector and append the result to s.
+ //
+ // See https://github.com/w3c/csswg-drafts/issues/1606, which is
+ // proposing to change this to match up with the behavior asserted
+ // in cssom/serialize-namespaced-type-selectors.html, which the
+ // following code tries to match.
+ if perform_step_2 {
+ for simple in compound.iter() {
+ if let Component::ExplicitUniversalType = *simple {
+ // Can't have a namespace followed by a pseudo-element
+ // selector followed by a universal selector in the same
+ // compound selector, so we don't have to worry about the
+ // real namespace being in a different `compound`.
+ if can_elide_namespace {
+ continue;
+ }
+ }
+ simple.to_css(dest)?;
+ }
+ }
+
+ // 3. If this is not the last part of the chain of the selector
+ // append a single SPACE (U+0020), followed by the combinator
+ // ">", "+", "~", ">>", "||", as appropriate, followed by another
+ // single SPACE (U+0020) if the combinator was not whitespace, to
+ // s.
+ match next_combinator {
+ Some(c) => c.to_css(dest)?,
+ None => combinators_exhausted = true,
+ };
+
+ // 4. If this is the last part of the chain of the selector and
+ // there is a pseudo-element, append "::" followed by the name of
+ // the pseudo-element, to s.
+ //
+ // (we handle this above)
+ }
+
+ Ok(())
+ }
+}
+
+impl Combinator {
+ fn to_css_internal<W>(&self, dest: &mut W, prefix_space: bool) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ if matches!(
+ *self,
+ Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment
+ ) {
+ return Ok(());
+ }
+ if prefix_space {
+ dest.write_char(' ')?;
+ }
+ match *self {
+ Combinator::Child => dest.write_str("> "),
+ Combinator::Descendant => Ok(()),
+ Combinator::NextSibling => dest.write_str("+ "),
+ Combinator::LaterSibling => dest.write_str("~ "),
+ Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => unsafe {
+ debug_unreachable!("Already handled")
+ },
+ }
+ }
+
+ fn to_css_relative<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.to_css_internal(dest, false)
+ }
+}
+
+impl ToCss for Combinator {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.to_css_internal(dest, true)
+ }
+}
+
+impl<Impl: SelectorImpl> ToCss for Component<Impl> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use self::Component::*;
+
+ match *self {
+ Combinator(ref c) => c.to_css(dest),
+ Slotted(ref selector) => {
+ dest.write_str("::slotted(")?;
+ selector.to_css(dest)?;
+ dest.write_char(')')
+ },
+ Part(ref part_names) => {
+ dest.write_str("::part(")?;
+ for (i, name) in part_names.iter().enumerate() {
+ if i != 0 {
+ dest.write_char(' ')?;
+ }
+ name.to_css(dest)?;
+ }
+ dest.write_char(')')
+ },
+ PseudoElement(ref p) => p.to_css(dest),
+ ID(ref s) => {
+ dest.write_char('#')?;
+ s.to_css(dest)
+ },
+ Class(ref s) => {
+ dest.write_char('.')?;
+ s.to_css(dest)
+ },
+ LocalName(ref s) => s.to_css(dest),
+ ExplicitUniversalType => dest.write_char('*'),
+
+ DefaultNamespace(_) => Ok(()),
+ ExplicitNoNamespace => dest.write_char('|'),
+ ExplicitAnyNamespace => dest.write_str("*|"),
+ Namespace(ref prefix, _) => {
+ prefix.to_css(dest)?;
+ dest.write_char('|')
+ },
+
+ AttributeInNoNamespaceExists { ref local_name, .. } => {
+ dest.write_char('[')?;
+ local_name.to_css(dest)?;
+ dest.write_char(']')
+ },
+ AttributeInNoNamespace {
+ ref local_name,
+ operator,
+ ref value,
+ case_sensitivity,
+ ..
+ } => {
+ dest.write_char('[')?;
+ local_name.to_css(dest)?;
+ operator.to_css(dest)?;
+ value.to_css(dest)?;
+ match case_sensitivity {
+ ParsedCaseSensitivity::CaseSensitive |
+ ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {},
+ ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?,
+ ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?,
+ }
+ dest.write_char(']')
+ },
+ AttributeOther(ref attr_selector) => attr_selector.to_css(dest),
+
+ // Pseudo-classes
+ Root => dest.write_str(":root"),
+ Empty => dest.write_str(":empty"),
+ Scope => dest.write_str(":scope"),
+ ParentSelector => dest.write_char('&'),
+ Host(ref selector) => {
+ dest.write_str(":host")?;
+ if let Some(ref selector) = *selector {
+ dest.write_char('(')?;
+ selector.to_css(dest)?;
+ dest.write_char(')')?;
+ }
+ Ok(())
+ },
+ Nth(ref nth_data) => {
+ nth_data.write_start(dest)?;
+ if nth_data.is_function {
+ nth_data.write_affine(dest)?;
+ dest.write_char(')')?;
+ }
+ Ok(())
+ },
+ NthOf(ref nth_of_data) => {
+ let nth_data = nth_of_data.nth_data();
+ nth_data.write_start(dest)?;
+ debug_assert!(
+ nth_data.is_function,
+ "A selector must be a function to hold An+B notation"
+ );
+ nth_data.write_affine(dest)?;
+ debug_assert!(
+ matches!(nth_data.ty, NthType::Child | NthType::LastChild),
+ "Only :nth-child or :nth-last-child can be of a selector list"
+ );
+ debug_assert!(
+ !nth_of_data.selectors().is_empty(),
+ "The selector list should not be empty"
+ );
+ dest.write_str(" of ")?;
+ serialize_selector_list(nth_of_data.selectors().iter(), dest)?;
+ dest.write_char(')')
+ },
+ Is(ref list) | Where(ref list) | Negation(ref list) => {
+ match *self {
+ Where(..) => dest.write_str(":where(")?,
+ Is(..) => dest.write_str(":is(")?,
+ Negation(..) => dest.write_str(":not(")?,
+ _ => unreachable!(),
+ }
+ serialize_selector_list(list.slice().iter(), dest)?;
+ dest.write_str(")")
+ },
+ Has(ref list) => {
+ dest.write_str(":has(")?;
+ let mut first = true;
+ for RelativeSelector { ref selector, .. } in list.iter() {
+ if !first {
+ dest.write_str(", ")?;
+ }
+ first = false;
+ selector.to_css(dest)?;
+ }
+ dest.write_str(")")
+ },
+ NonTSPseudoClass(ref pseudo) => pseudo.to_css(dest),
+ Invalid(ref css) => dest.write_str(css),
+ RelativeSelectorAnchor => Ok(()),
+ }
+ }
+}
+
+impl<Impl: SelectorImpl> ToCss for AttrSelectorWithOptionalNamespace<Impl> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_char('[')?;
+ match self.namespace {
+ Some(NamespaceConstraint::Specific((ref prefix, _))) => {
+ prefix.to_css(dest)?;
+ dest.write_char('|')?
+ },
+ Some(NamespaceConstraint::Any) => dest.write_str("*|")?,
+ None => {},
+ }
+ self.local_name.to_css(dest)?;
+ match self.operation {
+ ParsedAttrSelectorOperation::Exists => {},
+ ParsedAttrSelectorOperation::WithValue {
+ operator,
+ case_sensitivity,
+ ref value,
+ } => {
+ operator.to_css(dest)?;
+ value.to_css(dest)?;
+ match case_sensitivity {
+ ParsedCaseSensitivity::CaseSensitive |
+ ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {},
+ ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?,
+ ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?,
+ }
+ },
+ }
+ dest.write_char(']')
+ }
+}
+
+impl<Impl: SelectorImpl> ToCss for LocalName<Impl> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.name.to_css(dest)
+ }
+}
+
+/// Build up a Selector.
+/// selector : simple_selector_sequence [ combinator simple_selector_sequence ]* ;
+///
+/// `Err` means invalid selector.
+fn parse_selector<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ mut state: SelectorParsingState,
+ parse_relative: ParseRelative,
+) -> Result<Selector<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ let mut builder = SelectorBuilder::default();
+
+ // Helps rewind less, but also simplifies dealing with relative combinators below.
+ input.skip_whitespace();
+
+ if parse_relative != ParseRelative::No {
+ let combinator = try_parse_combinator::<P, Impl>(input);
+ match parse_relative {
+ ParseRelative::ForHas => {
+ builder.push_simple_selector(Component::RelativeSelectorAnchor);
+ // Do we see a combinator? If so, push that. Otherwise, push a descendant
+ // combinator.
+ builder.push_combinator(combinator.unwrap_or(Combinator::Descendant));
+ },
+ ParseRelative::ForNesting => {
+ if let Ok(combinator) = combinator {
+ builder.push_simple_selector(Component::ParentSelector);
+ builder.push_combinator(combinator);
+ }
+ },
+ ParseRelative::No => unreachable!(),
+ }
+ }
+ 'outer_loop: loop {
+ // Parse a sequence of simple selectors.
+ let empty = parse_compound_selector(parser, &mut state, input, &mut builder)?;
+ if empty {
+ return Err(input.new_custom_error(if builder.has_combinators() {
+ SelectorParseErrorKind::DanglingCombinator
+ } else {
+ SelectorParseErrorKind::EmptySelector
+ }));
+ }
+
+ if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+ debug_assert!(state.intersects(
+ SelectorParsingState::AFTER_PSEUDO_ELEMENT |
+ SelectorParsingState::AFTER_SLOTTED |
+ SelectorParsingState::AFTER_PART
+ ));
+ break;
+ }
+
+ let combinator = if let Ok(c) = try_parse_combinator::<P, Impl>(input) {
+ c
+ } else {
+ break 'outer_loop;
+ };
+
+ if !state.allows_combinators() {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+
+ builder.push_combinator(combinator);
+ }
+ return Ok(Selector(builder.build()));
+}
+
+fn try_parse_combinator<'i, 't, P, Impl>(input: &mut CssParser<'i, 't>) -> Result<Combinator, ()> {
+ let mut any_whitespace = false;
+ loop {
+ let before_this_token = input.state();
+ match input.next_including_whitespace() {
+ Err(_e) => return Err(()),
+ Ok(&Token::WhiteSpace(_)) => any_whitespace = true,
+ Ok(&Token::Delim('>')) => {
+ return Ok(Combinator::Child);
+ },
+ Ok(&Token::Delim('+')) => {
+ return Ok(Combinator::NextSibling);
+ },
+ Ok(&Token::Delim('~')) => {
+ return Ok(Combinator::LaterSibling);
+ },
+ Ok(_) => {
+ input.reset(&before_this_token);
+ if any_whitespace {
+ return Ok(Combinator::Descendant);
+ } else {
+ return Err(());
+ }
+ },
+ }
+ }
+}
+
+/// * `Err(())`: Invalid selector, abort
+/// * `Ok(false)`: Not a type selector, could be something else. `input` was not consumed.
+/// * `Ok(true)`: Length 0 (`*|*`), 1 (`*|E` or `ns|*`) or 2 (`|E` or `ns|E`)
+fn parse_type_selector<'i, 't, P, Impl, S>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ state: SelectorParsingState,
+ sink: &mut S,
+) -> Result<bool, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+ S: Push<Component<Impl>>,
+{
+ match parse_qualified_name(parser, input, /* in_attr_selector = */ false) {
+ Err(ParseError {
+ kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput),
+ ..
+ }) |
+ Ok(OptionalQName::None(_)) => Ok(false),
+ Ok(OptionalQName::Some(namespace, local_name)) => {
+ if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ match namespace {
+ QNamePrefix::ImplicitAnyNamespace => {},
+ QNamePrefix::ImplicitDefaultNamespace(url) => {
+ sink.push(Component::DefaultNamespace(url))
+ },
+ QNamePrefix::ExplicitNamespace(prefix, url) => {
+ sink.push(match parser.default_namespace() {
+ Some(ref default_url) if url == *default_url => {
+ Component::DefaultNamespace(url)
+ },
+ _ => Component::Namespace(prefix, url),
+ })
+ },
+ QNamePrefix::ExplicitNoNamespace => sink.push(Component::ExplicitNoNamespace),
+ QNamePrefix::ExplicitAnyNamespace => {
+ match parser.default_namespace() {
+ // Element type selectors that have no namespace
+ // component (no namespace separator) represent elements
+ // without regard to the element's namespace (equivalent
+ // to "*|") unless a default namespace has been declared
+ // for namespaced selectors (e.g. in CSS, in the style
+ // sheet). If a default namespace has been declared,
+ // such selectors will represent only elements in the
+ // default namespace.
+ // -- Selectors § 6.1.1
+ // So we'll have this act the same as the
+ // QNamePrefix::ImplicitAnyNamespace case.
+ None => {},
+ Some(_) => sink.push(Component::ExplicitAnyNamespace),
+ }
+ },
+ QNamePrefix::ImplicitNoNamespace => {
+ unreachable!() // Not returned with in_attr_selector = false
+ },
+ }
+ match local_name {
+ Some(name) => sink.push(Component::LocalName(LocalName {
+ lower_name: to_ascii_lowercase(&name).as_ref().into(),
+ name: name.as_ref().into(),
+ })),
+ None => sink.push(Component::ExplicitUniversalType),
+ }
+ Ok(true)
+ },
+ Err(e) => Err(e),
+ }
+}
+
+#[derive(Debug)]
+enum SimpleSelectorParseResult<Impl: SelectorImpl> {
+ SimpleSelector(Component<Impl>),
+ PseudoElement(Impl::PseudoElement),
+ SlottedPseudo(Selector<Impl>),
+ PartPseudo(Box<[Impl::Identifier]>),
+}
+
+#[derive(Debug)]
+enum QNamePrefix<Impl: SelectorImpl> {
+ ImplicitNoNamespace, // `foo` in attr selectors
+ ImplicitAnyNamespace, // `foo` in type selectors, without a default ns
+ ImplicitDefaultNamespace(Impl::NamespaceUrl), // `foo` in type selectors, with a default ns
+ ExplicitNoNamespace, // `|foo`
+ ExplicitAnyNamespace, // `*|foo`
+ ExplicitNamespace(Impl::NamespacePrefix, Impl::NamespaceUrl), // `prefix|foo`
+}
+
+enum OptionalQName<'i, Impl: SelectorImpl> {
+ Some(QNamePrefix<Impl>, Option<CowRcStr<'i>>),
+ None(Token<'i>),
+}
+
+/// * `Err(())`: Invalid selector, abort
+/// * `Ok(None(token))`: Not a simple selector, could be something else. `input` was not consumed,
+/// but the token is still returned.
+/// * `Ok(Some(namespace, local_name))`: `None` for the local name means a `*` universal selector
+fn parse_qualified_name<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ in_attr_selector: bool,
+) -> Result<OptionalQName<'i, Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ let default_namespace = |local_name| {
+ let namespace = match parser.default_namespace() {
+ Some(url) => QNamePrefix::ImplicitDefaultNamespace(url),
+ None => QNamePrefix::ImplicitAnyNamespace,
+ };
+ Ok(OptionalQName::Some(namespace, local_name))
+ };
+
+ let explicit_namespace = |input: &mut CssParser<'i, 't>, namespace| {
+ let location = input.current_source_location();
+ match input.next_including_whitespace() {
+ Ok(&Token::Delim('*')) if !in_attr_selector => Ok(OptionalQName::Some(namespace, None)),
+ Ok(&Token::Ident(ref local_name)) => {
+ Ok(OptionalQName::Some(namespace, Some(local_name.clone())))
+ },
+ Ok(t) if in_attr_selector => {
+ let e = SelectorParseErrorKind::InvalidQualNameInAttr(t.clone());
+ Err(location.new_custom_error(e))
+ },
+ Ok(t) => Err(location.new_custom_error(
+ SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(t.clone()),
+ )),
+ Err(e) => Err(e.into()),
+ }
+ };
+
+ let start = input.state();
+ match input.next_including_whitespace() {
+ Ok(Token::Ident(value)) => {
+ let value = value.clone();
+ let after_ident = input.state();
+ match input.next_including_whitespace() {
+ Ok(&Token::Delim('|')) => {
+ let prefix = value.as_ref().into();
+ let result = parser.namespace_for_prefix(&prefix);
+ let url = result.ok_or(
+ after_ident
+ .source_location()
+ .new_custom_error(SelectorParseErrorKind::ExpectedNamespace(value)),
+ )?;
+ explicit_namespace(input, QNamePrefix::ExplicitNamespace(prefix, url))
+ },
+ _ => {
+ input.reset(&after_ident);
+ if in_attr_selector {
+ Ok(OptionalQName::Some(
+ QNamePrefix::ImplicitNoNamespace,
+ Some(value),
+ ))
+ } else {
+ default_namespace(Some(value))
+ }
+ },
+ }
+ },
+ Ok(Token::Delim('*')) => {
+ let after_star = input.state();
+ match input.next_including_whitespace() {
+ Ok(&Token::Delim('|')) => {
+ explicit_namespace(input, QNamePrefix::ExplicitAnyNamespace)
+ },
+ _ if !in_attr_selector => {
+ input.reset(&after_star);
+ default_namespace(None)
+ },
+ result => {
+ let t = result?;
+ Err(after_star
+ .source_location()
+ .new_custom_error(SelectorParseErrorKind::ExpectedBarInAttr(t.clone())))
+ },
+ }
+ },
+ Ok(Token::Delim('|')) => explicit_namespace(input, QNamePrefix::ExplicitNoNamespace),
+ Ok(t) => {
+ let t = t.clone();
+ input.reset(&start);
+ Ok(OptionalQName::None(t))
+ },
+ Err(e) => {
+ input.reset(&start);
+ Err(e.into())
+ },
+ }
+}
+
+fn parse_attribute_selector<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+) -> Result<Component<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ let namespace;
+ let local_name;
+
+ input.skip_whitespace();
+
+ match parse_qualified_name(parser, input, /* in_attr_selector = */ true)? {
+ OptionalQName::None(t) => {
+ return Err(input.new_custom_error(
+ SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(t),
+ ));
+ },
+ OptionalQName::Some(_, None) => unreachable!(),
+ OptionalQName::Some(ns, Some(ln)) => {
+ local_name = ln;
+ namespace = match ns {
+ QNamePrefix::ImplicitNoNamespace | QNamePrefix::ExplicitNoNamespace => None,
+ QNamePrefix::ExplicitNamespace(prefix, url) => {
+ Some(NamespaceConstraint::Specific((prefix, url)))
+ },
+ QNamePrefix::ExplicitAnyNamespace => Some(NamespaceConstraint::Any),
+ QNamePrefix::ImplicitAnyNamespace | QNamePrefix::ImplicitDefaultNamespace(_) => {
+ unreachable!() // Not returned with in_attr_selector = true
+ },
+ }
+ },
+ }
+
+ let location = input.current_source_location();
+ let operator = match input.next() {
+ // [foo]
+ Err(_) => {
+ let local_name_lower = to_ascii_lowercase(&local_name).as_ref().into();
+ let local_name = local_name.as_ref().into();
+ if let Some(namespace) = namespace {
+ return Ok(Component::AttributeOther(Box::new(
+ AttrSelectorWithOptionalNamespace {
+ namespace: Some(namespace),
+ local_name,
+ local_name_lower,
+ operation: ParsedAttrSelectorOperation::Exists,
+ },
+ )));
+ } else {
+ return Ok(Component::AttributeInNoNamespaceExists {
+ local_name,
+ local_name_lower,
+ });
+ }
+ },
+
+ // [foo=bar]
+ Ok(&Token::Delim('=')) => AttrSelectorOperator::Equal,
+ // [foo~=bar]
+ Ok(&Token::IncludeMatch) => AttrSelectorOperator::Includes,
+ // [foo|=bar]
+ Ok(&Token::DashMatch) => AttrSelectorOperator::DashMatch,
+ // [foo^=bar]
+ Ok(&Token::PrefixMatch) => AttrSelectorOperator::Prefix,
+ // [foo*=bar]
+ Ok(&Token::SubstringMatch) => AttrSelectorOperator::Substring,
+ // [foo$=bar]
+ Ok(&Token::SuffixMatch) => AttrSelectorOperator::Suffix,
+ Ok(t) => {
+ return Err(location.new_custom_error(
+ SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(t.clone()),
+ ));
+ },
+ };
+
+ let value = match input.expect_ident_or_string() {
+ Ok(t) => t.clone(),
+ Err(BasicParseError {
+ kind: BasicParseErrorKind::UnexpectedToken(t),
+ location,
+ }) => return Err(location.new_custom_error(SelectorParseErrorKind::BadValueInAttr(t))),
+ Err(e) => return Err(e.into()),
+ };
+
+ let attribute_flags = parse_attribute_flags(input)?;
+ let value = value.as_ref().into();
+ let local_name_lower;
+ let local_name_is_ascii_lowercase;
+ let case_sensitivity;
+ {
+ let local_name_lower_cow = to_ascii_lowercase(&local_name);
+ case_sensitivity =
+ attribute_flags.to_case_sensitivity(local_name_lower_cow.as_ref(), namespace.is_some());
+ local_name_lower = local_name_lower_cow.as_ref().into();
+ local_name_is_ascii_lowercase = matches!(local_name_lower_cow, Cow::Borrowed(..));
+ }
+ let local_name = local_name.as_ref().into();
+ if namespace.is_some() || !local_name_is_ascii_lowercase {
+ Ok(Component::AttributeOther(Box::new(
+ AttrSelectorWithOptionalNamespace {
+ namespace,
+ local_name,
+ local_name_lower,
+ operation: ParsedAttrSelectorOperation::WithValue {
+ operator,
+ case_sensitivity,
+ value,
+ },
+ },
+ )))
+ } else {
+ Ok(Component::AttributeInNoNamespace {
+ local_name,
+ operator,
+ value,
+ case_sensitivity,
+ })
+ }
+}
+
+/// An attribute selector can have 's' or 'i' as flags, or no flags at all.
+enum AttributeFlags {
+ // Matching should be case-sensitive ('s' flag).
+ CaseSensitive,
+ // Matching should be case-insensitive ('i' flag).
+ AsciiCaseInsensitive,
+ // No flags. Matching behavior depends on the name of the attribute.
+ CaseSensitivityDependsOnName,
+}
+
+impl AttributeFlags {
+ fn to_case_sensitivity(self, local_name: &str, have_namespace: bool) -> ParsedCaseSensitivity {
+ match self {
+ AttributeFlags::CaseSensitive => ParsedCaseSensitivity::ExplicitCaseSensitive,
+ AttributeFlags::AsciiCaseInsensitive => ParsedCaseSensitivity::AsciiCaseInsensitive,
+ AttributeFlags::CaseSensitivityDependsOnName => {
+ if !have_namespace &&
+ include!(concat!(
+ env!("OUT_DIR"),
+ "/ascii_case_insensitive_html_attributes.rs"
+ ))
+ .contains(local_name)
+ {
+ ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument
+ } else {
+ ParsedCaseSensitivity::CaseSensitive
+ }
+ },
+ }
+ }
+}
+
+fn parse_attribute_flags<'i, 't>(
+ input: &mut CssParser<'i, 't>,
+) -> Result<AttributeFlags, BasicParseError<'i>> {
+ let location = input.current_source_location();
+ let token = match input.next() {
+ Ok(t) => t,
+ Err(..) => {
+ // Selectors spec says language-defined; HTML says it depends on the
+ // exact attribute name.
+ return Ok(AttributeFlags::CaseSensitivityDependsOnName);
+ },
+ };
+
+ let ident = match *token {
+ Token::Ident(ref i) => i,
+ ref other => return Err(location.new_basic_unexpected_token_error(other.clone())),
+ };
+
+ Ok(match_ignore_ascii_case! {
+ ident,
+ "i" => AttributeFlags::AsciiCaseInsensitive,
+ "s" => AttributeFlags::CaseSensitive,
+ _ => return Err(location.new_basic_unexpected_token_error(token.clone())),
+ })
+}
+
+/// Level 3: Parse **one** simple_selector. (Though we might insert a second
+/// implied "<defaultns>|*" type selector.)
+fn parse_negation<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ state: SelectorParsingState,
+) -> Result<Component<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ let list = SelectorList::parse_with_state(
+ parser,
+ input,
+ state |
+ SelectorParsingState::SKIP_DEFAULT_NAMESPACE |
+ SelectorParsingState::DISALLOW_PSEUDOS,
+ ForgivingParsing::No,
+ ParseRelative::No,
+ )?;
+
+ Ok(Component::Negation(list))
+}
+
+/// simple_selector_sequence
+/// : [ type_selector | universal ] [ HASH | class | attrib | pseudo | negation ]*
+/// | [ HASH | class | attrib | pseudo | negation ]+
+///
+/// `Err(())` means invalid selector.
+/// `Ok(true)` is an empty selector
+fn parse_compound_selector<'i, 't, P, Impl>(
+ parser: &P,
+ state: &mut SelectorParsingState,
+ input: &mut CssParser<'i, 't>,
+ builder: &mut SelectorBuilder<Impl>,
+) -> Result<bool, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ input.skip_whitespace();
+
+ let mut empty = true;
+ if parse_type_selector(parser, input, *state, builder)? {
+ empty = false;
+ }
+
+ loop {
+ let result = match parse_one_simple_selector(parser, input, *state)? {
+ None => break,
+ Some(result) => result,
+ };
+
+ if empty {
+ if let Some(url) = parser.default_namespace() {
+ // If there was no explicit type selector, but there is a
+ // default namespace, there is an implicit "<defaultns>|*" type
+ // selector. Except for :host() or :not() / :is() / :where(),
+ // where we ignore it.
+ //
+ // https://drafts.csswg.org/css-scoping/#host-element-in-tree:
+ //
+ // When considered within its own shadow trees, the shadow
+ // host is featureless. Only the :host, :host(), and
+ // :host-context() pseudo-classes are allowed to match it.
+ //
+ // https://drafts.csswg.org/selectors-4/#featureless:
+ //
+ // A featureless element does not match any selector at all,
+ // except those it is explicitly defined to match. If a
+ // given selector is allowed to match a featureless element,
+ // it must do so while ignoring the default namespace.
+ //
+ // https://drafts.csswg.org/selectors-4/#matches
+ //
+ // Default namespace declarations do not affect the compound
+ // selector representing the subject of any selector within
+ // a :is() pseudo-class, unless that compound selector
+ // contains an explicit universal selector or type selector.
+ //
+ // (Similar quotes for :where() / :not())
+ //
+ let ignore_default_ns = state
+ .intersects(SelectorParsingState::SKIP_DEFAULT_NAMESPACE) ||
+ matches!(
+ result,
+ SimpleSelectorParseResult::SimpleSelector(Component::Host(..))
+ );
+ if !ignore_default_ns {
+ builder.push_simple_selector(Component::DefaultNamespace(url));
+ }
+ }
+ }
+
+ empty = false;
+
+ match result {
+ SimpleSelectorParseResult::SimpleSelector(s) => {
+ builder.push_simple_selector(s);
+ },
+ SimpleSelectorParseResult::PartPseudo(part_names) => {
+ state.insert(SelectorParsingState::AFTER_PART);
+ builder.push_combinator(Combinator::Part);
+ builder.push_simple_selector(Component::Part(part_names));
+ },
+ SimpleSelectorParseResult::SlottedPseudo(selector) => {
+ state.insert(SelectorParsingState::AFTER_SLOTTED);
+ builder.push_combinator(Combinator::SlotAssignment);
+ builder.push_simple_selector(Component::Slotted(selector));
+ },
+ SimpleSelectorParseResult::PseudoElement(p) => {
+ state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT);
+ if !p.accepts_state_pseudo_classes() {
+ state.insert(SelectorParsingState::AFTER_NON_STATEFUL_PSEUDO_ELEMENT);
+ }
+ builder.push_combinator(Combinator::PseudoElement);
+ builder.push_simple_selector(Component::PseudoElement(p));
+ },
+ }
+ }
+ Ok(empty)
+}
+
+fn parse_is_where<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ state: SelectorParsingState,
+ component: impl FnOnce(SelectorList<Impl>) -> Component<Impl>,
+) -> Result<Component<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ debug_assert!(parser.parse_is_and_where());
+ // https://drafts.csswg.org/selectors/#matches-pseudo:
+ //
+ // Pseudo-elements cannot be represented by the matches-any
+ // pseudo-class; they are not valid within :is().
+ //
+ let inner = SelectorList::parse_with_state(
+ parser,
+ input,
+ state |
+ SelectorParsingState::SKIP_DEFAULT_NAMESPACE |
+ SelectorParsingState::DISALLOW_PSEUDOS,
+ ForgivingParsing::Yes,
+ ParseRelative::No,
+ )?;
+ Ok(component(inner))
+}
+
+fn parse_has<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ state: SelectorParsingState,
+) -> Result<Component<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ debug_assert!(parser.parse_has());
+ if state.intersects(SelectorParsingState::DISALLOW_RELATIVE_SELECTOR) {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ // Nested `:has()` is disallowed, mark it as such.
+ // Note: The spec defines ":has-allowed pseudo-element," but there's no
+ // pseudo-element defined as such at the moment.
+ // https://w3c.github.io/csswg-drafts/selectors-4/#has-allowed-pseudo-element
+ let inner = SelectorList::parse_with_state(
+ parser,
+ input,
+ state |
+ SelectorParsingState::SKIP_DEFAULT_NAMESPACE |
+ SelectorParsingState::DISALLOW_PSEUDOS |
+ SelectorParsingState::DISALLOW_RELATIVE_SELECTOR,
+ ForgivingParsing::No,
+ ParseRelative::ForHas,
+ )?;
+ Ok(Component::Has(RelativeSelector::from_selector_list(inner)))
+}
+
+fn parse_functional_pseudo_class<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ name: CowRcStr<'i>,
+ state: SelectorParsingState,
+) -> Result<Component<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ match_ignore_ascii_case! { &name,
+ "nth-child" => return parse_nth_pseudo_class(parser, input, state, NthType::Child),
+ "nth-of-type" => return parse_nth_pseudo_class(parser, input, state, NthType::OfType),
+ "nth-last-child" => return parse_nth_pseudo_class(parser, input, state, NthType::LastChild),
+ "nth-last-of-type" => return parse_nth_pseudo_class(parser, input, state, NthType::LastOfType),
+ "is" if parser.parse_is_and_where() => return parse_is_where(parser, input, state, Component::Is),
+ "where" if parser.parse_is_and_where() => return parse_is_where(parser, input, state, Component::Where),
+ "has" if parser.parse_has() => return parse_has(parser, input, state),
+ "host" => {
+ if !state.allows_tree_structural_pseudo_classes() {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ return Ok(Component::Host(Some(parse_inner_compound_selector(parser, input, state)?)));
+ },
+ "not" => {
+ return parse_negation(parser, input, state)
+ },
+ _ => {}
+ }
+
+ if parser.parse_is_and_where() && parser.is_is_alias(&name) {
+ return parse_is_where(parser, input, state, Component::Is);
+ }
+
+ if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT | SelectorParsingState::AFTER_SLOTTED) {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+
+ let after_part = state.intersects(SelectorParsingState::AFTER_PART);
+ P::parse_non_ts_functional_pseudo_class(parser, name, input, after_part).map(Component::NonTSPseudoClass)
+}
+
+fn parse_nth_pseudo_class<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ state: SelectorParsingState,
+ ty: NthType,
+) -> Result<Component<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ if !state.allows_tree_structural_pseudo_classes() {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ let (a, b) = parse_nth(input)?;
+ let nth_data = NthSelectorData {
+ ty,
+ is_function: true,
+ a,
+ b,
+ };
+ if !parser.parse_nth_child_of() || ty.is_of_type() {
+ return Ok(Component::Nth(nth_data));
+ }
+
+ // Try to parse "of <selector-list>".
+ if input.try_parse(|i| i.expect_ident_matching("of")).is_err() {
+ return Ok(Component::Nth(nth_data));
+ }
+ // Whitespace between "of" and the selector list is optional
+ // https://github.com/w3c/csswg-drafts/issues/8285
+ let selectors = SelectorList::parse_with_state(
+ parser,
+ input,
+ state |
+ SelectorParsingState::SKIP_DEFAULT_NAMESPACE |
+ SelectorParsingState::DISALLOW_PSEUDOS,
+ ForgivingParsing::No,
+ ParseRelative::No,
+ )?;
+ Ok(Component::NthOf(NthOfSelectorData::new(
+ &nth_data,
+ selectors.slice().iter().cloned(),
+ )))
+}
+
+/// Returns whether the name corresponds to a CSS2 pseudo-element that
+/// can be specified with the single colon syntax (in addition to the
+/// double-colon syntax, which can be used for all pseudo-elements).
+fn is_css2_pseudo_element(name: &str) -> bool {
+ // ** Do not add to this list! **
+ match_ignore_ascii_case! { name,
+ "before" | "after" | "first-line" | "first-letter" => true,
+ _ => false,
+ }
+}
+
+/// Parse a simple selector other than a type selector.
+///
+/// * `Err(())`: Invalid selector, abort
+/// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed.
+/// * `Ok(Some(_))`: Parsed a simple selector or pseudo-element
+fn parse_one_simple_selector<'i, 't, P, Impl>(
+ parser: &P,
+ input: &mut CssParser<'i, 't>,
+ state: SelectorParsingState,
+) -> Result<Option<SimpleSelectorParseResult<Impl>>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ let start = input.state();
+ let token = match input.next_including_whitespace().map(|t| t.clone()) {
+ Ok(t) => t,
+ Err(..) => {
+ input.reset(&start);
+ return Ok(None);
+ },
+ };
+
+ Ok(Some(match token {
+ Token::IDHash(id) => {
+ if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ let id = Component::ID(id.as_ref().into());
+ SimpleSelectorParseResult::SimpleSelector(id)
+ },
+ Token::Delim(delim) if delim == '.' || (delim == '&' && parser.parse_parent_selector()) => {
+ if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ let location = input.current_source_location();
+ SimpleSelectorParseResult::SimpleSelector(if delim == '&' {
+ Component::ParentSelector
+ } else {
+ let class = match *input.next_including_whitespace()? {
+ Token::Ident(ref class) => class,
+ ref t => {
+ let e = SelectorParseErrorKind::ClassNeedsIdent(t.clone());
+ return Err(location.new_custom_error(e));
+ },
+ };
+ Component::Class(class.as_ref().into())
+ })
+ },
+ Token::SquareBracketBlock => {
+ if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ let attr = input.parse_nested_block(|input| parse_attribute_selector(parser, input))?;
+ SimpleSelectorParseResult::SimpleSelector(attr)
+ },
+ Token::Colon => {
+ let location = input.current_source_location();
+ let (is_single_colon, next_token) = match input.next_including_whitespace()?.clone() {
+ Token::Colon => (false, input.next_including_whitespace()?.clone()),
+ t => (true, t),
+ };
+ let (name, is_functional) = match next_token {
+ Token::Ident(name) => (name, false),
+ Token::Function(name) => (name, true),
+ t => {
+ let e = SelectorParseErrorKind::PseudoElementExpectedIdent(t);
+ return Err(input.new_custom_error(e));
+ },
+ };
+ let is_pseudo_element = !is_single_colon || is_css2_pseudo_element(&name);
+ if is_pseudo_element {
+ if !state.allows_pseudos() {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ let pseudo_element = if is_functional {
+ if P::parse_part(parser) && name.eq_ignore_ascii_case("part") {
+ if !state.allows_part() {
+ return Err(
+ input.new_custom_error(SelectorParseErrorKind::InvalidState)
+ );
+ }
+ let names = input.parse_nested_block(|input| {
+ let mut result = Vec::with_capacity(1);
+ result.push(input.expect_ident()?.as_ref().into());
+ while !input.is_exhausted() {
+ result.push(input.expect_ident()?.as_ref().into());
+ }
+ Ok(result.into_boxed_slice())
+ })?;
+ return Ok(Some(SimpleSelectorParseResult::PartPseudo(names)));
+ }
+ if P::parse_slotted(parser) && name.eq_ignore_ascii_case("slotted") {
+ if !state.allows_slotted() {
+ return Err(
+ input.new_custom_error(SelectorParseErrorKind::InvalidState)
+ );
+ }
+ let selector = input.parse_nested_block(|input| {
+ parse_inner_compound_selector(parser, input, state)
+ })?;
+ return Ok(Some(SimpleSelectorParseResult::SlottedPseudo(selector)));
+ }
+ input.parse_nested_block(|input| {
+ P::parse_functional_pseudo_element(parser, name, input)
+ })?
+ } else {
+ P::parse_pseudo_element(parser, location, name)?
+ };
+
+ if state.intersects(SelectorParsingState::AFTER_SLOTTED) &&
+ !pseudo_element.valid_after_slotted()
+ {
+ return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ SimpleSelectorParseResult::PseudoElement(pseudo_element)
+ } else {
+ let pseudo_class = if is_functional {
+ input.parse_nested_block(|input| {
+ parse_functional_pseudo_class(parser, input, name, state)
+ })?
+ } else {
+ parse_simple_pseudo_class(parser, location, name, state)?
+ };
+ SimpleSelectorParseResult::SimpleSelector(pseudo_class)
+ }
+ },
+ _ => {
+ input.reset(&start);
+ return Ok(None);
+ },
+ }))
+}
+
+fn parse_simple_pseudo_class<'i, P, Impl>(
+ parser: &P,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ state: SelectorParsingState,
+) -> Result<Component<Impl>, ParseError<'i, P::Error>>
+where
+ P: Parser<'i, Impl = Impl>,
+ Impl: SelectorImpl,
+{
+ if !state.allows_non_functional_pseudo_classes() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+
+ if state.allows_tree_structural_pseudo_classes() {
+ match_ignore_ascii_case! { &name,
+ "first-child" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ false))),
+ "last-child" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ false))),
+ "only-child" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ false))),
+ "root" => return Ok(Component::Root),
+ "empty" => return Ok(Component::Empty),
+ "scope" => return Ok(Component::Scope),
+ "host" if P::parse_host(parser) => return Ok(Component::Host(None)),
+ "first-of-type" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ true))),
+ "last-of-type" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ true))),
+ "only-of-type" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ true))),
+ _ => {},
+ }
+ }
+
+ let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name)?;
+ if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) &&
+ !pseudo_class.is_user_action_state()
+ {
+ return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState));
+ }
+ Ok(Component::NonTSPseudoClass(pseudo_class))
+}
+
+// NB: pub module in order to access the DummyParser
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::builder::SelectorFlags;
+ use crate::parser;
+ use cssparser::{serialize_identifier, Parser as CssParser, ParserInput, ToCss};
+ use std::collections::HashMap;
+ use std::fmt;
+
+ #[derive(Clone, Debug, Eq, PartialEq)]
+ pub enum PseudoClass {
+ Hover,
+ Active,
+ Lang(String),
+ }
+
+ #[derive(Clone, Debug, Eq, PartialEq)]
+ pub enum PseudoElement {
+ Before,
+ After,
+ Highlight(String),
+ }
+
+ impl parser::PseudoElement for PseudoElement {
+ type Impl = DummySelectorImpl;
+
+ fn accepts_state_pseudo_classes(&self) -> bool {
+ true
+ }
+
+ fn valid_after_slotted(&self) -> bool {
+ true
+ }
+ }
+
+ impl parser::NonTSPseudoClass for PseudoClass {
+ type Impl = DummySelectorImpl;
+
+ #[inline]
+ fn is_active_or_hover(&self) -> bool {
+ matches!(*self, PseudoClass::Active | PseudoClass::Hover)
+ }
+
+ #[inline]
+ fn is_user_action_state(&self) -> bool {
+ self.is_active_or_hover()
+ }
+ }
+
+ impl ToCss for PseudoClass {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ PseudoClass::Hover => dest.write_str(":hover"),
+ PseudoClass::Active => dest.write_str(":active"),
+ PseudoClass::Lang(ref lang) => {
+ dest.write_str(":lang(")?;
+ serialize_identifier(lang, dest)?;
+ dest.write_char(')')
+ },
+ }
+ }
+ }
+
+ impl ToCss for PseudoElement {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ PseudoElement::Before => dest.write_str("::before"),
+ PseudoElement::After => dest.write_str("::after"),
+ PseudoElement::Highlight(ref name) => {
+ dest.write_str("::highlight(")?;
+ serialize_identifier(&name, dest)?;
+ dest.write_char(')')
+ },
+ }
+ }
+ }
+
+ #[derive(Clone, Debug, PartialEq)]
+ pub struct DummySelectorImpl;
+
+ #[derive(Default)]
+ pub struct DummyParser {
+ default_ns: Option<DummyAtom>,
+ ns_prefixes: HashMap<DummyAtom, DummyAtom>,
+ }
+
+ impl DummyParser {
+ fn default_with_namespace(default_ns: DummyAtom) -> DummyParser {
+ DummyParser {
+ default_ns: Some(default_ns),
+ ns_prefixes: Default::default(),
+ }
+ }
+ }
+
+ impl SelectorImpl for DummySelectorImpl {
+ type ExtraMatchingData<'a> = std::marker::PhantomData<&'a ()>;
+ type AttrValue = DummyAttrValue;
+ type Identifier = DummyAtom;
+ type LocalName = DummyAtom;
+ type NamespaceUrl = DummyAtom;
+ type NamespacePrefix = DummyAtom;
+ type BorrowedLocalName = DummyAtom;
+ type BorrowedNamespaceUrl = DummyAtom;
+ type NonTSPseudoClass = PseudoClass;
+ type PseudoElement = PseudoElement;
+ }
+
+ #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
+ pub struct DummyAttrValue(String);
+
+ impl ToCss for DummyAttrValue {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use std::fmt::Write;
+
+ dest.write_char('"')?;
+ write!(cssparser::CssStringWriter::new(dest), "{}", &self.0)?;
+ dest.write_char('"')
+ }
+ }
+
+ impl<'a> From<&'a str> for DummyAttrValue {
+ fn from(string: &'a str) -> Self {
+ Self(string.into())
+ }
+ }
+
+ #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
+ pub struct DummyAtom(String);
+
+ impl ToCss for DummyAtom {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ serialize_identifier(&self.0, dest)
+ }
+ }
+
+ impl From<String> for DummyAtom {
+ fn from(string: String) -> Self {
+ DummyAtom(string)
+ }
+ }
+
+ impl<'a> From<&'a str> for DummyAtom {
+ fn from(string: &'a str) -> Self {
+ DummyAtom(string.into())
+ }
+ }
+
+ impl PrecomputedHash for DummyAtom {
+ fn precomputed_hash(&self) -> u32 {
+ self.0.as_ptr() as u32
+ }
+ }
+
+ impl<'i> Parser<'i> for DummyParser {
+ type Impl = DummySelectorImpl;
+ type Error = SelectorParseErrorKind<'i>;
+
+ fn parse_slotted(&self) -> bool {
+ true
+ }
+
+ fn parse_nth_child_of(&self) -> bool {
+ true
+ }
+
+ fn parse_is_and_where(&self) -> bool {
+ true
+ }
+
+ fn parse_has(&self) -> bool {
+ true
+ }
+
+ fn parse_parent_selector(&self) -> bool {
+ true
+ }
+
+ fn parse_part(&self) -> bool {
+ true
+ }
+
+ fn parse_non_ts_pseudo_class(
+ &self,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ ) -> Result<PseudoClass, SelectorParseError<'i>> {
+ match_ignore_ascii_case! { &name,
+ "hover" => return Ok(PseudoClass::Hover),
+ "active" => return Ok(PseudoClass::Active),
+ _ => {}
+ }
+ Err(
+ location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn parse_non_ts_functional_pseudo_class<'t>(
+ &self,
+ name: CowRcStr<'i>,
+ parser: &mut CssParser<'i, 't>,
+ after_part: bool,
+ ) -> Result<PseudoClass, SelectorParseError<'i>> {
+ match_ignore_ascii_case! { &name,
+ "lang" if !after_part => {
+ let lang = parser.expect_ident_or_string()?.as_ref().to_owned();
+ return Ok(PseudoClass::Lang(lang));
+ },
+ _ => {}
+ }
+ Err(
+ parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn parse_pseudo_element(
+ &self,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ ) -> Result<PseudoElement, SelectorParseError<'i>> {
+ match_ignore_ascii_case! { &name,
+ "before" => return Ok(PseudoElement::Before),
+ "after" => return Ok(PseudoElement::After),
+ _ => {}
+ }
+ Err(
+ location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn parse_functional_pseudo_element<'t>(
+ &self,
+ name: CowRcStr<'i>,
+ parser: &mut CssParser<'i, 't>,
+ ) -> Result<PseudoElement, SelectorParseError<'i>> {
+ match_ignore_ascii_case! {&name,
+ "highlight" => return Ok(PseudoElement::Highlight(parser.expect_ident()?.as_ref().to_owned())),
+ _ => {}
+ }
+ Err(
+ parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn default_namespace(&self) -> Option<DummyAtom> {
+ self.default_ns.clone()
+ }
+
+ fn namespace_for_prefix(&self, prefix: &DummyAtom) -> Option<DummyAtom> {
+ self.ns_prefixes.get(prefix).cloned()
+ }
+ }
+
+ fn parse<'i>(
+ input: &'i str,
+ ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> {
+ parse_relative(input, ParseRelative::No)
+ }
+
+ fn parse_relative<'i>(
+ input: &'i str,
+ parse_relative: ParseRelative,
+ ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> {
+ parse_ns_relative(input, &DummyParser::default(), parse_relative)
+ }
+
+ fn parse_expected<'i, 'a>(
+ input: &'i str,
+ expected: Option<&'a str>,
+ ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> {
+ parse_ns_expected(input, &DummyParser::default(), expected)
+ }
+
+ fn parse_relative_expected<'i, 'a>(
+ input: &'i str,
+ parse_relative: ParseRelative,
+ expected: Option<&'a str>,
+ ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> {
+ parse_ns_relative_expected(input, &DummyParser::default(), parse_relative, expected)
+ }
+
+ fn parse_ns<'i>(
+ input: &'i str,
+ parser: &DummyParser,
+ ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> {
+ parse_ns_relative(input, parser, ParseRelative::No)
+ }
+
+ fn parse_ns_relative<'i>(
+ input: &'i str,
+ parser: &DummyParser,
+ parse_relative: ParseRelative,
+ ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> {
+ parse_ns_relative_expected(input, parser, parse_relative, None)
+ }
+
+ fn parse_ns_expected<'i, 'a>(
+ input: &'i str,
+ parser: &DummyParser,
+ expected: Option<&'a str>,
+ ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> {
+ parse_ns_relative_expected(input, parser, ParseRelative::No, expected)
+ }
+
+ fn parse_ns_relative_expected<'i, 'a>(
+ input: &'i str,
+ parser: &DummyParser,
+ parse_relative: ParseRelative,
+ expected: Option<&'a str>,
+ ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> {
+ let mut parser_input = ParserInput::new(input);
+ let result = SelectorList::parse(
+ parser,
+ &mut CssParser::new(&mut parser_input),
+ parse_relative,
+ );
+ if let Ok(ref selectors) = result {
+ // We can't assume that the serialized parsed selector will equal
+ // the input; for example, if there is no default namespace, '*|foo'
+ // should serialize to 'foo'.
+ assert_eq!(
+ selectors.to_css_string(),
+ match expected {
+ Some(x) => x,
+ None => input,
+ }
+ );
+ }
+ result
+ }
+
+ fn specificity(a: u32, b: u32, c: u32) -> u32 {
+ a << 20 | b << 10 | c
+ }
+
+ #[test]
+ fn test_empty() {
+ let mut input = ParserInput::new(":empty");
+ let list = SelectorList::parse(
+ &DummyParser::default(),
+ &mut CssParser::new(&mut input),
+ ParseRelative::No,
+ );
+ assert!(list.is_ok());
+ }
+
+ const MATHML: &str = "http://www.w3.org/1998/Math/MathML";
+ const SVG: &str = "http://www.w3.org/2000/svg";
+
+ #[test]
+ fn test_parsing() {
+ assert!(parse("").is_err());
+ assert!(parse(":lang(4)").is_err());
+ assert!(parse(":lang(en US)").is_err());
+ assert_eq!(
+ parse("EeÉ"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::LocalName(LocalName {
+ name: DummyAtom::from("EeÉ"),
+ lower_name: DummyAtom::from("eeÉ"),
+ })],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse("|e"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::ExplicitNoNamespace,
+ Component::LocalName(LocalName {
+ name: DummyAtom::from("e"),
+ lower_name: DummyAtom::from("e"),
+ }),
+ ],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ // When the default namespace is not set, *| should be elided.
+ // https://github.com/servo/servo/pull/17537
+ assert_eq!(
+ parse_expected("*|e", Some("e")),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::LocalName(LocalName {
+ name: DummyAtom::from("e"),
+ lower_name: DummyAtom::from("e"),
+ })],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ // When the default namespace is set, *| should _not_ be elided (as foo
+ // is no longer equivalent to *|foo--the former is only for foo in the
+ // default namespace).
+ // https://github.com/servo/servo/issues/16020
+ assert_eq!(
+ parse_ns(
+ "*|e",
+ &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org"))
+ ),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::ExplicitAnyNamespace,
+ Component::LocalName(LocalName {
+ name: DummyAtom::from("e"),
+ lower_name: DummyAtom::from("e"),
+ }),
+ ],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse("*"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::ExplicitUniversalType],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse("|*"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::ExplicitNoNamespace,
+ Component::ExplicitUniversalType,
+ ],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse_expected("*|*", Some("*")),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::ExplicitUniversalType],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse_ns(
+ "*|*",
+ &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org"))
+ ),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::ExplicitAnyNamespace,
+ Component::ExplicitUniversalType,
+ ],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse(".foo:lang(en-US)"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::Class(DummyAtom::from("foo")),
+ Component::NonTSPseudoClass(PseudoClass::Lang("en-US".to_owned())),
+ ],
+ specificity(0, 2, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse("#bar"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::ID(DummyAtom::from("bar"))],
+ specificity(1, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse("e.foo#bar"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::LocalName(LocalName {
+ name: DummyAtom::from("e"),
+ lower_name: DummyAtom::from("e"),
+ }),
+ Component::Class(DummyAtom::from("foo")),
+ Component::ID(DummyAtom::from("bar")),
+ ],
+ specificity(1, 1, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse("e.foo #bar"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::LocalName(LocalName {
+ name: DummyAtom::from("e"),
+ lower_name: DummyAtom::from("e"),
+ }),
+ Component::Class(DummyAtom::from("foo")),
+ Component::Combinator(Combinator::Descendant),
+ Component::ID(DummyAtom::from("bar")),
+ ],
+ specificity(1, 1, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ // Default namespace does not apply to attribute selectors
+ // https://github.com/mozilla/servo/pull/1652
+ let mut parser = DummyParser::default();
+ assert_eq!(
+ parse_ns("[Foo]", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::AttributeInNoNamespaceExists {
+ local_name: DummyAtom::from("Foo"),
+ local_name_lower: DummyAtom::from("foo"),
+ }],
+ specificity(0, 1, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert!(parse_ns("svg|circle", &parser).is_err());
+ parser
+ .ns_prefixes
+ .insert(DummyAtom("svg".into()), DummyAtom(SVG.into()));
+ assert_eq!(
+ parse_ns("svg|circle", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::Namespace(DummyAtom("svg".into()), SVG.into()),
+ Component::LocalName(LocalName {
+ name: DummyAtom::from("circle"),
+ lower_name: DummyAtom::from("circle"),
+ }),
+ ],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse_ns("svg|*", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::Namespace(DummyAtom("svg".into()), SVG.into()),
+ Component::ExplicitUniversalType,
+ ],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ // Default namespace does not apply to attribute selectors
+ // https://github.com/mozilla/servo/pull/1652
+ // but it does apply to implicit type selectors
+ // https://github.com/servo/rust-selectors/pull/82
+ parser.default_ns = Some(MATHML.into());
+ assert_eq!(
+ parse_ns("[Foo]", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(MATHML.into()),
+ Component::AttributeInNoNamespaceExists {
+ local_name: DummyAtom::from("Foo"),
+ local_name_lower: DummyAtom::from("foo"),
+ },
+ ],
+ specificity(0, 1, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ // Default namespace does apply to type selectors
+ assert_eq!(
+ parse_ns("e", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(MATHML.into()),
+ Component::LocalName(LocalName {
+ name: DummyAtom::from("e"),
+ lower_name: DummyAtom::from("e"),
+ }),
+ ],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse_ns("*", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(MATHML.into()),
+ Component::ExplicitUniversalType,
+ ],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse_ns("*|*", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::ExplicitAnyNamespace,
+ Component::ExplicitUniversalType,
+ ],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ // Default namespace applies to universal and type selectors inside :not and :matches,
+ // but not otherwise.
+ assert_eq!(
+ parse_ns(":not(.cl)", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(MATHML.into()),
+ Component::Negation(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::Class(DummyAtom::from("cl"))],
+ specificity(0, 1, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )])),
+ ],
+ specificity(0, 1, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse_ns(":not(*)", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(MATHML.into()),
+ Component::Negation(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(MATHML.into()),
+ Component::ExplicitUniversalType,
+ ],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]),),
+ ],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse_ns(":not(e)", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(MATHML.into()),
+ Component::Negation(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::DefaultNamespace(MATHML.into()),
+ Component::LocalName(LocalName {
+ name: DummyAtom::from("e"),
+ lower_name: DummyAtom::from("e"),
+ }),
+ ],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )])),
+ ],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse("[attr|=\"foo\"]"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::AttributeInNoNamespace {
+ local_name: DummyAtom::from("attr"),
+ operator: AttrSelectorOperator::DashMatch,
+ value: DummyAttrValue::from("foo"),
+ case_sensitivity: ParsedCaseSensitivity::CaseSensitive,
+ }],
+ specificity(0, 1, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ // https://github.com/mozilla/servo/issues/1723
+ assert_eq!(
+ parse("::before"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::Combinator(Combinator::PseudoElement),
+ Component::PseudoElement(PseudoElement::Before),
+ ],
+ specificity(0, 0, 1),
+ SelectorFlags::HAS_PSEUDO,
+ )]))
+ );
+ assert_eq!(
+ parse("::before:hover"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::Combinator(Combinator::PseudoElement),
+ Component::PseudoElement(PseudoElement::Before),
+ Component::NonTSPseudoClass(PseudoClass::Hover),
+ ],
+ specificity(0, 1, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO,
+ )]))
+ );
+ assert_eq!(
+ parse("::before:hover:hover"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::Combinator(Combinator::PseudoElement),
+ Component::PseudoElement(PseudoElement::Before),
+ Component::NonTSPseudoClass(PseudoClass::Hover),
+ Component::NonTSPseudoClass(PseudoClass::Hover),
+ ],
+ specificity(0, 2, 1),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO,
+ )]))
+ );
+ assert!(parse("::before:hover:lang(foo)").is_err());
+ assert!(parse("::before:hover .foo").is_err());
+ assert!(parse("::before .foo").is_err());
+ assert!(parse("::before ~ bar").is_err());
+ assert!(parse("::before:active").is_ok());
+
+ // https://github.com/servo/servo/issues/15335
+ assert!(parse(":: before").is_err());
+ assert_eq!(
+ parse("div ::after"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::LocalName(LocalName {
+ name: DummyAtom::from("div"),
+ lower_name: DummyAtom::from("div"),
+ }),
+ Component::Combinator(Combinator::Descendant),
+ Component::Combinator(Combinator::PseudoElement),
+ Component::PseudoElement(PseudoElement::After),
+ ],
+ specificity(0, 0, 2),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO,
+ )]))
+ );
+ assert_eq!(
+ parse("#d1 > .ok"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::ID(DummyAtom::from("d1")),
+ Component::Combinator(Combinator::Child),
+ Component::Class(DummyAtom::from("ok")),
+ ],
+ (1 << 20) + (1 << 10) + (0 << 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ parser.default_ns = None;
+ assert!(parse(":not(#provel.old)").is_ok());
+ assert!(parse(":not(#provel > old)").is_ok());
+ assert!(parse("table[rules]:not([rules=\"none\"]):not([rules=\"\"])").is_ok());
+ // https://github.com/servo/servo/issues/16017
+ assert_eq!(
+ parse_ns(":not(*)", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::Negation(SelectorList::from_vec(vec![
+ Selector::from_vec(
+ vec![Component::ExplicitUniversalType],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )
+ ]))],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ assert_eq!(
+ parse_ns(":not(|*)", &parser),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::Negation(SelectorList::from_vec(vec![
+ Selector::from_vec(
+ vec![
+ Component::ExplicitNoNamespace,
+ Component::ExplicitUniversalType,
+ ],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )
+ ]))],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+ // *| should be elided if there is no default namespace.
+ // https://github.com/servo/servo/pull/17537
+ assert_eq!(
+ parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![Component::Negation(SelectorList::from_vec(vec![
+ Selector::from_vec(
+ vec![Component::ExplicitUniversalType],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )
+ ]))],
+ specificity(0, 0, 0),
+ SelectorFlags::HAS_NON_FEATURELESS_COMPONENT,
+ )]))
+ );
+
+ assert!(parse("::highlight(foo)").is_ok());
+
+ assert!(parse("::slotted()").is_err());
+ assert!(parse("::slotted(div)").is_ok());
+ assert!(parse("::slotted(div).foo").is_err());
+ assert!(parse("::slotted(div + bar)").is_err());
+ assert!(parse("::slotted(div) + foo").is_err());
+
+ assert!(parse("::part()").is_err());
+ assert!(parse("::part(42)").is_err());
+ assert!(parse("::part(foo bar)").is_ok());
+ assert!(parse("::part(foo):hover").is_ok());
+ assert!(parse("::part(foo) + bar").is_err());
+
+ assert!(parse("div ::slotted(div)").is_ok());
+ assert!(parse("div + slot::slotted(div)").is_ok());
+ assert!(parse("div + slot::slotted(div.foo)").is_ok());
+ assert!(parse("slot::slotted(div,foo)::first-line").is_err());
+ assert!(parse("::slotted(div)::before").is_ok());
+ assert!(parse("slot::slotted(div,foo)").is_err());
+
+ assert!(parse("foo:where()").is_ok());
+ assert!(parse("foo:where(div, foo, .bar baz)").is_ok());
+ assert!(parse("foo:where(::before)").is_ok());
+ }
+
+ #[test]
+ fn parent_selector() {
+ assert!(parse("foo &").is_ok());
+ assert_eq!(
+ parse("#foo &.bar"),
+ Ok(SelectorList::from_vec(vec![Selector::from_vec(
+ vec![
+ Component::ID(DummyAtom::from("foo")),
+ Component::Combinator(Combinator::Descendant),
+ Component::ParentSelector,
+ Component::Class(DummyAtom::from("bar")),
+ ],
+ (1 << 20) + (1 << 10) + (0 << 0),
+ SelectorFlags::HAS_PARENT | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT
+ )]))
+ );
+
+ let parent = parse(".bar, div .baz").unwrap();
+ let child = parse("#foo &.bar").unwrap();
+ assert_eq!(
+ child.replace_parent_selector(&parent),
+ parse("#foo :is(.bar, div .baz).bar").unwrap()
+ );
+
+ let has_child = parse("#foo:has(&.bar)").unwrap();
+ assert_eq!(
+ has_child.replace_parent_selector(&parent),
+ parse("#foo:has(:is(.bar, div .baz).bar)").unwrap()
+ );
+
+ let child = parse("#foo").unwrap();
+ assert_eq!(
+ child.replace_parent_selector(&parent),
+ parse(":is(.bar, div .baz) #foo").unwrap()
+ );
+
+ let child =
+ parse_relative_expected("+ #foo", ParseRelative::ForNesting, Some("& + #foo")).unwrap();
+ assert_eq!(child, parse("& + #foo").unwrap());
+ }
+
+ #[test]
+ fn test_pseudo_iter() {
+ let list = parse("q::before").unwrap();
+ let selector = &list.slice()[0];
+ assert!(!selector.is_universal());
+ let mut iter = selector.iter();
+ assert_eq!(
+ iter.next(),
+ Some(&Component::PseudoElement(PseudoElement::Before))
+ );
+ assert_eq!(iter.next(), None);
+ let combinator = iter.next_sequence();
+ assert_eq!(combinator, Some(Combinator::PseudoElement));
+ assert!(matches!(iter.next(), Some(&Component::LocalName(..))));
+ assert_eq!(iter.next(), None);
+ assert_eq!(iter.next_sequence(), None);
+ }
+
+ #[test]
+ fn test_universal() {
+ let list = parse_ns(
+ "*|*::before",
+ &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")),
+ )
+ .unwrap();
+ let selector = &list.slice()[0];
+ assert!(selector.is_universal());
+ }
+
+ #[test]
+ fn test_empty_pseudo_iter() {
+ let list = parse("::before").unwrap();
+ let selector = &list.slice()[0];
+ assert!(selector.is_universal());
+ let mut iter = selector.iter();
+ assert_eq!(
+ iter.next(),
+ Some(&Component::PseudoElement(PseudoElement::Before))
+ );
+ assert_eq!(iter.next(), None);
+ assert_eq!(iter.next_sequence(), Some(Combinator::PseudoElement));
+ assert_eq!(iter.next(), None);
+ assert_eq!(iter.next_sequence(), None);
+ }
+
+ struct TestVisitor {
+ seen: Vec<String>,
+ }
+
+ impl SelectorVisitor for TestVisitor {
+ type Impl = DummySelectorImpl;
+
+ fn visit_simple_selector(&mut self, s: &Component<DummySelectorImpl>) -> bool {
+ let mut dest = String::new();
+ s.to_css(&mut dest).unwrap();
+ self.seen.push(dest);
+ true
+ }
+ }
+
+ #[test]
+ fn visitor() {
+ let mut test_visitor = TestVisitor { seen: vec![] };
+ parse(":not(:hover) ~ label").unwrap().slice()[0].visit(&mut test_visitor);
+ assert!(test_visitor.seen.contains(&":hover".into()));
+
+ let mut test_visitor = TestVisitor { seen: vec![] };
+ parse("::before:hover").unwrap().slice()[0].visit(&mut test_visitor);
+ assert!(test_visitor.seen.contains(&":hover".into()));
+ }
+}
diff --git a/servo/components/selectors/relative_selector/cache.rs b/servo/components/selectors/relative_selector/cache.rs
new file mode 100644
index 0000000000..d7681aa3a4
--- /dev/null
+++ b/servo/components/selectors/relative_selector/cache.rs
@@ -0,0 +1,81 @@
+/* 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/. */
+
+use fxhash::FxHashMap;
+/// Relative selector cache. This is useful for following cases.
+/// First case is non-subject relative selector: Imagine `.anchor:has(<..>) ~ .foo`, with DOM
+/// `.anchor + .foo + .. + .foo`. Each match on `.foo` triggers `:has()` traversal that
+/// yields the same result. This is simple enough, since we just need to store
+/// the exact match on that anchor pass/fail.
+/// Second case is `querySelectorAll`: Imagine `querySelectorAll(':has(.a)')`, with DOM
+/// `div > .. > div > .a`. When the we perform the traversal at the top div,
+/// we basically end up evaluating `:has(.a)` for all anchors, which could be reused.
+/// Also consider the sibling version: `querySelectorAll(':has(~ .a)')` with DOM
+/// `div + .. + div + .a`.
+/// TODO(dshin): Second case is not yet handled. That is tracked in Bug 1845291.
+use std::hash::Hash;
+
+use crate::parser::{RelativeSelector, SelectorKey};
+use crate::{tree::OpaqueElement, SelectorImpl};
+
+/// Match data for a given element and a selector.
+#[derive(Clone, Copy)]
+pub enum RelativeSelectorCachedMatch {
+ /// This selector matches this element.
+ Matched,
+ /// This selector does not match this element.
+ NotMatched,
+}
+
+impl RelativeSelectorCachedMatch {
+ /// Is the cached result a match?
+ pub fn matched(&self) -> bool {
+ matches!(*self, Self::Matched)
+ }
+}
+
+#[derive(Clone, Copy, Hash, Eq, PartialEq)]
+struct Key {
+ element: OpaqueElement,
+ selector: SelectorKey,
+}
+
+impl Key {
+ pub fn new<Impl: SelectorImpl>(
+ element: OpaqueElement,
+ selector: &RelativeSelector<Impl>,
+ ) -> Self {
+ Key {
+ element,
+ selector: SelectorKey::new(&selector.selector),
+ }
+ }
+}
+
+/// Cache to speed up matching of relative selectors.
+#[derive(Default)]
+pub struct RelativeSelectorCache {
+ cache: FxHashMap<Key, RelativeSelectorCachedMatch>,
+}
+
+impl RelativeSelectorCache {
+ /// Add a relative selector match into the cache.
+ pub fn add<Impl: SelectorImpl>(
+ &mut self,
+ anchor: OpaqueElement,
+ selector: &RelativeSelector<Impl>,
+ matched: RelativeSelectorCachedMatch,
+ ) {
+ self.cache.insert(Key::new(anchor, selector), matched);
+ }
+
+ /// Check if we have a cache entry for the element.
+ pub fn lookup<Impl: SelectorImpl>(
+ &mut self,
+ element: OpaqueElement,
+ selector: &RelativeSelector<Impl>,
+ ) -> Option<RelativeSelectorCachedMatch> {
+ self.cache.get(&Key::new(element, selector)).copied()
+ }
+}
diff --git a/servo/components/selectors/relative_selector/filter.rs b/servo/components/selectors/relative_selector/filter.rs
new file mode 100644
index 0000000000..3c3eb7d2fa
--- /dev/null
+++ b/servo/components/selectors/relative_selector/filter.rs
@@ -0,0 +1,159 @@
+/* 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/. */
+
+/// Bloom filter for relative selectors.
+use fxhash::FxHashMap;
+
+use crate::bloom::BloomFilter;
+use crate::context::QuirksMode;
+use crate::parser::{collect_selector_hashes, RelativeSelector, RelativeSelectorMatchHint};
+use crate::tree::{Element, OpaqueElement};
+use crate::SelectorImpl;
+
+enum Entry {
+ /// Filter lookup happened once. Construction of the filter is expensive,
+ /// so this is set when the element for subtree traversal is encountered.
+ Lookup,
+ /// Filter lookup happened more than once, and the filter for this element's
+ /// subtree traversal is constructed. Could use special handlings for pseudo-classes
+ /// such as `:hover` and `:focus`, see Bug 1845503.
+ HasFilter(Box<BloomFilter>),
+}
+
+#[derive(Clone, Copy, Hash, Eq, PartialEq)]
+enum TraversalKind {
+ Children,
+ Descendants,
+}
+
+fn add_to_filter<E: Element>(element: &E, filter: &mut BloomFilter, kind: TraversalKind) -> bool {
+ let mut child = element.first_element_child();
+ while let Some(e) = child {
+ if !e.add_element_unique_hashes(filter) {
+ return false;
+ }
+ if kind == TraversalKind::Descendants {
+ if !add_to_filter(&e, filter, kind) {
+ return false;
+ }
+ }
+ child = e.next_sibling_element();
+ }
+ true
+}
+
+#[derive(Clone, Copy, Hash, Eq, PartialEq)]
+struct Key(OpaqueElement, TraversalKind);
+
+/// Map of bloom filters for fast-rejecting relative selectors.
+#[derive(Default)]
+pub struct RelativeSelectorFilterMap {
+ map: FxHashMap<Key, Entry>,
+}
+
+fn fast_reject<Impl: SelectorImpl>(
+ selector: &RelativeSelector<Impl>,
+ quirks_mode: QuirksMode,
+ filter: &BloomFilter,
+) -> bool {
+ let mut hashes = [0u32; 4];
+ let mut len = 0;
+ // For inner selectors, we only collect from the single rightmost compound.
+ // This is because inner selectors can cause breakouts: e.g. `.anchor:has(:is(.a .b) .c)`
+ // can match when `.a` is the ancestor of `.anchor`. Including `.a` would possibly fast
+ // reject the subtree for not having `.a`, even if the selector would match.
+ // Technically, if the selector's traversal is non-sibling subtree, we can traverse
+ // inner selectors up to the point where a descendant/child combinator is encountered
+ // (e.g. In `.anchor:has(:is(.a ~ .b) .c)`, `.a` is guaranteed to be the a descendant
+ // of `.anchor`). While that can be separately handled, well, this is simpler.
+ collect_selector_hashes(
+ selector.selector.iter(),
+ quirks_mode,
+ &mut hashes,
+ &mut len,
+ |s| s.iter(),
+ );
+ for i in 0..len {
+ if !filter.might_contain_hash(hashes[i]) {
+ // Definitely rejected.
+ return true;
+ }
+ }
+ false
+}
+
+impl RelativeSelectorFilterMap {
+ fn get_filter<E: Element>(&mut self, element: &E, kind: TraversalKind) -> Option<&BloomFilter> {
+ // Insert flag to indicate that we looked up the filter once, and
+ // create the filter if and only if that flag is there.
+ let key = Key(element.opaque(), kind);
+ let entry = self
+ .map
+ .entry(key)
+ .and_modify(|entry| {
+ if !matches!(entry, Entry::Lookup) {
+ return;
+ }
+ let mut filter = BloomFilter::new();
+ // Go through all children/descendants of this element and add their hashes.
+ if add_to_filter(element, &mut filter, kind) {
+ *entry = Entry::HasFilter(Box::new(filter));
+ }
+ })
+ .or_insert(Entry::Lookup);
+ match entry {
+ Entry::Lookup => None,
+ Entry::HasFilter(ref filter) => Some(filter.as_ref()),
+ }
+ }
+
+ /// Potentially reject the given selector for this element.
+ /// This may seem redundant in presence of the cache, but the cache keys into the
+ /// selector-element pair specifically, while this filter keys to the element
+ /// and the traversal kind, so it is useful for handling multiple selectors
+ /// that effectively end up looking at the same(-ish, for siblings) subtree.
+ pub fn fast_reject<Impl: SelectorImpl, E: Element>(
+ &mut self,
+ element: &E,
+ selector: &RelativeSelector<Impl>,
+ quirks_mode: QuirksMode,
+ ) -> bool {
+ if matches!(
+ selector.match_hint,
+ RelativeSelectorMatchHint::InNextSibling
+ ) {
+ // Don't bother.
+ return false;
+ }
+ let is_sibling = matches!(
+ selector.match_hint,
+ RelativeSelectorMatchHint::InSibling |
+ RelativeSelectorMatchHint::InNextSiblingSubtree |
+ RelativeSelectorMatchHint::InSiblingSubtree
+ );
+ let is_subtree = matches!(
+ selector.match_hint,
+ RelativeSelectorMatchHint::InSubtree |
+ RelativeSelectorMatchHint::InNextSiblingSubtree |
+ RelativeSelectorMatchHint::InSiblingSubtree
+ );
+ let kind = if is_subtree {
+ TraversalKind::Descendants
+ } else {
+ TraversalKind::Children
+ };
+ if is_sibling {
+ // Contain the entirety of the parent's children/subtree in the filter, and use that.
+ // This is less likely to reject, especially for sibling subtree matches; however, it's less
+ // expensive memory-wise, compared to storing filters for each sibling.
+ element.parent_element().map_or(false, |parent| {
+ self.get_filter(&parent, kind)
+ .map_or(false, |filter| fast_reject(selector, quirks_mode, filter))
+ })
+ } else {
+ self.get_filter(element, kind)
+ .map_or(false, |filter| fast_reject(selector, quirks_mode, filter))
+ }
+ }
+}
diff --git a/servo/components/selectors/relative_selector/mod.rs b/servo/components/selectors/relative_selector/mod.rs
new file mode 100644
index 0000000000..6dd39f7327
--- /dev/null
+++ b/servo/components/selectors/relative_selector/mod.rs
@@ -0,0 +1,6 @@
+/* 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/. */
+
+pub mod cache;
+pub mod filter;
diff --git a/servo/components/selectors/sink.rs b/servo/components/selectors/sink.rs
new file mode 100644
index 0000000000..dcdd7ff259
--- /dev/null
+++ b/servo/components/selectors/sink.rs
@@ -0,0 +1,31 @@
+/* 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/. */
+
+//! Small helpers to abstract over different containers.
+#![deny(missing_docs)]
+
+use smallvec::{Array, SmallVec};
+
+/// A trait to abstract over a `push` method that may be implemented for
+/// different kind of types.
+///
+/// Used to abstract over `Array`, `SmallVec` and `Vec`, and also to implement a
+/// type which `push` method does only tweak a byte when we only need to check
+/// for the presence of something.
+pub trait Push<T> {
+ /// Push a value into self.
+ fn push(&mut self, value: T);
+}
+
+impl<T> Push<T> for Vec<T> {
+ fn push(&mut self, value: T) {
+ Vec::push(self, value);
+ }
+}
+
+impl<A: Array> Push<A::Item> for SmallVec<A> {
+ fn push(&mut self, value: A::Item) {
+ SmallVec::push(self, value);
+ }
+}
diff --git a/servo/components/selectors/tree.rs b/servo/components/selectors/tree.rs
new file mode 100644
index 0000000000..c1ea8ff5ae
--- /dev/null
+++ b/servo/components/selectors/tree.rs
@@ -0,0 +1,168 @@
+/* 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/. */
+
+//! Traits that nodes must implement. Breaks the otherwise-cyclic dependency
+//! between layout and style.
+
+use crate::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
+use crate::bloom::BloomFilter;
+use crate::matching::{ElementSelectorFlags, MatchingContext};
+use crate::parser::SelectorImpl;
+use std::fmt::Debug;
+use std::ptr::NonNull;
+
+/// Opaque representation of an Element, for identity comparisons.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct OpaqueElement(NonNull<()>);
+
+unsafe impl Send for OpaqueElement {}
+
+impl OpaqueElement {
+ /// Creates a new OpaqueElement from an arbitrarily-typed pointer.
+ pub fn new<T>(ptr: &T) -> Self {
+ unsafe {
+ OpaqueElement(NonNull::new_unchecked(
+ ptr as *const T as *const () as *mut (),
+ ))
+ }
+ }
+}
+
+pub trait Element: Sized + Clone + Debug {
+ type Impl: SelectorImpl;
+
+ /// Converts self into an opaque representation.
+ fn opaque(&self) -> OpaqueElement;
+
+ fn parent_element(&self) -> Option<Self>;
+
+ /// Whether the parent node of this element is a shadow root.
+ fn parent_node_is_shadow_root(&self) -> bool;
+
+ /// The host of the containing shadow root, if any.
+ fn containing_shadow_host(&self) -> Option<Self>;
+
+ /// The parent of a given pseudo-element, after matching a pseudo-element
+ /// selector.
+ ///
+ /// This is guaranteed to be called in a pseudo-element.
+ fn pseudo_element_originating_element(&self) -> Option<Self> {
+ debug_assert!(self.is_pseudo_element());
+ self.parent_element()
+ }
+
+ /// Whether we're matching on a pseudo-element.
+ fn is_pseudo_element(&self) -> bool;
+
+ /// Skips non-element nodes
+ fn prev_sibling_element(&self) -> Option<Self>;
+
+ /// Skips non-element nodes
+ fn next_sibling_element(&self) -> Option<Self>;
+
+ /// Skips non-element nodes
+ fn first_element_child(&self) -> Option<Self>;
+
+ fn is_html_element_in_html_document(&self) -> bool;
+
+ fn has_local_name(&self, local_name: &<Self::Impl as SelectorImpl>::BorrowedLocalName) -> bool;
+
+ /// Empty string for no namespace
+ fn has_namespace(&self, ns: &<Self::Impl as SelectorImpl>::BorrowedNamespaceUrl) -> bool;
+
+ /// Whether this element and the `other` element have the same local name and namespace.
+ fn is_same_type(&self, other: &Self) -> bool;
+
+ fn attr_matches(
+ &self,
+ ns: &NamespaceConstraint<&<Self::Impl as SelectorImpl>::NamespaceUrl>,
+ local_name: &<Self::Impl as SelectorImpl>::LocalName,
+ operation: &AttrSelectorOperation<&<Self::Impl as SelectorImpl>::AttrValue>,
+ ) -> bool;
+
+ fn has_attr_in_no_namespace(
+ &self,
+ local_name: &<Self::Impl as SelectorImpl>::LocalName,
+ ) -> bool {
+ self.attr_matches(
+ &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::<Self::Impl>()),
+ local_name,
+ &AttrSelectorOperation::Exists,
+ )
+ }
+
+ fn match_non_ts_pseudo_class(
+ &self,
+ pc: &<Self::Impl as SelectorImpl>::NonTSPseudoClass,
+ context: &mut MatchingContext<Self::Impl>,
+ ) -> bool;
+
+ fn match_pseudo_element(
+ &self,
+ pe: &<Self::Impl as SelectorImpl>::PseudoElement,
+ context: &mut MatchingContext<Self::Impl>,
+ ) -> bool;
+
+ /// Sets selector flags on the elemnt itself or the parent, depending on the
+ /// flags, which indicate what kind of work may need to be performed when
+ /// DOM state changes.
+ fn apply_selector_flags(&self, flags: ElementSelectorFlags);
+
+ /// Whether this element is a `link`.
+ fn is_link(&self) -> bool;
+
+ /// Returns whether the element is an HTML <slot> element.
+ fn is_html_slot_element(&self) -> bool;
+
+ /// Returns the assigned <slot> element this element is assigned to.
+ ///
+ /// Necessary for the `::slotted` pseudo-class.
+ fn assigned_slot(&self) -> Option<Self> {
+ None
+ }
+
+ fn has_id(
+ &self,
+ id: &<Self::Impl as SelectorImpl>::Identifier,
+ case_sensitivity: CaseSensitivity,
+ ) -> bool;
+
+ fn has_class(
+ &self,
+ name: &<Self::Impl as SelectorImpl>::Identifier,
+ case_sensitivity: CaseSensitivity,
+ ) -> bool;
+
+ /// Returns the mapping from the `exportparts` attribute in the reverse
+ /// direction, that is, in an outer-tree -> inner-tree direction.
+ fn imported_part(
+ &self,
+ name: &<Self::Impl as SelectorImpl>::Identifier,
+ ) -> Option<<Self::Impl as SelectorImpl>::Identifier>;
+
+ fn is_part(&self, name: &<Self::Impl as SelectorImpl>::Identifier) -> bool;
+
+ /// Returns whether this element matches `:empty`.
+ ///
+ /// That is, whether it does not contain any child element or any non-zero-length text node.
+ /// See http://dev.w3.org/csswg/selectors-3/#empty-pseudo
+ fn is_empty(&self) -> bool;
+
+ /// Returns whether this element matches `:root`,
+ /// i.e. whether it is the root element of a document.
+ ///
+ /// Note: this can be false even if `.parent_element()` is `None`
+ /// if the parent node is a `DocumentFragment`.
+ fn is_root(&self) -> bool;
+
+ /// Returns whether this element should ignore matching nth child
+ /// selector.
+ fn ignores_nth_child_selectors(&self) -> bool {
+ false
+ }
+
+ /// Add hashes unique to this element to the given filter, returning true
+ /// if any got added.
+ fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool;
+}
diff --git a/servo/components/selectors/visitor.rs b/servo/components/selectors/visitor.rs
new file mode 100644
index 0000000000..d5befbc68b
--- /dev/null
+++ b/servo/components/selectors/visitor.rs
@@ -0,0 +1,136 @@
+/* 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/. */
+
+//! Visitor traits for selectors.
+
+#![deny(missing_docs)]
+
+use crate::attr::NamespaceConstraint;
+use crate::parser::{Combinator, Component, RelativeSelector, Selector, SelectorImpl};
+
+/// A trait to visit selector properties.
+///
+/// All the `visit_foo` methods return a boolean indicating whether the
+/// traversal should continue or not.
+pub trait SelectorVisitor: Sized {
+ /// The selector implementation this visitor wants to visit.
+ type Impl: SelectorImpl;
+
+ /// Visit an attribute selector that may match (there are other selectors
+ /// that may never match, like those containing whitespace or the empty
+ /// string).
+ fn visit_attribute_selector(
+ &mut self,
+ _namespace: &NamespaceConstraint<&<Self::Impl as SelectorImpl>::NamespaceUrl>,
+ _local_name: &<Self::Impl as SelectorImpl>::LocalName,
+ _local_name_lower: &<Self::Impl as SelectorImpl>::LocalName,
+ ) -> bool {
+ true
+ }
+
+ /// Visit a simple selector.
+ fn visit_simple_selector(&mut self, _: &Component<Self::Impl>) -> bool {
+ true
+ }
+
+ /// Visit a nested relative selector list. The caller is responsible to call visit
+ /// into the internal selectors if / as needed.
+ ///
+ /// The default implementation skips it altogether.
+ fn visit_relative_selector_list(&mut self, _list: &[RelativeSelector<Self::Impl>]) -> bool {
+ true
+ }
+
+ /// Visit a nested selector list. The caller is responsible to call visit
+ /// into the internal selectors if / as needed.
+ ///
+ /// The default implementation does this.
+ fn visit_selector_list(
+ &mut self,
+ _list_kind: SelectorListKind,
+ list: &[Selector<Self::Impl>],
+ ) -> bool {
+ for nested in list {
+ if !nested.visit(self) {
+ return false;
+ }
+ }
+ true
+ }
+
+ /// Visits a complex selector.
+ ///
+ /// Gets the combinator to the right of the selector, or `None` if the
+ /// selector is the rightmost one.
+ fn visit_complex_selector(&mut self, _combinator_to_right: Option<Combinator>) -> bool {
+ true
+ }
+}
+
+bitflags! {
+ /// The kinds of components the visitor is visiting the selector list of, if any
+ #[derive(Clone, Copy, Default)]
+ pub struct SelectorListKind: u8 {
+ /// The visitor is inside :not(..)
+ const NEGATION = 1 << 0;
+ /// The visitor is inside :is(..)
+ const IS = 1 << 1;
+ /// The visitor is inside :where(..)
+ const WHERE = 1 << 2;
+ /// The visitor is inside :nth-child(.. of <selector list>) or
+ /// :nth-last-child(.. of <selector list>)
+ const NTH_OF = 1 << 3;
+ /// The visitor is inside :has(..)
+ const HAS = 1 << 4;
+ }
+}
+
+impl SelectorListKind {
+ /// Construct a SelectorListKind for the corresponding component.
+ pub fn from_component<Impl: SelectorImpl>(component: &Component<Impl>) -> Self {
+ match component {
+ Component::Negation(_) => SelectorListKind::NEGATION,
+ Component::Is(_) => SelectorListKind::IS,
+ Component::Where(_) => SelectorListKind::WHERE,
+ Component::NthOf(_) => SelectorListKind::NTH_OF,
+ _ => SelectorListKind::empty(),
+ }
+ }
+
+ /// Whether the visitor is inside :not(..)
+ pub fn in_negation(&self) -> bool {
+ self.intersects(SelectorListKind::NEGATION)
+ }
+
+ /// Whether the visitor is inside :is(..)
+ pub fn in_is(&self) -> bool {
+ self.intersects(SelectorListKind::IS)
+ }
+
+ /// Whether the visitor is inside :where(..)
+ pub fn in_where(&self) -> bool {
+ self.intersects(SelectorListKind::WHERE)
+ }
+
+ /// Whether the visitor is inside :nth-child(.. of <selector list>) or
+ /// :nth-last-child(.. of <selector list>)
+ pub fn in_nth_of(&self) -> bool {
+ self.intersects(SelectorListKind::NTH_OF)
+ }
+
+ /// Whether the visitor is inside :has(..)
+ pub fn in_has(&self) -> bool {
+ self.intersects(SelectorListKind::HAS)
+ }
+
+ /// Whether this nested selector is relevant for nth-of dependencies.
+ pub fn relevant_to_nth_of_dependencies(&self) -> bool {
+ // Order of nesting for `:has` and `:nth-child(.. of ..)` doesn't matter, because:
+ // * `:has(:nth-child(.. of ..))`: The location of the anchoring element is
+ // independent from where `:nth-child(.. of ..)` is applied.
+ // * `:nth-child(.. of :has(..))`: Invalidations inside `:has` must first use the
+ // `:has` machinary to find the anchor, then carry out the remaining invalidation.
+ self.in_nth_of() && !self.in_has()
+ }
+}
diff --git a/servo/components/servo_arc/Cargo.toml b/servo/components/servo_arc/Cargo.toml
new file mode 100644
index 0000000000..03b1004bac
--- /dev/null
+++ b/servo/components/servo_arc/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "servo_arc"
+version = "0.1.1"
+authors = ["The Servo Project Developers"]
+license = "MIT/Apache-2.0"
+repository = "https://github.com/servo/servo"
+description = "A fork of std::sync::Arc with some extra functionality and without weak references"
+
+[lib]
+name = "servo_arc"
+path = "lib.rs"
+
+[features]
+gecko_refcount_logging = []
+servo = ["serde"]
+
+[dependencies]
+serde = { version = "1.0", optional = true }
+stable_deref_trait = "1.0.0"
diff --git a/servo/components/servo_arc/lib.rs b/servo/components/servo_arc/lib.rs
new file mode 100644
index 0000000000..1438ccebfd
--- /dev/null
+++ b/servo/components/servo_arc/lib.rs
@@ -0,0 +1,1195 @@
+// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+//! Fork of Arc for Servo. This has the following advantages over std::sync::Arc:
+//!
+//! * We don't waste storage on the weak reference count.
+//! * We don't do extra RMU operations to handle the possibility of weak references.
+//! * We can experiment with arena allocation (todo).
+//! * We can add methods to support our custom use cases [1].
+//! * We have support for dynamically-sized types (see from_header_and_iter).
+//! * We have support for thin arcs to unsized types (see ThinArc).
+//! * We have support for references to static data, which don't do any
+//! refcounting.
+//!
+//! [1]: https://bugzilla.mozilla.org/show_bug.cgi?id=1360883
+
+// The semantics of `Arc` are already documented in the Rust docs, so we don't
+// duplicate those here.
+#![allow(missing_docs)]
+
+#[cfg(feature = "servo")]
+extern crate serde;
+extern crate stable_deref_trait;
+
+#[cfg(feature = "servo")]
+use serde::{Deserialize, Serialize};
+use stable_deref_trait::{CloneStableDeref, StableDeref};
+use std::alloc::{self, Layout};
+use std::borrow;
+use std::cmp::Ordering;
+use std::convert::From;
+use std::fmt;
+use std::hash::{Hash, Hasher};
+use std::iter::{ExactSizeIterator, Iterator};
+use std::marker::PhantomData;
+use std::mem::{self, align_of, size_of};
+use std::ops::{Deref, DerefMut};
+use std::os::raw::c_void;
+use std::process;
+use std::ptr;
+use std::sync::atomic;
+use std::sync::atomic::Ordering::{Acquire, Relaxed, Release};
+use std::{isize, usize};
+
+/// A soft limit on the amount of references that may be made to an `Arc`.
+///
+/// Going above this limit will abort your program (although not
+/// necessarily) at _exactly_ `MAX_REFCOUNT + 1` references.
+const MAX_REFCOUNT: usize = (isize::MAX) as usize;
+
+/// Special refcount value that means the data is not reference counted,
+/// and that the `Arc` is really acting as a read-only static reference.
+const STATIC_REFCOUNT: usize = usize::MAX;
+
+/// An atomically reference counted shared pointer
+///
+/// See the documentation for [`Arc`] in the standard library. Unlike the
+/// standard library `Arc`, this `Arc` does not support weak reference counting.
+///
+/// See the discussion in https://github.com/rust-lang/rust/pull/60594 for the
+/// usage of PhantomData.
+///
+/// [`Arc`]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html
+///
+/// cbindgen:derive-eq=false
+/// cbindgen:derive-neq=false
+#[repr(C)]
+pub struct Arc<T: ?Sized> {
+ p: ptr::NonNull<ArcInner<T>>,
+ phantom: PhantomData<T>,
+}
+
+/// An `Arc` that is known to be uniquely owned
+///
+/// When `Arc`s are constructed, they are known to be
+/// uniquely owned. In such a case it is safe to mutate
+/// the contents of the `Arc`. Normally, one would just handle
+/// this by mutating the data on the stack before allocating the
+/// `Arc`, however it's possible the data is large or unsized
+/// and you need to heap-allocate it earlier in such a way
+/// that it can be freely converted into a regular `Arc` once you're
+/// done.
+///
+/// `UniqueArc` exists for this purpose, when constructed it performs
+/// the same allocations necessary for an `Arc`, however it allows mutable access.
+/// Once the mutation is finished, you can call `.shareable()` and get a regular `Arc`
+/// out of it.
+///
+/// Ignore the doctest below there's no way to skip building with refcount
+/// logging during doc tests (see rust-lang/rust#45599).
+///
+/// ```rust,ignore
+/// # use servo_arc::UniqueArc;
+/// let data = [1, 2, 3, 4, 5];
+/// let mut x = UniqueArc::new(data);
+/// x[4] = 7; // mutate!
+/// let y = x.shareable(); // y is an Arc<T>
+/// ```
+pub struct UniqueArc<T: ?Sized>(Arc<T>);
+
+impl<T> UniqueArc<T> {
+ #[inline]
+ /// Construct a new UniqueArc
+ pub fn new(data: T) -> Self {
+ UniqueArc(Arc::new(data))
+ }
+
+ /// Construct an uninitialized arc
+ #[inline]
+ pub fn new_uninit() -> UniqueArc<mem::MaybeUninit<T>> {
+ unsafe {
+ let layout = Layout::new::<ArcInner<mem::MaybeUninit<T>>>();
+ let ptr = alloc::alloc(layout);
+ let mut p = ptr::NonNull::new(ptr)
+ .unwrap_or_else(|| alloc::handle_alloc_error(layout))
+ .cast::<ArcInner<mem::MaybeUninit<T>>>();
+ ptr::write(&mut p.as_mut().count, atomic::AtomicUsize::new(1));
+
+ #[cfg(feature = "gecko_refcount_logging")]
+ {
+ NS_LogCtor(p.as_ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8)
+ }
+
+ UniqueArc(Arc {
+ p,
+ phantom: PhantomData,
+ })
+ }
+ }
+
+ #[inline]
+ /// Convert to a shareable Arc<T> once we're done mutating it
+ pub fn shareable(self) -> Arc<T> {
+ self.0
+ }
+}
+
+impl<T> UniqueArc<mem::MaybeUninit<T>> {
+ /// Convert to an initialized Arc.
+ #[inline]
+ pub unsafe fn assume_init(this: Self) -> UniqueArc<T> {
+ UniqueArc(Arc {
+ p: mem::ManuallyDrop::new(this).0.p.cast(),
+ phantom: PhantomData,
+ })
+ }
+}
+
+impl<T> Deref for UniqueArc<T> {
+ type Target = T;
+ fn deref(&self) -> &T {
+ &*self.0
+ }
+}
+
+impl<T> DerefMut for UniqueArc<T> {
+ fn deref_mut(&mut self) -> &mut T {
+ // We know this to be uniquely owned
+ unsafe { &mut (*self.0.ptr()).data }
+ }
+}
+
+unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
+unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}
+
+/// The object allocated by an Arc<T>
+#[repr(C)]
+struct ArcInner<T: ?Sized> {
+ count: atomic::AtomicUsize,
+ data: T,
+}
+
+unsafe impl<T: ?Sized + Sync + Send> Send for ArcInner<T> {}
+unsafe impl<T: ?Sized + Sync + Send> Sync for ArcInner<T> {}
+
+/// Computes the offset of the data field within ArcInner.
+fn data_offset<T>() -> usize {
+ let size = size_of::<ArcInner<()>>();
+ let align = align_of::<T>();
+ // https://github.com/rust-lang/rust/blob/1.36.0/src/libcore/alloc.rs#L187-L207
+ size.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1)
+}
+
+impl<T> Arc<T> {
+ /// Construct an `Arc<T>`
+ #[inline]
+ pub fn new(data: T) -> Self {
+ let ptr = Box::into_raw(Box::new(ArcInner {
+ count: atomic::AtomicUsize::new(1),
+ data,
+ }));
+
+ #[cfg(feature = "gecko_refcount_logging")]
+ unsafe {
+ // FIXME(emilio): Would be so amazing to have
+ // std::intrinsics::type_name() around, so that we could also report
+ // a real size.
+ NS_LogCtor(ptr as *mut _, b"ServoArc\0".as_ptr() as *const _, 8);
+ }
+
+ unsafe {
+ Arc {
+ p: ptr::NonNull::new_unchecked(ptr),
+ phantom: PhantomData,
+ }
+ }
+ }
+
+ /// Construct an intentionally-leaked arc.
+ #[inline]
+ pub fn new_leaked(data: T) -> Self {
+ let arc = Self::new(data);
+ arc.mark_as_intentionally_leaked();
+ arc
+ }
+
+ /// Convert the Arc<T> to a raw pointer, suitable for use across FFI
+ ///
+ /// Note: This returns a pointer to the data T, which is offset in the allocation.
+ #[inline]
+ pub fn into_raw(this: Self) -> *const T {
+ let ptr = unsafe { &((*this.ptr()).data) as *const _ };
+ mem::forget(this);
+ ptr
+ }
+
+ /// Reconstruct the Arc<T> from a raw pointer obtained from into_raw()
+ ///
+ /// Note: This raw pointer will be offset in the allocation and must be preceded
+ /// by the atomic count.
+ #[inline]
+ pub unsafe fn from_raw(ptr: *const T) -> Self {
+ // To find the corresponding pointer to the `ArcInner` we need
+ // to subtract the offset of the `data` field from the pointer.
+ let ptr = (ptr as *const u8).sub(data_offset::<T>());
+ Arc {
+ p: ptr::NonNull::new_unchecked(ptr as *mut ArcInner<T>),
+ phantom: PhantomData,
+ }
+ }
+
+ /// Like from_raw, but returns an addrefed arc instead.
+ #[inline]
+ pub unsafe fn from_raw_addrefed(ptr: *const T) -> Self {
+ let arc = Self::from_raw(ptr);
+ mem::forget(arc.clone());
+ arc
+ }
+
+ /// Create a new static Arc<T> (one that won't reference count the object)
+ /// and place it in the allocation provided by the specified `alloc`
+ /// function.
+ ///
+ /// `alloc` must return a pointer into a static allocation suitable for
+ /// storing data with the `Layout` passed into it. The pointer returned by
+ /// `alloc` will not be freed.
+ #[inline]
+ pub unsafe fn new_static<F>(alloc: F, data: T) -> Arc<T>
+ where
+ F: FnOnce(Layout) -> *mut u8,
+ {
+ let ptr = alloc(Layout::new::<ArcInner<T>>()) as *mut ArcInner<T>;
+
+ let x = ArcInner {
+ count: atomic::AtomicUsize::new(STATIC_REFCOUNT),
+ data,
+ };
+
+ ptr::write(ptr, x);
+
+ Arc {
+ p: ptr::NonNull::new_unchecked(ptr),
+ phantom: PhantomData,
+ }
+ }
+
+ /// Produce a pointer to the data that can be converted back
+ /// to an Arc. This is basically an `&Arc<T>`, without the extra indirection.
+ /// It has the benefits of an `&T` but also knows about the underlying refcount
+ /// and can be converted into more `Arc<T>`s if necessary.
+ #[inline]
+ pub fn borrow_arc<'a>(&'a self) -> ArcBorrow<'a, T> {
+ ArcBorrow(&**self)
+ }
+
+ /// Returns the address on the heap of the Arc itself -- not the T within it -- for memory
+ /// reporting.
+ ///
+ /// If this is a static reference, this returns null.
+ pub fn heap_ptr(&self) -> *const c_void {
+ if self.inner().count.load(Relaxed) == STATIC_REFCOUNT {
+ ptr::null()
+ } else {
+ self.p.as_ptr() as *const ArcInner<T> as *const c_void
+ }
+ }
+}
+
+impl<T: ?Sized> Arc<T> {
+ #[inline]
+ fn inner(&self) -> &ArcInner<T> {
+ // This unsafety is ok because while this arc is alive we're guaranteed
+ // that the inner pointer is valid. Furthermore, we know that the
+ // `ArcInner` structure itself is `Sync` because the inner data is
+ // `Sync` as well, so we're ok loaning out an immutable pointer to these
+ // contents.
+ unsafe { &*self.ptr() }
+ }
+
+ #[inline(always)]
+ fn record_drop(&self) {
+ #[cfg(feature = "gecko_refcount_logging")]
+ unsafe {
+ NS_LogDtor(self.ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8);
+ }
+ }
+
+ /// Marks this `Arc` as intentionally leaked for the purposes of refcount
+ /// logging.
+ ///
+ /// It's a logic error to call this more than once, but it's not unsafe, as
+ /// it'd just report negative leaks.
+ #[inline(always)]
+ pub fn mark_as_intentionally_leaked(&self) {
+ self.record_drop();
+ }
+
+ // Non-inlined part of `drop`. Just invokes the destructor and calls the
+ // refcount logging machinery if enabled.
+ #[inline(never)]
+ unsafe fn drop_slow(&mut self) {
+ self.record_drop();
+ let _ = Box::from_raw(self.ptr());
+ }
+
+ /// Test pointer equality between the two Arcs, i.e. they must be the _same_
+ /// allocation
+ #[inline]
+ pub fn ptr_eq(this: &Self, other: &Self) -> bool {
+ this.ptr() as *const () == other.ptr() as *const ()
+ }
+
+ fn ptr(&self) -> *mut ArcInner<T> {
+ self.p.as_ptr()
+ }
+
+ /// Returns a raw ptr to the underlying allocation.
+ pub fn raw_ptr(&self) -> *const c_void {
+ self.p.as_ptr() as *const _
+ }
+}
+
+#[cfg(feature = "gecko_refcount_logging")]
+extern "C" {
+ fn NS_LogCtor(
+ aPtr: *mut std::os::raw::c_void,
+ aTypeName: *const std::os::raw::c_char,
+ aSize: u32,
+ );
+ fn NS_LogDtor(
+ aPtr: *mut std::os::raw::c_void,
+ aTypeName: *const std::os::raw::c_char,
+ aSize: u32,
+ );
+}
+
+impl<T: ?Sized> Clone for Arc<T> {
+ #[inline]
+ fn clone(&self) -> Self {
+ // NOTE(emilio): If you change anything here, make sure that the
+ // implementation in layout/style/ServoStyleConstsInlines.h matches!
+ //
+ // Using a relaxed ordering to check for STATIC_REFCOUNT is safe, since
+ // `count` never changes between STATIC_REFCOUNT and other values.
+ if self.inner().count.load(Relaxed) != STATIC_REFCOUNT {
+ // Using a relaxed ordering is alright here, as knowledge of the
+ // original reference prevents other threads from erroneously deleting
+ // the object.
+ //
+ // As explained in the [Boost documentation][1], Increasing the
+ // reference counter can always be done with memory_order_relaxed: New
+ // references to an object can only be formed from an existing
+ // reference, and passing an existing reference from one thread to
+ // another must already provide any required synchronization.
+ //
+ // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html)
+ let old_size = self.inner().count.fetch_add(1, Relaxed);
+
+ // However we need to guard against massive refcounts in case someone
+ // is `mem::forget`ing Arcs. If we don't do this the count can overflow
+ // and users will use-after free. We racily saturate to `isize::MAX` on
+ // the assumption that there aren't ~2 billion threads incrementing
+ // the reference count at once. This branch will never be taken in
+ // any realistic program.
+ //
+ // We abort because such a program is incredibly degenerate, and we
+ // don't care to support it.
+ if old_size > MAX_REFCOUNT {
+ process::abort();
+ }
+ }
+
+ unsafe {
+ Arc {
+ p: ptr::NonNull::new_unchecked(self.ptr()),
+ phantom: PhantomData,
+ }
+ }
+ }
+}
+
+impl<T: ?Sized> Deref for Arc<T> {
+ type Target = T;
+
+ #[inline]
+ fn deref(&self) -> &T {
+ &self.inner().data
+ }
+}
+
+impl<T: Clone> Arc<T> {
+ /// Makes a mutable reference to the `Arc`, cloning if necessary
+ ///
+ /// This is functionally equivalent to [`Arc::make_mut`][mm] from the standard library.
+ ///
+ /// If this `Arc` is uniquely owned, `make_mut()` will provide a mutable
+ /// reference to the contents. If not, `make_mut()` will create a _new_ `Arc`
+ /// with a copy of the contents, update `this` to point to it, and provide
+ /// a mutable reference to its contents.
+ ///
+ /// This is useful for implementing copy-on-write schemes where you wish to
+ /// avoid copying things if your `Arc` is not shared.
+ ///
+ /// [mm]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html#method.make_mut
+ #[inline]
+ pub fn make_mut(this: &mut Self) -> &mut T {
+ if !this.is_unique() {
+ // Another pointer exists; clone
+ *this = Arc::new((**this).clone());
+ }
+
+ unsafe {
+ // This unsafety is ok because we're guaranteed that the pointer
+ // returned is the *only* pointer that will ever be returned to T. Our
+ // reference count is guaranteed to be 1 at this point, and we required
+ // the Arc itself to be `mut`, so we're returning the only possible
+ // reference to the inner data.
+ &mut (*this.ptr()).data
+ }
+ }
+}
+
+impl<T: ?Sized> Arc<T> {
+ /// Provides mutable access to the contents _if_ the `Arc` is uniquely owned.
+ #[inline]
+ pub fn get_mut(this: &mut Self) -> Option<&mut T> {
+ if this.is_unique() {
+ unsafe {
+ // See make_mut() for documentation of the threadsafety here.
+ Some(&mut (*this.ptr()).data)
+ }
+ } else {
+ None
+ }
+ }
+
+ /// Whether or not the `Arc` is a static reference.
+ #[inline]
+ pub fn is_static(&self) -> bool {
+ // Using a relaxed ordering to check for STATIC_REFCOUNT is safe, since
+ // `count` never changes between STATIC_REFCOUNT and other values.
+ self.inner().count.load(Relaxed) == STATIC_REFCOUNT
+ }
+
+ /// Whether or not the `Arc` is uniquely owned (is the refcount 1?) and not
+ /// a static reference.
+ #[inline]
+ pub fn is_unique(&self) -> bool {
+ // See the extensive discussion in [1] for why this needs to be Acquire.
+ //
+ // [1] https://github.com/servo/servo/issues/21186
+ self.inner().count.load(Acquire) == 1
+ }
+}
+
+impl<T: ?Sized> Drop for Arc<T> {
+ #[inline]
+ fn drop(&mut self) {
+ // NOTE(emilio): If you change anything here, make sure that the
+ // implementation in layout/style/ServoStyleConstsInlines.h matches!
+ if self.is_static() {
+ return;
+ }
+
+ // Because `fetch_sub` is already atomic, we do not need to synchronize
+ // with other threads unless we are going to delete the object.
+ if self.inner().count.fetch_sub(1, Release) != 1 {
+ return;
+ }
+
+ // FIXME(bholley): Use the updated comment when [2] is merged.
+ //
+ // This load is needed to prevent reordering of use of the data and
+ // deletion of the data. Because it is marked `Release`, the decreasing
+ // of the reference count synchronizes with this `Acquire` load. This
+ // means that use of the data happens before decreasing the reference
+ // count, which happens before this load, which happens before the
+ // deletion of the data.
+ //
+ // As explained in the [Boost documentation][1],
+ //
+ // > It is important to enforce any possible access to the object in one
+ // > thread (through an existing reference) to *happen before* deleting
+ // > the object in a different thread. This is achieved by a "release"
+ // > operation after dropping a reference (any access to the object
+ // > through this reference must obviously happened before), and an
+ // > "acquire" operation before deleting the object.
+ //
+ // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html)
+ // [2]: https://github.com/rust-lang/rust/pull/41714
+ self.inner().count.load(Acquire);
+
+ unsafe {
+ self.drop_slow();
+ }
+ }
+}
+
+impl<T: ?Sized + PartialEq> PartialEq for Arc<T> {
+ fn eq(&self, other: &Arc<T>) -> bool {
+ Self::ptr_eq(self, other) || *(*self) == *(*other)
+ }
+
+ fn ne(&self, other: &Arc<T>) -> bool {
+ !Self::ptr_eq(self, other) && *(*self) != *(*other)
+ }
+}
+
+impl<T: ?Sized + PartialOrd> PartialOrd for Arc<T> {
+ fn partial_cmp(&self, other: &Arc<T>) -> Option<Ordering> {
+ (**self).partial_cmp(&**other)
+ }
+
+ fn lt(&self, other: &Arc<T>) -> bool {
+ *(*self) < *(*other)
+ }
+
+ fn le(&self, other: &Arc<T>) -> bool {
+ *(*self) <= *(*other)
+ }
+
+ fn gt(&self, other: &Arc<T>) -> bool {
+ *(*self) > *(*other)
+ }
+
+ fn ge(&self, other: &Arc<T>) -> bool {
+ *(*self) >= *(*other)
+ }
+}
+impl<T: ?Sized + Ord> Ord for Arc<T> {
+ fn cmp(&self, other: &Arc<T>) -> Ordering {
+ (**self).cmp(&**other)
+ }
+}
+impl<T: ?Sized + Eq> Eq for Arc<T> {}
+
+impl<T: ?Sized + fmt::Display> fmt::Display for Arc<T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Display::fmt(&**self, f)
+ }
+}
+
+impl<T: ?Sized + fmt::Debug> fmt::Debug for Arc<T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Debug::fmt(&**self, f)
+ }
+}
+
+impl<T: ?Sized> fmt::Pointer for Arc<T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Pointer::fmt(&self.ptr(), f)
+ }
+}
+
+impl<T: Default> Default for Arc<T> {
+ fn default() -> Arc<T> {
+ Arc::new(Default::default())
+ }
+}
+
+impl<T: ?Sized + Hash> Hash for Arc<T> {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ (**self).hash(state)
+ }
+}
+
+impl<T> From<T> for Arc<T> {
+ #[inline]
+ fn from(t: T) -> Self {
+ Arc::new(t)
+ }
+}
+
+impl<T: ?Sized> borrow::Borrow<T> for Arc<T> {
+ #[inline]
+ fn borrow(&self) -> &T {
+ &**self
+ }
+}
+
+impl<T: ?Sized> AsRef<T> for Arc<T> {
+ #[inline]
+ fn as_ref(&self) -> &T {
+ &**self
+ }
+}
+
+unsafe impl<T: ?Sized> StableDeref for Arc<T> {}
+unsafe impl<T: ?Sized> CloneStableDeref for Arc<T> {}
+
+#[cfg(feature = "servo")]
+impl<'de, T: Deserialize<'de>> Deserialize<'de> for Arc<T> {
+ fn deserialize<D>(deserializer: D) -> Result<Arc<T>, D::Error>
+ where
+ D: ::serde::de::Deserializer<'de>,
+ {
+ T::deserialize(deserializer).map(Arc::new)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<T: Serialize> Serialize for Arc<T> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: ::serde::ser::Serializer,
+ {
+ (**self).serialize(serializer)
+ }
+}
+
+/// Structure to allow Arc-managing some fixed-sized data and a variably-sized
+/// slice in a single allocation.
+///
+/// cbindgen:derive-eq=false
+/// cbindgen:derive-neq=false
+#[derive(Debug, Eq)]
+#[repr(C)]
+pub struct HeaderSlice<H, T> {
+ /// The fixed-sized data.
+ pub header: H,
+
+ /// The length of the slice at our end.
+ len: usize,
+
+ /// The dynamically-sized data.
+ data: [T; 0],
+}
+
+impl<H: PartialEq, T: PartialEq> PartialEq for HeaderSlice<H, T> {
+ fn eq(&self, other: &Self) -> bool {
+ self.header == other.header && self.slice() == other.slice()
+ }
+}
+
+impl<H, T> Drop for HeaderSlice<H, T> {
+ fn drop(&mut self) {
+ unsafe {
+ let mut ptr = self.data_mut();
+ for _ in 0..self.len {
+ std::ptr::drop_in_place(ptr);
+ ptr = ptr.offset(1);
+ }
+ }
+ }
+}
+
+impl<H, T> HeaderSlice<H, T> {
+ /// Returns the dynamically sized slice in this HeaderSlice.
+ #[inline(always)]
+ pub fn slice(&self) -> &[T] {
+ unsafe { std::slice::from_raw_parts(self.data(), self.len) }
+ }
+
+ #[inline(always)]
+ fn data(&self) -> *const T {
+ std::ptr::addr_of!(self.data) as *const _
+ }
+
+ #[inline(always)]
+ fn data_mut(&mut self) -> *mut T {
+ std::ptr::addr_of_mut!(self.data) as *mut _
+ }
+
+ /// Returns the dynamically sized slice in this HeaderSlice.
+ #[inline(always)]
+ pub fn slice_mut(&mut self) -> &mut [T] {
+ unsafe { std::slice::from_raw_parts_mut(self.data_mut(), self.len) }
+ }
+
+ /// Returns the len of the slice.
+ #[inline(always)]
+ pub fn len(&self) -> usize {
+ self.len
+ }
+}
+
+#[inline(always)]
+fn divide_rounding_up(dividend: usize, divisor: usize) -> usize {
+ (dividend + divisor - 1) / divisor
+}
+
+impl<H, T> Arc<HeaderSlice<H, T>> {
+ /// Creates an Arc for a HeaderSlice using the given header struct and
+ /// iterator to generate the slice.
+ ///
+ /// `is_static` indicates whether to create a static Arc.
+ ///
+ /// `alloc` is used to get a pointer to the memory into which the
+ /// dynamically sized ArcInner<HeaderSlice<H, T>> value will be
+ /// written. If `is_static` is true, then `alloc` must return a
+ /// pointer into some static memory allocation. If it is false,
+ /// then `alloc` must return an allocation that can be dellocated
+ /// by calling Box::from_raw::<ArcInner<HeaderSlice<H, T>>> on it.
+ #[inline]
+ pub fn from_header_and_iter_alloc<F, I>(
+ alloc: F,
+ header: H,
+ mut items: I,
+ num_items: usize,
+ is_static: bool,
+ ) -> Self
+ where
+ F: FnOnce(Layout) -> *mut u8,
+ I: Iterator<Item = T>,
+ {
+ assert_ne!(size_of::<T>(), 0, "Need to think about ZST");
+
+ let size = size_of::<ArcInner<HeaderSlice<H, T>>>() + size_of::<T>() * num_items;
+ let inner_align = align_of::<ArcInner<HeaderSlice<H, T>>>();
+ debug_assert!(inner_align >= align_of::<T>());
+
+ let ptr: *mut ArcInner<HeaderSlice<H, T>>;
+ unsafe {
+ // Allocate the buffer.
+ let layout = if inner_align <= align_of::<usize>() {
+ Layout::from_size_align_unchecked(size, align_of::<usize>())
+ } else if inner_align <= align_of::<u64>() {
+ // On 32-bit platforms <T> may have 8 byte alignment while usize
+ // has 4 byte aligment. Use u64 to avoid over-alignment.
+ // This branch will compile away in optimized builds.
+ Layout::from_size_align_unchecked(size, align_of::<u64>())
+ } else {
+ panic!("Over-aligned type not handled");
+ };
+
+ let buffer = alloc(layout);
+ ptr = buffer as *mut ArcInner<HeaderSlice<H, T>>;
+
+ // Write the data.
+ //
+ // Note that any panics here (i.e. from the iterator) are safe, since
+ // we'll just leak the uninitialized memory.
+ let count = if is_static {
+ atomic::AtomicUsize::new(STATIC_REFCOUNT)
+ } else {
+ atomic::AtomicUsize::new(1)
+ };
+ ptr::write(&mut ((*ptr).count), count);
+ ptr::write(&mut ((*ptr).data.header), header);
+ ptr::write(&mut ((*ptr).data.len), num_items);
+ if num_items != 0 {
+ let mut current = std::ptr::addr_of_mut!((*ptr).data.data) as *mut T;
+ for _ in 0..num_items {
+ ptr::write(
+ current,
+ items
+ .next()
+ .expect("ExactSizeIterator over-reported length"),
+ );
+ current = current.offset(1);
+ }
+ // We should have consumed the buffer exactly, maybe accounting
+ // for some padding from the alignment.
+ debug_assert!(
+ (buffer.add(size) as usize - current as *mut u8 as usize) < inner_align
+ );
+ }
+ assert!(
+ items.next().is_none(),
+ "ExactSizeIterator under-reported length"
+ );
+ }
+ #[cfg(feature = "gecko_refcount_logging")]
+ unsafe {
+ if !is_static {
+ // FIXME(emilio): Would be so amazing to have
+ // std::intrinsics::type_name() around.
+ NS_LogCtor(ptr as *mut _, b"ServoArc\0".as_ptr() as *const _, 8)
+ }
+ }
+
+ // Return the fat Arc.
+ assert_eq!(
+ size_of::<Self>(),
+ size_of::<usize>(),
+ "The Arc should be thin"
+ );
+ unsafe {
+ Arc {
+ p: ptr::NonNull::new_unchecked(ptr),
+ phantom: PhantomData,
+ }
+ }
+ }
+
+ /// Creates an Arc for a HeaderSlice using the given header struct and iterator to generate the
+ /// slice. Panics if num_items doesn't match the number of items.
+ #[inline]
+ pub fn from_header_and_iter_with_size<I>(header: H, items: I, num_items: usize) -> Self
+ where
+ I: Iterator<Item = T>,
+ {
+ Arc::from_header_and_iter_alloc(
+ |layout| {
+ // align will only ever be align_of::<usize>() or align_of::<u64>()
+ let align = layout.align();
+ unsafe {
+ if align == mem::align_of::<usize>() {
+ Self::allocate_buffer::<usize>(layout.size())
+ } else {
+ assert_eq!(align, mem::align_of::<u64>());
+ Self::allocate_buffer::<u64>(layout.size())
+ }
+ }
+ },
+ header,
+ items,
+ num_items,
+ /* is_static = */ false,
+ )
+ }
+
+ /// Creates an Arc for a HeaderSlice using the given header struct and
+ /// iterator to generate the slice. The resulting Arc will be fat.
+ #[inline]
+ pub fn from_header_and_iter<I>(header: H, items: I) -> Self
+ where
+ I: Iterator<Item = T> + ExactSizeIterator,
+ {
+ let len = items.len();
+ Self::from_header_and_iter_with_size(header, items, len)
+ }
+
+ #[inline]
+ unsafe fn allocate_buffer<W>(size: usize) -> *mut u8 {
+ // We use Vec because the underlying allocation machinery isn't
+ // available in stable Rust. To avoid alignment issues, we allocate
+ // words rather than bytes, rounding up to the nearest word size.
+ let words_to_allocate = divide_rounding_up(size, mem::size_of::<W>());
+ let mut vec = Vec::<W>::with_capacity(words_to_allocate);
+ vec.set_len(words_to_allocate);
+ Box::into_raw(vec.into_boxed_slice()) as *mut W as *mut u8
+ }
+}
+
+/// This is functionally equivalent to Arc<(H, [T])>
+///
+/// When you create an `Arc` containing a dynamically sized type like a slice, the `Arc` is
+/// represented on the stack as a "fat pointer", where the length of the slice is stored alongside
+/// the `Arc`'s pointer. In some situations you may wish to have a thin pointer instead, perhaps
+/// for FFI compatibility or space efficiency. `ThinArc` solves this by storing the length in the
+/// allocation itself, via `HeaderSlice`.
+pub type ThinArc<H, T> = Arc<HeaderSlice<H, T>>;
+
+/// See `ArcUnion`. This is a version that works for `ThinArc`s.
+pub type ThinArcUnion<H1, T1, H2, T2> = ArcUnion<HeaderSlice<H1, T1>, HeaderSlice<H2, T2>>;
+
+impl<H, T> UniqueArc<HeaderSlice<H, T>> {
+ #[inline]
+ pub fn from_header_and_iter<I>(header: H, items: I) -> Self
+ where
+ I: Iterator<Item = T> + ExactSizeIterator,
+ {
+ Self(Arc::from_header_and_iter(header, items))
+ }
+
+ #[inline]
+ pub fn from_header_and_iter_with_size<I>(header: H, items: I, num_items: usize) -> Self
+ where
+ I: Iterator<Item = T>,
+ {
+ Self(Arc::from_header_and_iter_with_size(
+ header, items, num_items,
+ ))
+ }
+
+ /// Returns a mutable reference to the header.
+ pub fn header_mut(&mut self) -> &mut H {
+ // We know this to be uniquely owned
+ unsafe { &mut (*self.0.ptr()).data.header }
+ }
+
+ /// Returns a mutable reference to the slice.
+ pub fn data_mut(&mut self) -> &mut [T] {
+ // We know this to be uniquely owned
+ unsafe { (*self.0.ptr()).data.slice_mut() }
+ }
+}
+
+/// A "borrowed `Arc`". This is a pointer to
+/// a T that is known to have been allocated within an
+/// `Arc`.
+///
+/// This is equivalent in guarantees to `&Arc<T>`, however it is
+/// a bit more flexible. To obtain an `&Arc<T>` you must have
+/// an `Arc<T>` instance somewhere pinned down until we're done with it.
+/// It's also a direct pointer to `T`, so using this involves less pointer-chasing
+///
+/// However, C++ code may hand us refcounted things as pointers to T directly,
+/// so we have to conjure up a temporary `Arc` on the stack each time.
+///
+/// `ArcBorrow` lets us deal with borrows of known-refcounted objects
+/// without needing to worry about where the `Arc<T>` is.
+#[derive(Debug, Eq, PartialEq)]
+pub struct ArcBorrow<'a, T: 'a>(&'a T);
+
+impl<'a, T> Copy for ArcBorrow<'a, T> {}
+impl<'a, T> Clone for ArcBorrow<'a, T> {
+ #[inline]
+ fn clone(&self) -> Self {
+ *self
+ }
+}
+
+impl<'a, T> ArcBorrow<'a, T> {
+ /// Clone this as an `Arc<T>`. This bumps the refcount.
+ #[inline]
+ pub fn clone_arc(&self) -> Arc<T> {
+ let arc = unsafe { Arc::from_raw(self.0) };
+ // addref it!
+ mem::forget(arc.clone());
+ arc
+ }
+
+ /// For constructing from a reference known to be Arc-backed,
+ /// e.g. if we obtain such a reference over FFI
+ #[inline]
+ pub unsafe fn from_ref(r: &'a T) -> Self {
+ ArcBorrow(r)
+ }
+
+ /// Compare two `ArcBorrow`s via pointer equality. Will only return
+ /// true if they come from the same allocation
+ pub fn ptr_eq(this: &Self, other: &Self) -> bool {
+ this.0 as *const T == other.0 as *const T
+ }
+
+ /// Temporarily converts |self| into a bonafide Arc and exposes it to the
+ /// provided callback. The refcount is not modified.
+ #[inline]
+ pub fn with_arc<F, U>(&self, f: F) -> U
+ where
+ F: FnOnce(&Arc<T>) -> U,
+ T: 'static,
+ {
+ // Synthesize transient Arc, which never touches the refcount.
+ let transient = unsafe { mem::ManuallyDrop::new(Arc::from_raw(self.0)) };
+
+ // Expose the transient Arc to the callback, which may clone it if it wants.
+ let result = f(&transient);
+
+ // Forward the result.
+ result
+ }
+
+ /// Similar to deref, but uses the lifetime |a| rather than the lifetime of
+ /// self, which is incompatible with the signature of the Deref trait.
+ #[inline]
+ pub fn get(&self) -> &'a T {
+ self.0
+ }
+}
+
+impl<'a, T> Deref for ArcBorrow<'a, T> {
+ type Target = T;
+
+ #[inline]
+ fn deref(&self) -> &T {
+ self.0
+ }
+}
+
+/// A tagged union that can represent `Arc<A>` or `Arc<B>` while only consuming a
+/// single word. The type is also `NonNull`, and thus can be stored in an Option
+/// without increasing size.
+///
+/// This is functionally equivalent to
+/// `enum ArcUnion<A, B> { First(Arc<A>), Second(Arc<B>)` but only takes up
+/// up a single word of stack space.
+///
+/// This could probably be extended to support four types if necessary.
+pub struct ArcUnion<A, B> {
+ p: ptr::NonNull<()>,
+ phantom_a: PhantomData<A>,
+ phantom_b: PhantomData<B>,
+}
+
+unsafe impl<A: Sync + Send, B: Send + Sync> Send for ArcUnion<A, B> {}
+unsafe impl<A: Sync + Send, B: Send + Sync> Sync for ArcUnion<A, B> {}
+
+impl<A: PartialEq, B: PartialEq> PartialEq for ArcUnion<A, B> {
+ fn eq(&self, other: &Self) -> bool {
+ use crate::ArcUnionBorrow::*;
+ match (self.borrow(), other.borrow()) {
+ (First(x), First(y)) => x == y,
+ (Second(x), Second(y)) => x == y,
+ (_, _) => false,
+ }
+ }
+}
+
+impl<A: Eq, B: Eq> Eq for ArcUnion<A, B> {}
+
+/// This represents a borrow of an `ArcUnion`.
+#[derive(Debug)]
+pub enum ArcUnionBorrow<'a, A: 'a, B: 'a> {
+ First(ArcBorrow<'a, A>),
+ Second(ArcBorrow<'a, B>),
+}
+
+impl<A, B> ArcUnion<A, B> {
+ unsafe fn new(ptr: *mut ()) -> Self {
+ ArcUnion {
+ p: ptr::NonNull::new_unchecked(ptr),
+ phantom_a: PhantomData,
+ phantom_b: PhantomData,
+ }
+ }
+
+ /// Returns true if the two values are pointer-equal.
+ #[inline]
+ pub fn ptr_eq(this: &Self, other: &Self) -> bool {
+ this.p == other.p
+ }
+
+ #[inline]
+ pub fn ptr(&self) -> ptr::NonNull<()> {
+ self.p
+ }
+
+ /// Returns an enum representing a borrow of either A or B.
+ #[inline]
+ pub fn borrow(&self) -> ArcUnionBorrow<A, B> {
+ if self.is_first() {
+ let ptr = self.p.as_ptr() as *const ArcInner<A>;
+ let borrow = unsafe { ArcBorrow::from_ref(&(*ptr).data) };
+ ArcUnionBorrow::First(borrow)
+ } else {
+ let ptr = ((self.p.as_ptr() as usize) & !0x1) as *const ArcInner<B>;
+ let borrow = unsafe { ArcBorrow::from_ref(&(*ptr).data) };
+ ArcUnionBorrow::Second(borrow)
+ }
+ }
+
+ /// Creates an `ArcUnion` from an instance of the first type.
+ pub fn from_first(other: Arc<A>) -> Self {
+ let union = unsafe { Self::new(other.ptr() as *mut _) };
+ mem::forget(other);
+ union
+ }
+
+ /// Creates an `ArcUnion` from an instance of the second type.
+ pub fn from_second(other: Arc<B>) -> Self {
+ let union = unsafe { Self::new(((other.ptr() as usize) | 0x1) as *mut _) };
+ mem::forget(other);
+ union
+ }
+
+ /// Returns true if this `ArcUnion` contains the first type.
+ pub fn is_first(&self) -> bool {
+ self.p.as_ptr() as usize & 0x1 == 0
+ }
+
+ /// Returns true if this `ArcUnion` contains the second type.
+ pub fn is_second(&self) -> bool {
+ !self.is_first()
+ }
+
+ /// Returns a borrow of the first type if applicable, otherwise `None`.
+ pub fn as_first(&self) -> Option<ArcBorrow<A>> {
+ match self.borrow() {
+ ArcUnionBorrow::First(x) => Some(x),
+ ArcUnionBorrow::Second(_) => None,
+ }
+ }
+
+ /// Returns a borrow of the second type if applicable, otherwise None.
+ pub fn as_second(&self) -> Option<ArcBorrow<B>> {
+ match self.borrow() {
+ ArcUnionBorrow::First(_) => None,
+ ArcUnionBorrow::Second(x) => Some(x),
+ }
+ }
+}
+
+impl<A, B> Clone for ArcUnion<A, B> {
+ fn clone(&self) -> Self {
+ match self.borrow() {
+ ArcUnionBorrow::First(x) => ArcUnion::from_first(x.clone_arc()),
+ ArcUnionBorrow::Second(x) => ArcUnion::from_second(x.clone_arc()),
+ }
+ }
+}
+
+impl<A, B> Drop for ArcUnion<A, B> {
+ fn drop(&mut self) {
+ match self.borrow() {
+ ArcUnionBorrow::First(x) => unsafe {
+ let _ = Arc::from_raw(&*x);
+ },
+ ArcUnionBorrow::Second(x) => unsafe {
+ let _ = Arc::from_raw(&*x);
+ },
+ }
+ }
+}
+
+impl<A: fmt::Debug, B: fmt::Debug> fmt::Debug for ArcUnion<A, B> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Debug::fmt(&self.borrow(), f)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{Arc, ThinArc};
+ use std::clone::Clone;
+ use std::ops::Drop;
+ use std::sync::atomic;
+ use std::sync::atomic::Ordering::{Acquire, SeqCst};
+
+ #[derive(PartialEq)]
+ struct Canary(*mut atomic::AtomicUsize);
+
+ impl Drop for Canary {
+ fn drop(&mut self) {
+ unsafe {
+ (*self.0).fetch_add(1, SeqCst);
+ }
+ }
+ }
+
+ #[test]
+ fn empty_thin() {
+ let x = Arc::from_header_and_iter(100u32, std::iter::empty::<i32>());
+ assert_eq!(x.header, 100);
+ assert!(x.slice().is_empty());
+ }
+
+ #[test]
+ fn thin_assert_padding() {
+ #[derive(Clone, Default)]
+ #[repr(C)]
+ struct Padded {
+ i: u16,
+ }
+
+ // The header will have more alignment than `Padded`
+ let items = vec![Padded { i: 0xdead }, Padded { i: 0xbeef }];
+ let a = ThinArc::from_header_and_iter(0i32, items.into_iter());
+ assert_eq!(a.len(), 2);
+ assert_eq!(a.slice()[0].i, 0xdead);
+ assert_eq!(a.slice()[1].i, 0xbeef);
+ }
+
+ #[test]
+ fn slices_and_thin() {
+ let mut canary = atomic::AtomicUsize::new(0);
+ let c = Canary(&mut canary as *mut atomic::AtomicUsize);
+ let v = vec![5, 6];
+ {
+ let x = Arc::from_header_and_iter(c, v.into_iter());
+ let _ = x.clone();
+ let _ = x == x;
+ }
+ assert_eq!(canary.load(Acquire), 1);
+ }
+}
diff --git a/servo/components/style/Cargo.toml b/servo/components/style/Cargo.toml
new file mode 100644
index 0000000000..acf1bcf6fe
--- /dev/null
+++ b/servo/components/style/Cargo.toml
@@ -0,0 +1,91 @@
+[package]
+name = "style"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+publish = false
+
+build = "build.rs"
+edition = "2018"
+
+# https://github.com/rust-lang/cargo/issues/3544
+links = "servo_style_crate"
+
+[lib]
+name = "style"
+path = "lib.rs"
+doctest = false
+
+[features]
+gecko = ["nsstring", "serde", "style_traits/gecko", "bindgen", "regex", "toml", "mozbuild"]
+servo = ["serde", "style_traits/servo", "servo_atoms", "servo_config", "html5ever",
+ "cssparser/serde", "encoding_rs", "malloc_size_of/servo", "arrayvec/use_union",
+ "servo_url", "string_cache", "to_shmem/servo", "servo_arc/servo"]
+servo-layout-2013 = []
+servo-layout-2020 = []
+gecko_debug = []
+gecko_refcount_logging = []
+
+[dependencies]
+app_units = "0.7"
+arrayvec = "0.7"
+atomic_refcell = "0.1"
+bitflags = "2"
+byteorder = "1.0"
+cssparser = "0.33"
+derive_more = { version = "0.99", default-features = false, features = ["add", "add_assign", "deref", "deref_mut", "from"] }
+dom = { path = "../../../dom/base/rust" }
+new_debug_unreachable = "1.0"
+encoding_rs = {version = "0.8", optional = true}
+euclid = "0.22"
+fxhash = "0.2"
+html5ever = {version = "0.24", optional = true}
+icu_segmenter = { version = "1.4", default-features = false, features = ["auto", "compiled_data"] }
+indexmap = {version = "1.0", features = ["std"]}
+itertools = "0.10"
+itoa = "1.0"
+lazy_static = "1"
+log = "0.4"
+malloc_size_of = { path = "../malloc_size_of" }
+malloc_size_of_derive = { path = "../../../xpcom/rust/malloc_size_of_derive" }
+matches = "0.1"
+nsstring = {path = "../../../xpcom/rust/nsstring/", optional = true}
+num_cpus = {version = "1.1.0"}
+num-integer = "0.1"
+num-traits = "0.2"
+num-derive = "0.4"
+owning_ref = "0.4"
+parking_lot = "0.12"
+precomputed-hash = "0.1.1"
+rayon = "1"
+rayon-core = "1"
+selectors = { path = "../selectors" }
+serde = {version = "1.0", optional = true, features = ["derive"]}
+servo_arc = { path = "../servo_arc" }
+servo_atoms = {path = "../atoms", optional = true}
+servo_config = {path = "../config", optional = true}
+smallbitvec = "2.3.0"
+smallvec = "1.0"
+static_assertions = "1.1"
+static_prefs = { path = "../../../modules/libpref/init/static_prefs" }
+string_cache = { version = "0.8", optional = true }
+style_derive = {path = "../style_derive"}
+style_traits = {path = "../style_traits"}
+servo_url = {path = "../url", optional = true}
+to_shmem = {path = "../to_shmem"}
+to_shmem_derive = {path = "../to_shmem_derive"}
+time = "0.1"
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+uluru = "3.0"
+unicode-bidi = { version = "0.3", default-features = false }
+void = "1.0.2"
+gecko-profiler = { path = "../../../tools/profiler/rust-api" }
+
+[build-dependencies]
+lazy_static = "1"
+log = { version = "0.4", features = ["std"] }
+bindgen = {version = "0.69", optional = true, default-features = false}
+regex = {version = "1.0", optional = true, default-features = false, features = ["perf", "std"]}
+walkdir = "2.1.4"
+toml = {version = "0.5", optional = true, default-features = false}
+mozbuild = {version = "0.1", optional = true}
diff --git a/servo/components/style/README.md b/servo/components/style/README.md
new file mode 100644
index 0000000000..96457e1b30
--- /dev/null
+++ b/servo/components/style/README.md
@@ -0,0 +1,6 @@
+servo-style
+===========
+
+Style system for Servo, using [rust-cssparser](https://github.com/servo/rust-cssparser) for parsing.
+
+ * [Documentation](https://github.com/servo/servo/blob/master/docs/components/style.md).
diff --git a/servo/components/style/animation.rs b/servo/components/style/animation.rs
new file mode 100644
index 0000000000..b865120aba
--- /dev/null
+++ b/servo/components/style/animation.rs
@@ -0,0 +1,1415 @@
+/* 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/. */
+
+//! CSS transitions and animations.
+
+// NOTE(emilio): This code isn't really executed in Gecko, but we don't want to
+// compile it out so that people remember it exists.
+
+use crate::context::{CascadeInputs, SharedStyleContext};
+use crate::dom::{OpaqueNode, TDocument, TElement, TNode};
+use crate::properties::animated_properties::{AnimationValue, AnimationValueMap};
+use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection;
+use crate::properties::longhands::animation_fill_mode::computed_value::single_value::T as AnimationFillMode;
+use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState;
+use crate::properties::AnimationDeclarations;
+use crate::properties::{
+ ComputedValues, Importance, LonghandId, PropertyDeclarationBlock, PropertyDeclarationId,
+ PropertyDeclarationIdSet,
+};
+use crate::rule_tree::CascadeLevel;
+use crate::selector_parser::PseudoElement;
+use crate::shared_lock::{Locked, SharedRwLock};
+use crate::style_resolver::StyleResolverForElement;
+use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue};
+use crate::stylesheets::layer_rule::LayerOrder;
+use crate::values::animated::{Animate, Procedure};
+use crate::values::computed::{Time, TimingFunction};
+use crate::values::generics::easing::BeforeFlag;
+use crate::Atom;
+use fxhash::FxHashMap;
+use parking_lot::RwLock;
+use servo_arc::Arc;
+use std::fmt;
+
+/// Represents an animation for a given property.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct PropertyAnimation {
+ /// The value we are animating from.
+ from: AnimationValue,
+
+ /// The value we are animating to.
+ to: AnimationValue,
+
+ /// The timing function of this `PropertyAnimation`.
+ timing_function: TimingFunction,
+
+ /// The duration of this `PropertyAnimation` in seconds.
+ pub duration: f64,
+}
+
+impl PropertyAnimation {
+ /// Returns the given property longhand id.
+ pub fn property_id(&self) -> PropertyDeclarationId {
+ debug_assert_eq!(self.from.id(), self.to.id());
+ self.from.id()
+ }
+
+ fn from_property_declaration(
+ property_declaration: &PropertyDeclarationId,
+ timing_function: TimingFunction,
+ duration: Time,
+ old_style: &ComputedValues,
+ new_style: &ComputedValues,
+ ) -> Option<PropertyAnimation> {
+ // FIXME(emilio): Handle the case where old_style and new_style's writing mode differ.
+ let property_declaration = property_declaration.to_physical(new_style.writing_mode);
+ let from = AnimationValue::from_computed_values(property_declaration, old_style)?;
+ let to = AnimationValue::from_computed_values(property_declaration, new_style)?;
+ let duration = duration.seconds() as f64;
+
+ if from == to || duration == 0.0 {
+ return None;
+ }
+
+ Some(PropertyAnimation {
+ from,
+ to,
+ timing_function,
+ duration,
+ })
+ }
+
+ /// The output of the timing function given the progress ration of this animation.
+ fn timing_function_output(&self, progress: f64) -> f64 {
+ let epsilon = 1. / (200. * self.duration);
+ // FIXME: Need to set the before flag correctly.
+ // In order to get the before flag, we have to know the current animation phase
+ // and whether the iteration is reversed. For now, we skip this calculation
+ // by treating as if the flag is unset at all times.
+ // https://drafts.csswg.org/css-easing/#step-timing-function-algo
+ self.timing_function
+ .calculate_output(progress, BeforeFlag::Unset, epsilon)
+ }
+
+ /// Update the given animation at a given point of progress.
+ fn calculate_value(&self, progress: f64) -> Result<AnimationValue, ()> {
+ let procedure = Procedure::Interpolate {
+ progress: self.timing_function_output(progress),
+ };
+ self.from.animate(&self.to, procedure)
+ }
+}
+
+/// This structure represents the state of an animation.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
+pub enum AnimationState {
+ /// The animation has been created, but is not running yet. This state
+ /// is also used when an animation is still in the first delay phase.
+ Pending,
+ /// This animation is currently running.
+ Running,
+ /// This animation is paused. The inner field is the percentage of progress
+ /// when it was paused, from 0 to 1.
+ Paused(f64),
+ /// This animation has finished.
+ Finished,
+ /// This animation has been canceled.
+ Canceled,
+}
+
+impl AnimationState {
+ /// Whether or not this state requires its owning animation to be ticked.
+ fn needs_to_be_ticked(&self) -> bool {
+ *self == AnimationState::Running || *self == AnimationState::Pending
+ }
+}
+
+/// This structure represents a keyframes animation current iteration state.
+///
+/// If the iteration count is infinite, there's no other state, otherwise we
+/// have to keep track the current iteration and the max iteration count.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub enum KeyframesIterationState {
+ /// Infinite iterations with the current iteration count.
+ Infinite(f64),
+ /// Current and max iterations.
+ Finite(f64, f64),
+}
+
+/// A temporary data structure used when calculating ComputedKeyframes for an
+/// animation. This data structure is used to collapse information for steps
+/// which may be spread across multiple keyframe declarations into a single
+/// instance per `start_percentage`.
+struct IntermediateComputedKeyframe {
+ declarations: PropertyDeclarationBlock,
+ timing_function: Option<TimingFunction>,
+ start_percentage: f32,
+}
+
+impl IntermediateComputedKeyframe {
+ fn new(start_percentage: f32) -> Self {
+ IntermediateComputedKeyframe {
+ declarations: PropertyDeclarationBlock::new(),
+ timing_function: None,
+ start_percentage,
+ }
+ }
+
+ /// Walk through all keyframe declarations and combine all declarations with the
+ /// same `start_percentage` into individual `IntermediateComputedKeyframe`s.
+ fn generate_for_keyframes(
+ animation: &KeyframesAnimation,
+ context: &SharedStyleContext,
+ base_style: &ComputedValues,
+ ) -> Vec<Self> {
+ let mut intermediate_steps: Vec<Self> = Vec::with_capacity(animation.steps.len());
+ let mut current_step = IntermediateComputedKeyframe::new(0.);
+ for step in animation.steps.iter() {
+ let start_percentage = step.start_percentage.0;
+ if start_percentage != current_step.start_percentage {
+ let new_step = IntermediateComputedKeyframe::new(start_percentage);
+ intermediate_steps.push(std::mem::replace(&mut current_step, new_step));
+ }
+
+ current_step.update_from_step(step, context, base_style);
+ }
+ intermediate_steps.push(current_step);
+
+ // We should always have a first and a last step, even if these are just
+ // generated by KeyframesStepValue::ComputedValues.
+ debug_assert!(intermediate_steps.first().unwrap().start_percentage == 0.);
+ debug_assert!(intermediate_steps.last().unwrap().start_percentage == 1.);
+
+ intermediate_steps
+ }
+
+ fn update_from_step(
+ &mut self,
+ step: &KeyframesStep,
+ context: &SharedStyleContext,
+ base_style: &ComputedValues,
+ ) {
+ // Each keyframe declaration may optionally specify a timing function, falling
+ // back to the one defined global for the animation.
+ let guard = &context.guards.author;
+ if let Some(timing_function) = step.get_animation_timing_function(&guard) {
+ self.timing_function = Some(timing_function.to_computed_value_without_context());
+ }
+
+ let block = match step.value {
+ KeyframesStepValue::ComputedValues => return,
+ KeyframesStepValue::Declarations { ref block } => block,
+ };
+
+ // Filter out !important, non-animatable properties, and the
+ // 'display' property (which is only animatable from SMIL).
+ let guard = block.read_with(&guard);
+ for declaration in guard.normal_declaration_iter() {
+ if let PropertyDeclarationId::Longhand(id) = declaration.id() {
+ if id == LonghandId::Display {
+ continue;
+ }
+
+ if !id.is_animatable() {
+ continue;
+ }
+ }
+
+ self.declarations.push(
+ declaration.to_physical(base_style.writing_mode),
+ Importance::Normal,
+ );
+ }
+ }
+
+ fn resolve_style<E>(
+ self,
+ element: E,
+ context: &SharedStyleContext,
+ base_style: &Arc<ComputedValues>,
+ resolver: &mut StyleResolverForElement<E>,
+ ) -> Arc<ComputedValues>
+ where
+ E: TElement,
+ {
+ if !self.declarations.any_normal() {
+ return base_style.clone();
+ }
+
+ let document = element.as_node().owner_doc();
+ let locked_block = Arc::new(document.shared_lock().wrap(self.declarations));
+ let mut important_rules_changed = false;
+ let rule_node = base_style.rules().clone();
+ let new_node = context.stylist.rule_tree().update_rule_at_level(
+ CascadeLevel::Animations,
+ LayerOrder::root(),
+ Some(locked_block.borrow_arc()),
+ &rule_node,
+ &context.guards,
+ &mut important_rules_changed,
+ );
+
+ if new_node.is_none() {
+ return base_style.clone();
+ }
+
+ let inputs = CascadeInputs {
+ rules: new_node,
+ visited_rules: base_style.visited_rules().cloned(),
+ flags: base_style.flags.for_cascade_inputs(),
+ };
+ resolver
+ .cascade_style_and_visited_with_default_parents(inputs)
+ .0
+ }
+}
+
+/// A single computed keyframe for a CSS Animation.
+#[derive(Clone, MallocSizeOf)]
+struct ComputedKeyframe {
+ /// The timing function to use for transitions between this step
+ /// and the next one.
+ timing_function: TimingFunction,
+
+ /// The starting percentage (a number between 0 and 1) which represents
+ /// at what point in an animation iteration this step is.
+ start_percentage: f32,
+
+ /// The animation values to transition to and from when processing this
+ /// keyframe animation step.
+ values: Vec<AnimationValue>,
+}
+
+impl ComputedKeyframe {
+ fn generate_for_keyframes<E>(
+ element: E,
+ animation: &KeyframesAnimation,
+ context: &SharedStyleContext,
+ base_style: &Arc<ComputedValues>,
+ default_timing_function: TimingFunction,
+ resolver: &mut StyleResolverForElement<E>,
+ ) -> Vec<Self>
+ where
+ E: TElement,
+ {
+ let mut animating_properties = PropertyDeclarationIdSet::default();
+ for property in animation.properties_changed.iter() {
+ debug_assert!(property.is_animatable());
+ animating_properties.insert(property.to_physical(base_style.writing_mode));
+ }
+
+ let animation_values_from_style: Vec<AnimationValue> = animating_properties
+ .iter()
+ .map(|property| {
+ AnimationValue::from_computed_values(property, &**base_style)
+ .expect("Unexpected non-animatable property.")
+ })
+ .collect();
+
+ let intermediate_steps =
+ IntermediateComputedKeyframe::generate_for_keyframes(animation, context, base_style);
+
+ let mut computed_steps: Vec<Self> = Vec::with_capacity(intermediate_steps.len());
+ for (step_index, step) in intermediate_steps.into_iter().enumerate() {
+ let start_percentage = step.start_percentage;
+ let properties_changed_in_step = step.declarations.property_ids().clone();
+ let step_timing_function = step.timing_function.clone();
+ let step_style = step.resolve_style(element, context, base_style, resolver);
+ let timing_function =
+ step_timing_function.unwrap_or_else(|| default_timing_function.clone());
+
+ let values = {
+ // If a value is not set in a property declaration we use the value from
+ // the style for the first and last keyframe. For intermediate ones, we
+ // use the value from the previous keyframe.
+ //
+ // TODO(mrobinson): According to the spec, we should use an interpolated
+ // value for properties missing from keyframe declarations.
+ let default_values = if start_percentage == 0. || start_percentage == 1.0 {
+ &animation_values_from_style
+ } else {
+ debug_assert!(step_index != 0);
+ &computed_steps[step_index - 1].values
+ };
+
+ // For each property that is animating, pull the value from the resolved
+ // style for this step if it's in one of the declarations. Otherwise, we
+ // use the default value from the set we calculated above.
+ animating_properties
+ .iter()
+ .zip(default_values.iter())
+ .map(|(property_declaration, default_value)| {
+ if properties_changed_in_step.contains(property_declaration) {
+ AnimationValue::from_computed_values(property_declaration, &step_style)
+ .unwrap_or_else(|| default_value.clone())
+ } else {
+ default_value.clone()
+ }
+ })
+ .collect()
+ };
+
+ computed_steps.push(ComputedKeyframe {
+ timing_function,
+ start_percentage,
+ values,
+ });
+ }
+ computed_steps
+ }
+}
+
+/// A CSS Animation
+#[derive(Clone, MallocSizeOf)]
+pub struct Animation {
+ /// The name of this animation as defined by the style.
+ pub name: Atom,
+
+ /// The properties that change in this animation.
+ properties_changed: PropertyDeclarationIdSet,
+
+ /// The computed style for each keyframe of this animation.
+ computed_steps: Vec<ComputedKeyframe>,
+
+ /// The time this animation started at, which is the current value of the animation
+ /// timeline when this animation was created plus any animation delay.
+ pub started_at: f64,
+
+ /// The duration of this animation.
+ pub duration: f64,
+
+ /// The delay of the animation.
+ pub delay: f64,
+
+ /// The `animation-fill-mode` property of this animation.
+ pub fill_mode: AnimationFillMode,
+
+ /// The current iteration state for the animation.
+ pub iteration_state: KeyframesIterationState,
+
+ /// Whether this animation is paused.
+ pub state: AnimationState,
+
+ /// The declared animation direction of this animation.
+ pub direction: AnimationDirection,
+
+ /// The current animation direction. This can only be `normal` or `reverse`.
+ pub current_direction: AnimationDirection,
+
+ /// The original cascade style, needed to compute the generated keyframes of
+ /// the animation.
+ #[ignore_malloc_size_of = "ComputedValues"]
+ pub cascade_style: Arc<ComputedValues>,
+
+ /// Whether or not this animation is new and or has already been tracked
+ /// by the script thread.
+ pub is_new: bool,
+}
+
+impl Animation {
+ /// Whether or not this animation is cancelled by changes from a new style.
+ fn is_cancelled_in_new_style(&self, new_style: &Arc<ComputedValues>) -> bool {
+ let new_ui = new_style.get_ui();
+ let index = new_ui
+ .animation_name_iter()
+ .position(|animation_name| Some(&self.name) == animation_name.as_atom());
+ let index = match index {
+ Some(index) => index,
+ None => return true,
+ };
+
+ new_ui.animation_duration_mod(index).seconds() == 0.
+ }
+
+ /// Given the current time, advances this animation to the next iteration,
+ /// updates times, and then toggles the direction if appropriate. Otherwise
+ /// does nothing. Returns true if this animation has iterated.
+ pub fn iterate_if_necessary(&mut self, time: f64) -> bool {
+ if !self.iteration_over(time) {
+ return false;
+ }
+
+ // Only iterate animations that are currently running.
+ if self.state != AnimationState::Running {
+ return false;
+ }
+
+ if self.on_last_iteration() {
+ return false;
+ }
+
+ self.iterate();
+ true
+ }
+
+ fn iterate(&mut self) {
+ debug_assert!(!self.on_last_iteration());
+
+ if let KeyframesIterationState::Finite(ref mut current, max) = self.iteration_state {
+ *current = (*current + 1.).min(max);
+ }
+
+ if let AnimationState::Paused(ref mut progress) = self.state {
+ debug_assert!(*progress > 1.);
+ *progress -= 1.;
+ }
+
+ // Update the next iteration direction if applicable.
+ self.started_at += self.duration;
+ match self.direction {
+ AnimationDirection::Alternate | AnimationDirection::AlternateReverse => {
+ self.current_direction = match self.current_direction {
+ AnimationDirection::Normal => AnimationDirection::Reverse,
+ AnimationDirection::Reverse => AnimationDirection::Normal,
+ _ => unreachable!(),
+ };
+ },
+ _ => {},
+ }
+ }
+
+ /// A number (> 0 and <= 1) which represents the fraction of a full iteration
+ /// that the current iteration of the animation lasts. This will be less than 1
+ /// if the current iteration is the fractional remainder of a non-integral
+ /// iteration count.
+ pub fn current_iteration_end_progress(&self) -> f64 {
+ match self.iteration_state {
+ KeyframesIterationState::Finite(current, max) => (max - current).min(1.),
+ KeyframesIterationState::Infinite(_) => 1.,
+ }
+ }
+
+ /// The duration of the current iteration of this animation which may be less
+ /// than the animation duration if it has a non-integral iteration count.
+ pub fn current_iteration_duration(&self) -> f64 {
+ self.current_iteration_end_progress() * self.duration
+ }
+
+ /// Whether or not the current iteration is over. Note that this method assumes that
+ /// the animation is still running.
+ fn iteration_over(&self, time: f64) -> bool {
+ time > (self.started_at + self.current_iteration_duration())
+ }
+
+ /// Assuming this animation is running, whether or not it is on the last iteration.
+ fn on_last_iteration(&self) -> bool {
+ match self.iteration_state {
+ KeyframesIterationState::Finite(current, max) => current >= (max - 1.),
+ KeyframesIterationState::Infinite(_) => false,
+ }
+ }
+
+ /// Whether or not this animation has finished at the provided time. This does
+ /// not take into account canceling i.e. when an animation or transition is
+ /// canceled due to changes in the style.
+ pub fn has_ended(&self, time: f64) -> bool {
+ if !self.on_last_iteration() {
+ return false;
+ }
+
+ let progress = match self.state {
+ AnimationState::Finished => return true,
+ AnimationState::Paused(progress) => progress,
+ AnimationState::Running => (time - self.started_at) / self.duration,
+ AnimationState::Pending | AnimationState::Canceled => return false,
+ };
+
+ progress >= self.current_iteration_end_progress()
+ }
+
+ /// Updates the appropiate state from other animation.
+ ///
+ /// This happens when an animation is re-submitted to layout, presumably
+ /// because of an state change.
+ ///
+ /// There are some bits of state we can't just replace, over all taking in
+ /// account times, so here's that logic.
+ pub fn update_from_other(&mut self, other: &Self, now: f64) {
+ use self::AnimationState::*;
+
+ debug!(
+ "KeyframesAnimationState::update_from_other({:?}, {:?})",
+ self, other
+ );
+
+ // NB: We shall not touch the started_at field, since we don't want to
+ // restart the animation.
+ let old_started_at = self.started_at;
+ let old_duration = self.duration;
+ let old_direction = self.current_direction;
+ let old_state = self.state.clone();
+ let old_iteration_state = self.iteration_state.clone();
+
+ *self = other.clone();
+
+ self.started_at = old_started_at;
+ self.current_direction = old_direction;
+
+ // Don't update the iteration count, just the iteration limit.
+ // TODO: see how changing the limit affects rendering in other browsers.
+ // We might need to keep the iteration count even when it's infinite.
+ match (&mut self.iteration_state, old_iteration_state) {
+ (
+ &mut KeyframesIterationState::Finite(ref mut iters, _),
+ KeyframesIterationState::Finite(old_iters, _),
+ ) => *iters = old_iters,
+ _ => {},
+ }
+
+ // Don't pause or restart animations that should remain finished.
+ // We call mem::replace because `has_ended(...)` looks at `Animation::state`.
+ let new_state = std::mem::replace(&mut self.state, Running);
+ if old_state == Finished && self.has_ended(now) {
+ self.state = Finished;
+ } else {
+ self.state = new_state;
+ }
+
+ // If we're unpausing the animation, fake the start time so we seem to
+ // restore it.
+ //
+ // If the animation keeps paused, keep the old value.
+ //
+ // If we're pausing the animation, compute the progress value.
+ match (&mut self.state, &old_state) {
+ (&mut Pending, &Paused(progress)) => {
+ self.started_at = now - (self.duration * progress);
+ },
+ (&mut Paused(ref mut new), &Paused(old)) => *new = old,
+ (&mut Paused(ref mut progress), &Running) => {
+ *progress = (now - old_started_at) / old_duration
+ },
+ _ => {},
+ }
+
+ // Try to detect when we should skip straight to the running phase to
+ // avoid sending multiple animationstart events.
+ if self.state == Pending && self.started_at <= now && old_state != Pending {
+ self.state = Running;
+ }
+ }
+
+ /// Fill in an `AnimationValueMap` with values calculated from this animation at
+ /// the given time value.
+ fn get_property_declaration_at_time(&self, now: f64, map: &mut AnimationValueMap) {
+ debug_assert!(!self.computed_steps.is_empty());
+
+ let total_progress = match self.state {
+ AnimationState::Running | AnimationState::Pending | AnimationState::Finished => {
+ (now - self.started_at) / self.duration
+ },
+ AnimationState::Paused(progress) => progress,
+ AnimationState::Canceled => return,
+ };
+
+ if total_progress < 0. &&
+ self.fill_mode != AnimationFillMode::Backwards &&
+ self.fill_mode != AnimationFillMode::Both
+ {
+ return;
+ }
+ if self.has_ended(now) &&
+ self.fill_mode != AnimationFillMode::Forwards &&
+ self.fill_mode != AnimationFillMode::Both
+ {
+ return;
+ }
+ let total_progress = total_progress
+ .min(self.current_iteration_end_progress())
+ .max(0.0);
+
+ // Get the indices of the previous (from) keyframe and the next (to) keyframe.
+ let next_keyframe_index;
+ let prev_keyframe_index;
+ let num_steps = self.computed_steps.len();
+ match self.current_direction {
+ AnimationDirection::Normal => {
+ next_keyframe_index = self
+ .computed_steps
+ .iter()
+ .position(|step| total_progress as f32 <= step.start_percentage);
+ prev_keyframe_index = next_keyframe_index
+ .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None })
+ .unwrap_or(0);
+ },
+ AnimationDirection::Reverse => {
+ next_keyframe_index = self
+ .computed_steps
+ .iter()
+ .rev()
+ .position(|step| total_progress as f32 <= 1. - step.start_percentage)
+ .map(|pos| num_steps - pos - 1);
+ prev_keyframe_index = next_keyframe_index
+ .and_then(|pos| {
+ if pos != num_steps - 1 {
+ Some(pos + 1)
+ } else {
+ None
+ }
+ })
+ .unwrap_or(num_steps - 1)
+ },
+ _ => unreachable!(),
+ }
+
+ debug!(
+ "Animation::get_property_declaration_at_time: keyframe from {:?} to {:?}",
+ prev_keyframe_index, next_keyframe_index
+ );
+
+ let prev_keyframe = &self.computed_steps[prev_keyframe_index];
+ let next_keyframe = match next_keyframe_index {
+ Some(index) => &self.computed_steps[index],
+ None => return,
+ };
+
+ // If we only need to take into account one keyframe, then exit early
+ // in order to avoid doing more work.
+ let mut add_declarations_to_map = |keyframe: &ComputedKeyframe| {
+ for value in keyframe.values.iter() {
+ map.insert(value.id().to_owned(), value.clone());
+ }
+ };
+ if total_progress <= 0.0 {
+ add_declarations_to_map(&prev_keyframe);
+ return;
+ }
+ if total_progress >= 1.0 {
+ add_declarations_to_map(&next_keyframe);
+ return;
+ }
+
+ let percentage_between_keyframes =
+ (next_keyframe.start_percentage - prev_keyframe.start_percentage).abs() as f64;
+ let duration_between_keyframes = percentage_between_keyframes * self.duration;
+ let direction_aware_prev_keyframe_start_percentage = match self.current_direction {
+ AnimationDirection::Normal => prev_keyframe.start_percentage as f64,
+ AnimationDirection::Reverse => 1. - prev_keyframe.start_percentage as f64,
+ _ => unreachable!(),
+ };
+ let progress_between_keyframes = (total_progress -
+ direction_aware_prev_keyframe_start_percentage) /
+ percentage_between_keyframes;
+
+ for (from, to) in prev_keyframe.values.iter().zip(next_keyframe.values.iter()) {
+ let animation = PropertyAnimation {
+ from: from.clone(),
+ to: to.clone(),
+ timing_function: prev_keyframe.timing_function.clone(),
+ duration: duration_between_keyframes as f64,
+ };
+
+ if let Ok(value) = animation.calculate_value(progress_between_keyframes) {
+ map.insert(value.id().to_owned(), value);
+ }
+ }
+ }
+}
+
+impl fmt::Debug for Animation {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("Animation")
+ .field("name", &self.name)
+ .field("started_at", &self.started_at)
+ .field("duration", &self.duration)
+ .field("delay", &self.delay)
+ .field("iteration_state", &self.iteration_state)
+ .field("state", &self.state)
+ .field("direction", &self.direction)
+ .field("current_direction", &self.current_direction)
+ .field("cascade_style", &())
+ .finish()
+ }
+}
+
+/// A CSS Transition
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct Transition {
+ /// The start time of this transition, which is the current value of the animation
+ /// timeline when this transition was created plus any animation delay.
+ pub start_time: f64,
+
+ /// The delay used for this transition.
+ pub delay: f64,
+
+ /// The internal style `PropertyAnimation` for this transition.
+ pub property_animation: PropertyAnimation,
+
+ /// The state of this transition.
+ pub state: AnimationState,
+
+ /// Whether or not this transition is new and or has already been tracked
+ /// by the script thread.
+ pub is_new: bool,
+
+ /// If this `Transition` has been replaced by a new one this field is
+ /// used to help produce better reversed transitions.
+ pub reversing_adjusted_start_value: AnimationValue,
+
+ /// If this `Transition` has been replaced by a new one this field is
+ /// used to help produce better reversed transitions.
+ pub reversing_shortening_factor: f64,
+}
+
+impl Transition {
+ fn update_for_possibly_reversed_transition(
+ &mut self,
+ replaced_transition: &Transition,
+ delay: f64,
+ now: f64,
+ ) {
+ // If we reach here, we need to calculate a reversed transition according to
+ // https://drafts.csswg.org/css-transitions/#starting
+ //
+ // "...if the reversing-adjusted start value of the running transition
+ // is the same as the value of the property in the after-change style (see
+ // the section on reversing of transitions for why these case exists),
+ // implementations must cancel the running transition and start
+ // a new transition..."
+ if replaced_transition.reversing_adjusted_start_value != self.property_animation.to {
+ return;
+ }
+
+ // "* reversing-adjusted start value is the end value of the running transition"
+ let replaced_animation = &replaced_transition.property_animation;
+ self.reversing_adjusted_start_value = replaced_animation.to.clone();
+
+ // "* reversing shortening factor is the absolute value, clamped to the
+ // range [0, 1], of the sum of:
+ // 1. the output of the timing function of the old transition at the
+ // time of the style change event, times the reversing shortening
+ // factor of the old transition
+ // 2. 1 minus the reversing shortening factor of the old transition."
+ let transition_progress = ((now - replaced_transition.start_time) /
+ (replaced_transition.property_animation.duration))
+ .min(1.0)
+ .max(0.0);
+ let timing_function_output = replaced_animation.timing_function_output(transition_progress);
+ let old_reversing_shortening_factor = replaced_transition.reversing_shortening_factor;
+ self.reversing_shortening_factor = ((timing_function_output *
+ old_reversing_shortening_factor) +
+ (1.0 - old_reversing_shortening_factor))
+ .abs()
+ .min(1.0)
+ .max(0.0);
+
+ // "* start time is the time of the style change event plus:
+ // 1. if the matching transition delay is nonnegative, the matching
+ // transition delay, or.
+ // 2. if the matching transition delay is negative, the product of the new
+ // transition’s reversing shortening factor and the matching transition delay,"
+ self.start_time = if delay >= 0. {
+ now + delay
+ } else {
+ now + (self.reversing_shortening_factor * delay)
+ };
+
+ // "* end time is the start time plus the product of the matching transition
+ // duration and the new transition’s reversing shortening factor,"
+ self.property_animation.duration *= self.reversing_shortening_factor;
+
+ // "* start value is the current value of the property in the running transition,
+ // * end value is the value of the property in the after-change style,"
+ let procedure = Procedure::Interpolate {
+ progress: timing_function_output,
+ };
+ match replaced_animation
+ .from
+ .animate(&replaced_animation.to, procedure)
+ {
+ Ok(new_start) => self.property_animation.from = new_start,
+ Err(..) => {},
+ }
+ }
+
+ /// Whether or not this animation has ended at the provided time. This does
+ /// not take into account canceling i.e. when an animation or transition is
+ /// canceled due to changes in the style.
+ pub fn has_ended(&self, time: f64) -> bool {
+ time >= self.start_time + (self.property_animation.duration)
+ }
+
+ /// Update the given animation at a given point of progress.
+ pub fn calculate_value(&self, time: f64) -> Option<AnimationValue> {
+ let progress = (time - self.start_time) / (self.property_animation.duration);
+ if progress < 0.0 {
+ return None;
+ }
+
+ self.property_animation
+ .calculate_value(progress.min(1.0))
+ .ok()
+ }
+}
+
+/// Holds the animation state for a particular element.
+#[derive(Debug, Default, MallocSizeOf)]
+pub struct ElementAnimationSet {
+ /// The animations for this element.
+ pub animations: Vec<Animation>,
+
+ /// The transitions for this element.
+ pub transitions: Vec<Transition>,
+
+ /// Whether or not this ElementAnimationSet has had animations or transitions
+ /// which have been added, removed, or had their state changed.
+ pub dirty: bool,
+}
+
+impl ElementAnimationSet {
+ /// Cancel all animations in this `ElementAnimationSet`. This is typically called
+ /// when the element has been removed from the DOM.
+ pub fn cancel_all_animations(&mut self) {
+ self.dirty = !self.animations.is_empty();
+ for animation in self.animations.iter_mut() {
+ animation.state = AnimationState::Canceled;
+ }
+ self.cancel_active_transitions();
+ }
+
+ fn cancel_active_transitions(&mut self) {
+ for transition in self.transitions.iter_mut() {
+ if transition.state != AnimationState::Finished {
+ self.dirty = true;
+ transition.state = AnimationState::Canceled;
+ }
+ }
+ }
+
+ /// Apply all active animations.
+ pub fn apply_active_animations(
+ &self,
+ context: &SharedStyleContext,
+ style: &mut Arc<ComputedValues>,
+ ) {
+ let now = context.current_time_for_animations;
+ let mutable_style = Arc::make_mut(style);
+ if let Some(map) = self.get_value_map_for_active_animations(now) {
+ for value in map.values() {
+ value.set_in_style_for_servo(mutable_style);
+ }
+ }
+
+ if let Some(map) = self.get_value_map_for_active_transitions(now) {
+ for value in map.values() {
+ value.set_in_style_for_servo(mutable_style);
+ }
+ }
+ }
+
+ /// Clear all canceled animations and transitions from this `ElementAnimationSet`.
+ pub fn clear_canceled_animations(&mut self) {
+ self.animations
+ .retain(|animation| animation.state != AnimationState::Canceled);
+ self.transitions
+ .retain(|animation| animation.state != AnimationState::Canceled);
+ }
+
+ /// Whether this `ElementAnimationSet` is empty, which means it doesn't
+ /// hold any animations in any state.
+ pub fn is_empty(&self) -> bool {
+ self.animations.is_empty() && self.transitions.is_empty()
+ }
+
+ /// Whether or not this state needs animation ticks for its transitions
+ /// or animations.
+ pub fn needs_animation_ticks(&self) -> bool {
+ self.animations
+ .iter()
+ .any(|animation| animation.state.needs_to_be_ticked()) ||
+ self.transitions
+ .iter()
+ .any(|transition| transition.state.needs_to_be_ticked())
+ }
+
+ /// The number of running animations and transitions for this `ElementAnimationSet`.
+ pub fn running_animation_and_transition_count(&self) -> usize {
+ self.animations
+ .iter()
+ .filter(|animation| animation.state.needs_to_be_ticked())
+ .count() +
+ self.transitions
+ .iter()
+ .filter(|transition| transition.state.needs_to_be_ticked())
+ .count()
+ }
+
+ /// If this `ElementAnimationSet` has any any active animations.
+ pub fn has_active_animation(&self) -> bool {
+ self.animations
+ .iter()
+ .any(|animation| animation.state != AnimationState::Canceled)
+ }
+
+ /// If this `ElementAnimationSet` has any any active transitions.
+ pub fn has_active_transition(&self) -> bool {
+ self.transitions
+ .iter()
+ .any(|transition| transition.state != AnimationState::Canceled)
+ }
+
+ /// Update our animations given a new style, canceling or starting new animations
+ /// when appropriate.
+ pub fn update_animations_for_new_style<E>(
+ &mut self,
+ element: E,
+ context: &SharedStyleContext,
+ new_style: &Arc<ComputedValues>,
+ resolver: &mut StyleResolverForElement<E>,
+ ) where
+ E: TElement,
+ {
+ for animation in self.animations.iter_mut() {
+ if animation.is_cancelled_in_new_style(new_style) {
+ animation.state = AnimationState::Canceled;
+ }
+ }
+
+ maybe_start_animations(element, &context, &new_style, self, resolver);
+ }
+
+ /// Update our transitions given a new style, canceling or starting new animations
+ /// when appropriate.
+ pub fn update_transitions_for_new_style(
+ &mut self,
+ might_need_transitions_update: bool,
+ context: &SharedStyleContext,
+ old_style: Option<&Arc<ComputedValues>>,
+ after_change_style: &Arc<ComputedValues>,
+ ) {
+ // If this is the first style, we don't trigger any transitions and we assume
+ // there were no previously triggered transitions.
+ let mut before_change_style = match old_style {
+ Some(old_style) => Arc::clone(old_style),
+ None => return,
+ };
+
+ // If the style of this element is display:none, then cancel all active transitions.
+ if after_change_style.get_box().clone_display().is_none() {
+ self.cancel_active_transitions();
+ return;
+ }
+
+ if !might_need_transitions_update {
+ return;
+ }
+
+ // We convert old values into `before-change-style` here.
+ if self.has_active_transition() || self.has_active_animation() {
+ self.apply_active_animations(context, &mut before_change_style);
+ }
+
+ let transitioning_properties = start_transitions_if_applicable(
+ context,
+ &before_change_style,
+ after_change_style,
+ self,
+ );
+
+ // Cancel any non-finished transitions that have properties which no longer transition.
+ for transition in self.transitions.iter_mut() {
+ if transition.state == AnimationState::Finished {
+ continue;
+ }
+ if transitioning_properties.contains(transition.property_animation.property_id()) {
+ continue;
+ }
+ transition.state = AnimationState::Canceled;
+ self.dirty = true;
+ }
+ }
+
+ fn start_transition_if_applicable(
+ &mut self,
+ context: &SharedStyleContext,
+ property_declaration_id: &PropertyDeclarationId,
+ index: usize,
+ old_style: &ComputedValues,
+ new_style: &Arc<ComputedValues>,
+ ) {
+ let style = new_style.get_ui();
+ let timing_function = style.transition_timing_function_mod(index);
+ let duration = style.transition_duration_mod(index);
+ let delay = style.transition_delay_mod(index).seconds() as f64;
+ let now = context.current_time_for_animations;
+
+ // Only start a new transition if the style actually changes between
+ // the old style and the new style.
+ let property_animation = match PropertyAnimation::from_property_declaration(
+ property_declaration_id,
+ timing_function,
+ duration,
+ old_style,
+ new_style,
+ ) {
+ Some(property_animation) => property_animation,
+ None => return,
+ };
+
+ // Per [1], don't trigger a new transition if the end state for that
+ // transition is the same as that of a transition that's running or
+ // completed. We don't take into account any canceled animations.
+ // [1]: https://drafts.csswg.org/css-transitions/#starting
+ if self
+ .transitions
+ .iter()
+ .filter(|transition| transition.state != AnimationState::Canceled)
+ .any(|transition| transition.property_animation.to == property_animation.to)
+ {
+ return;
+ }
+
+ // We are going to start a new transition, but we might have to update
+ // it if we are replacing a reversed transition.
+ let reversing_adjusted_start_value = property_animation.from.clone();
+ let mut new_transition = Transition {
+ start_time: now + delay,
+ delay,
+ property_animation,
+ state: AnimationState::Pending,
+ is_new: true,
+ reversing_adjusted_start_value,
+ reversing_shortening_factor: 1.0,
+ };
+
+ if let Some(old_transition) = self
+ .transitions
+ .iter_mut()
+ .filter(|transition| transition.state == AnimationState::Running)
+ .find(|transition| {
+ transition.property_animation.property_id() == *property_declaration_id
+ })
+ {
+ // We always cancel any running transitions for the same property.
+ old_transition.state = AnimationState::Canceled;
+ new_transition.update_for_possibly_reversed_transition(old_transition, delay, now);
+ }
+
+ self.transitions.push(new_transition);
+ self.dirty = true;
+ }
+
+ /// Generate a `AnimationValueMap` for this `ElementAnimationSet`'s
+ /// active transitions at the given time value.
+ pub fn get_value_map_for_active_transitions(&self, now: f64) -> Option<AnimationValueMap> {
+ if !self.has_active_transition() {
+ return None;
+ }
+
+ let mut map =
+ AnimationValueMap::with_capacity_and_hasher(self.transitions.len(), Default::default());
+ for transition in &self.transitions {
+ if transition.state == AnimationState::Canceled {
+ continue;
+ }
+ let value = match transition.calculate_value(now) {
+ Some(value) => value,
+ None => continue,
+ };
+ map.insert(value.id().to_owned(), value);
+ }
+
+ Some(map)
+ }
+
+ /// Generate a `AnimationValueMap` for this `ElementAnimationSet`'s
+ /// active animations at the given time value.
+ pub fn get_value_map_for_active_animations(&self, now: f64) -> Option<AnimationValueMap> {
+ if !self.has_active_animation() {
+ return None;
+ }
+
+ let mut map = Default::default();
+ for animation in &self.animations {
+ animation.get_property_declaration_at_time(now, &mut map);
+ }
+
+ Some(map)
+ }
+}
+
+#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+/// A key that is used to identify nodes in the `DocumentAnimationSet`.
+pub struct AnimationSetKey {
+ /// The node for this `AnimationSetKey`.
+ pub node: OpaqueNode,
+ /// The pseudo element for this `AnimationSetKey`. If `None` this key will
+ /// refer to the main content for its node.
+ pub pseudo_element: Option<PseudoElement>,
+}
+
+impl AnimationSetKey {
+ /// Create a new key given a node and optional pseudo element.
+ pub fn new(node: OpaqueNode, pseudo_element: Option<PseudoElement>) -> Self {
+ AnimationSetKey {
+ node,
+ pseudo_element,
+ }
+ }
+
+ /// Create a new key for the main content of this node.
+ pub fn new_for_non_pseudo(node: OpaqueNode) -> Self {
+ AnimationSetKey {
+ node,
+ pseudo_element: None,
+ }
+ }
+
+ /// Create a new key for given node and pseudo element.
+ pub fn new_for_pseudo(node: OpaqueNode, pseudo_element: PseudoElement) -> Self {
+ AnimationSetKey {
+ node,
+ pseudo_element: Some(pseudo_element),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, MallocSizeOf)]
+/// A set of animations for a document.
+pub struct DocumentAnimationSet {
+ /// The `ElementAnimationSet`s that this set contains.
+ #[ignore_malloc_size_of = "Arc is hard"]
+ pub sets: Arc<RwLock<FxHashMap<AnimationSetKey, ElementAnimationSet>>>,
+}
+
+impl DocumentAnimationSet {
+ /// Return whether or not the provided node has active CSS animations.
+ pub fn has_active_animations(&self, key: &AnimationSetKey) -> bool {
+ self.sets
+ .read()
+ .get(key)
+ .map_or(false, |set| set.has_active_animation())
+ }
+
+ /// Return whether or not the provided node has active CSS transitions.
+ pub fn has_active_transitions(&self, key: &AnimationSetKey) -> bool {
+ self.sets
+ .read()
+ .get(key)
+ .map_or(false, |set| set.has_active_transition())
+ }
+
+ /// Return a locked PropertyDeclarationBlock with animation values for the given
+ /// key and time.
+ pub fn get_animation_declarations(
+ &self,
+ key: &AnimationSetKey,
+ time: f64,
+ shared_lock: &SharedRwLock,
+ ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> {
+ self.sets
+ .read()
+ .get(key)
+ .and_then(|set| set.get_value_map_for_active_animations(time))
+ .map(|map| {
+ let block = PropertyDeclarationBlock::from_animation_value_map(&map);
+ Arc::new(shared_lock.wrap(block))
+ })
+ }
+
+ /// Return a locked PropertyDeclarationBlock with transition values for the given
+ /// key and time.
+ pub fn get_transition_declarations(
+ &self,
+ key: &AnimationSetKey,
+ time: f64,
+ shared_lock: &SharedRwLock,
+ ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> {
+ self.sets
+ .read()
+ .get(key)
+ .and_then(|set| set.get_value_map_for_active_transitions(time))
+ .map(|map| {
+ let block = PropertyDeclarationBlock::from_animation_value_map(&map);
+ Arc::new(shared_lock.wrap(block))
+ })
+ }
+
+ /// Get all the animation declarations for the given key, returning an empty
+ /// `AnimationDeclarations` if there are no animations.
+ pub fn get_all_declarations(
+ &self,
+ key: &AnimationSetKey,
+ time: f64,
+ shared_lock: &SharedRwLock,
+ ) -> AnimationDeclarations {
+ let sets = self.sets.read();
+ let set = match sets.get(key) {
+ Some(set) => set,
+ None => return Default::default(),
+ };
+
+ let animations = set.get_value_map_for_active_animations(time).map(|map| {
+ let block = PropertyDeclarationBlock::from_animation_value_map(&map);
+ Arc::new(shared_lock.wrap(block))
+ });
+ let transitions = set.get_value_map_for_active_transitions(time).map(|map| {
+ let block = PropertyDeclarationBlock::from_animation_value_map(&map);
+ Arc::new(shared_lock.wrap(block))
+ });
+ AnimationDeclarations {
+ animations,
+ transitions,
+ }
+ }
+
+ /// Cancel all animations for set at the given key.
+ pub fn cancel_all_animations_for_key(&self, key: &AnimationSetKey) {
+ if let Some(set) = self.sets.write().get_mut(key) {
+ set.cancel_all_animations();
+ }
+ }
+}
+
+/// Kick off any new transitions for this node and return all of the properties that are
+/// transitioning. This is at the end of calculating style for a single node.
+pub fn start_transitions_if_applicable(
+ context: &SharedStyleContext,
+ old_style: &ComputedValues,
+ new_style: &Arc<ComputedValues>,
+ animation_state: &mut ElementAnimationSet,
+) -> PropertyDeclarationIdSet {
+ let mut properties_that_transition = PropertyDeclarationIdSet::default();
+ for transition in new_style.transition_properties() {
+ let physical_property = PropertyDeclarationId::Longhand(
+ transition.longhand_id.to_physical(new_style.writing_mode),
+ );
+ if properties_that_transition.contains(physical_property) {
+ continue;
+ }
+
+ properties_that_transition.insert(physical_property);
+ animation_state.start_transition_if_applicable(
+ context,
+ &physical_property,
+ transition.index,
+ old_style,
+ new_style,
+ );
+ }
+
+ properties_that_transition
+}
+
+/// Triggers animations for a given node looking at the animation property
+/// values.
+pub fn maybe_start_animations<E>(
+ element: E,
+ context: &SharedStyleContext,
+ new_style: &Arc<ComputedValues>,
+ animation_state: &mut ElementAnimationSet,
+ resolver: &mut StyleResolverForElement<E>,
+) where
+ E: TElement,
+{
+ let style = new_style.get_ui();
+ for (i, name) in style.animation_name_iter().enumerate() {
+ let name = match name.as_atom() {
+ Some(atom) => atom,
+ None => continue,
+ };
+
+ debug!("maybe_start_animations: name={}", name);
+ let duration = style.animation_duration_mod(i).seconds() as f64;
+ if duration == 0. {
+ continue;
+ }
+
+ let keyframe_animation = match context.stylist.get_animation(name, element) {
+ Some(animation) => animation,
+ None => continue,
+ };
+
+ debug!("maybe_start_animations: animation {} found", name);
+
+ // If this animation doesn't have any keyframe, we can just continue
+ // without submitting it to the compositor, since both the first and
+ // the second keyframes would be synthetised from the computed
+ // values.
+ if keyframe_animation.steps.is_empty() {
+ continue;
+ }
+
+ // NB: This delay may be negative, meaning that the animation may be created
+ // in a state where we have advanced one or more iterations or even that the
+ // animation begins in a finished state.
+ let delay = style.animation_delay_mod(i).seconds();
+
+ let iteration_count = style.animation_iteration_count_mod(i);
+ let iteration_state = if iteration_count.0.is_infinite() {
+ KeyframesIterationState::Infinite(0.0)
+ } else {
+ KeyframesIterationState::Finite(0.0, iteration_count.0 as f64)
+ };
+
+ let animation_direction = style.animation_direction_mod(i);
+
+ let initial_direction = match animation_direction {
+ AnimationDirection::Normal | AnimationDirection::Alternate => {
+ AnimationDirection::Normal
+ },
+ AnimationDirection::Reverse | AnimationDirection::AlternateReverse => {
+ AnimationDirection::Reverse
+ },
+ };
+
+ let now = context.current_time_for_animations;
+ let started_at = now + delay as f64;
+ let mut starting_progress = (now - started_at) / duration;
+ let state = match style.animation_play_state_mod(i) {
+ AnimationPlayState::Paused => AnimationState::Paused(starting_progress),
+ AnimationPlayState::Running => AnimationState::Pending,
+ };
+
+ let computed_steps = ComputedKeyframe::generate_for_keyframes(
+ element,
+ &keyframe_animation,
+ context,
+ new_style,
+ style.animation_timing_function_mod(i),
+ resolver,
+ );
+
+ let mut new_animation = Animation {
+ name: name.clone(),
+ properties_changed: keyframe_animation.properties_changed.clone(),
+ computed_steps,
+ started_at,
+ duration,
+ fill_mode: style.animation_fill_mode_mod(i),
+ delay: delay as f64,
+ iteration_state,
+ state,
+ direction: animation_direction,
+ current_direction: initial_direction,
+ cascade_style: new_style.clone(),
+ is_new: true,
+ };
+
+ // If we started with a negative delay, make sure we iterate the animation if
+ // the delay moves us past the first iteration.
+ while starting_progress > 1. && !new_animation.on_last_iteration() {
+ new_animation.iterate();
+ starting_progress -= 1.;
+ }
+
+ animation_state.dirty = true;
+
+ // If the animation was already present in the list for the node, just update its state.
+ for existing_animation in animation_state.animations.iter_mut() {
+ if existing_animation.state == AnimationState::Canceled {
+ continue;
+ }
+
+ if new_animation.name == existing_animation.name {
+ existing_animation
+ .update_from_other(&new_animation, context.current_time_for_animations);
+ return;
+ }
+ }
+
+ animation_state.animations.push(new_animation);
+ }
+}
diff --git a/servo/components/style/applicable_declarations.rs b/servo/components/style/applicable_declarations.rs
new file mode 100644
index 0000000000..96049b76e3
--- /dev/null
+++ b/servo/components/style/applicable_declarations.rs
@@ -0,0 +1,215 @@
+/* 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/. */
+
+//! Applicable declarations management.
+
+use crate::properties::PropertyDeclarationBlock;
+use crate::rule_tree::{CascadeLevel, StyleSource};
+use crate::shared_lock::Locked;
+use crate::stylesheets::layer_rule::LayerOrder;
+use servo_arc::Arc;
+use smallvec::SmallVec;
+
+/// List of applicable declarations. This is a transient structure that shuttles
+/// declarations between selector matching and inserting into the rule tree, and
+/// therefore we want to avoid heap-allocation where possible.
+///
+/// In measurements on wikipedia, we pretty much never have more than 8 applicable
+/// declarations, so we could consider making this 8 entries instead of 16.
+/// However, it may depend a lot on workload, and stack space is cheap.
+pub type ApplicableDeclarationList = SmallVec<[ApplicableDeclarationBlock; 16]>;
+
+/// Blink uses 18 bits to store source order, and does not check overflow [1].
+/// That's a limit that could be reached in realistic webpages, so we use
+/// 24 bits and enforce defined behavior in the overflow case.
+///
+/// Note that right now this restriction could be lifted if wanted (because we
+/// no longer stash the cascade level in the remaining bits), but we keep it in
+/// place in case we come up with a use-case for them, lacking reports of the
+/// current limit being too small.
+///
+/// [1] https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/css/
+/// RuleSet.h?l=128&rcl=90140ab80b84d0f889abc253410f44ed54ae04f3
+const SOURCE_ORDER_BITS: usize = 24;
+const SOURCE_ORDER_MAX: u32 = (1 << SOURCE_ORDER_BITS) - 1;
+const SOURCE_ORDER_MASK: u32 = SOURCE_ORDER_MAX;
+
+/// The cascade-level+layer order of this declaration.
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+pub struct CascadePriority {
+ cascade_level: CascadeLevel,
+ layer_order: LayerOrder,
+}
+
+const_assert_eq!(
+ std::mem::size_of::<CascadePriority>(),
+ std::mem::size_of::<u32>()
+);
+
+impl PartialOrd for CascadePriority {
+ #[inline]
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for CascadePriority {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.cascade_level.cmp(&other.cascade_level).then_with(|| {
+ let ordering = self.layer_order.cmp(&other.layer_order);
+ if ordering == std::cmp::Ordering::Equal {
+ return ordering;
+ }
+ // https://drafts.csswg.org/css-cascade-5/#cascade-layering
+ //
+ // Cascade layers (like declarations) are ordered by order
+ // of appearance. When comparing declarations that belong to
+ // different layers, then for normal rules the declaration
+ // whose cascade layer is last wins, and for important rules
+ // the declaration whose cascade layer is first wins.
+ //
+ // But the style attribute layer for some reason is special.
+ if self.cascade_level.is_important() &&
+ !self.layer_order.is_style_attribute_layer() &&
+ !other.layer_order.is_style_attribute_layer()
+ {
+ ordering.reverse()
+ } else {
+ ordering
+ }
+ })
+ }
+}
+
+impl CascadePriority {
+ /// Construct a new CascadePriority for a given (level, order) pair.
+ pub fn new(cascade_level: CascadeLevel, layer_order: LayerOrder) -> Self {
+ Self {
+ cascade_level,
+ layer_order,
+ }
+ }
+
+ /// Returns the layer order.
+ #[inline]
+ pub fn layer_order(&self) -> LayerOrder {
+ self.layer_order
+ }
+
+ /// Returns the cascade level.
+ #[inline]
+ pub fn cascade_level(&self) -> CascadeLevel {
+ self.cascade_level
+ }
+
+ /// Whether this declaration should be allowed if `revert` or `revert-layer`
+ /// have been specified on a given origin.
+ ///
+ /// `self` is the priority at which the `revert` or `revert-layer` keyword
+ /// have been specified.
+ pub fn allows_when_reverted(&self, other: &Self, origin_revert: bool) -> bool {
+ if origin_revert {
+ other.cascade_level.origin() < self.cascade_level.origin()
+ } else {
+ other.unimportant() < self.unimportant()
+ }
+ }
+
+ /// Convert this priority from "important" to "non-important", if needed.
+ pub fn unimportant(&self) -> Self {
+ Self::new(self.cascade_level().unimportant(), self.layer_order())
+ }
+
+ /// Convert this priority from "non-important" to "important", if needed.
+ pub fn important(&self) -> Self {
+ Self::new(self.cascade_level().important(), self.layer_order())
+ }
+
+ /// The same tree, in author origin, at the root layer.
+ pub fn same_tree_author_normal_at_root_layer() -> Self {
+ Self::new(CascadeLevel::same_tree_author_normal(), LayerOrder::root())
+ }
+}
+
+/// A property declaration together with its precedence among rules of equal
+/// specificity so that we can sort them.
+///
+/// This represents the declarations in a given declaration block for a given
+/// importance.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
+pub struct ApplicableDeclarationBlock {
+ /// The style source, either a style rule, or a property declaration block.
+ #[ignore_malloc_size_of = "Arc"]
+ pub source: StyleSource,
+ /// The bits containing the source order, cascade level, and shadow cascade
+ /// order.
+ source_order: u32,
+ /// The specificity of the selector.
+ pub specificity: u32,
+ /// The cascade priority of the rule.
+ pub cascade_priority: CascadePriority,
+}
+
+impl ApplicableDeclarationBlock {
+ /// Constructs an applicable declaration block from a given property
+ /// declaration block and importance.
+ #[inline]
+ pub fn from_declarations(
+ declarations: Arc<Locked<PropertyDeclarationBlock>>,
+ level: CascadeLevel,
+ layer_order: LayerOrder,
+ ) -> Self {
+ ApplicableDeclarationBlock {
+ source: StyleSource::from_declarations(declarations),
+ source_order: 0,
+ specificity: 0,
+ cascade_priority: CascadePriority::new(level, layer_order),
+ }
+ }
+
+ /// Constructs an applicable declaration block from the given components.
+ #[inline]
+ pub fn new(
+ source: StyleSource,
+ source_order: u32,
+ level: CascadeLevel,
+ specificity: u32,
+ layer_order: LayerOrder,
+ ) -> Self {
+ ApplicableDeclarationBlock {
+ source,
+ source_order: source_order & SOURCE_ORDER_MASK,
+ specificity,
+ cascade_priority: CascadePriority::new(level, layer_order),
+ }
+ }
+
+ /// Returns the source order of the block.
+ #[inline]
+ pub fn source_order(&self) -> u32 {
+ self.source_order
+ }
+
+ /// Returns the cascade level of the block.
+ #[inline]
+ pub fn level(&self) -> CascadeLevel {
+ self.cascade_priority.cascade_level()
+ }
+
+ /// Returns the cascade level of the block.
+ #[inline]
+ pub fn layer_order(&self) -> LayerOrder {
+ self.cascade_priority.layer_order()
+ }
+
+ /// Convenience method to consume self and return the right thing for the
+ /// rule tree to iterate over.
+ #[inline]
+ pub fn for_rule_tree(self) -> (StyleSource, CascadePriority) {
+ (self.source, self.cascade_priority)
+ }
+}
+
+// Size of this struct determines sorting and selector-matching performance.
+size_of_test!(ApplicableDeclarationBlock, 24);
diff --git a/servo/components/style/attr.rs b/servo/components/style/attr.rs
new file mode 100644
index 0000000000..05833fa08d
--- /dev/null
+++ b/servo/components/style/attr.rs
@@ -0,0 +1,599 @@
+/* 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/. */
+
+//! Parsed representations of [DOM attributes][attr].
+//!
+//! [attr]: https://dom.spec.whatwg.org/#interface-attr
+
+use crate::properties::PropertyDeclarationBlock;
+use crate::shared_lock::Locked;
+use crate::str::str_join;
+use crate::str::{read_exponent, read_fraction, HTML_SPACE_CHARACTERS};
+use crate::str::{read_numbers, split_commas, split_html_space_chars};
+use crate::values::specified::Length;
+use crate::values::AtomString;
+use crate::{Atom, LocalName, Namespace, Prefix};
+use app_units::Au;
+use cssparser::{self, Color, RGBA};
+use euclid::num::Zero;
+use num_traits::ToPrimitive;
+use selectors::attr::AttrSelectorOperation;
+use servo_arc::Arc;
+use servo_url::ServoUrl;
+use std::str::FromStr;
+
+// Duplicated from script::dom::values.
+const UNSIGNED_LONG_MAX: u32 = 2147483647;
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub enum LengthOrPercentageOrAuto {
+ Auto,
+ Percentage(f32),
+ Length(Au),
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub enum AttrValue {
+ String(String),
+ TokenList(String, Vec<Atom>),
+ UInt(String, u32),
+ Int(String, i32),
+ Double(String, f64),
+ Atom(Atom),
+ Length(String, Option<Length>),
+ Color(String, Option<RGBA>),
+ Dimension(String, LengthOrPercentageOrAuto),
+
+ /// Stores a URL, computed from the input string and a document's base URL.
+ ///
+ /// The URL is resolved at setting-time, so this kind of attribute value is
+ /// not actually suitable for most URL-reflecting IDL attributes.
+ ResolvedUrl(String, Option<ServoUrl>),
+
+ /// Note that this variant is only used transitively as a fast path to set
+ /// the property declaration block relevant to the style of an element when
+ /// set from the inline declaration of that element (that is,
+ /// `element.style`).
+ ///
+ /// This can, as of this writing, only correspond to the value of the
+ /// `style` element, and is set from its relevant CSSInlineStyleDeclaration,
+ /// and then converted to a string in Element::attribute_mutated.
+ ///
+ /// Note that we don't necessarily need to do that (we could just clone the
+ /// declaration block), but that avoids keeping a refcounted
+ /// declarationblock for longer than needed.
+ Declaration(
+ String,
+ #[ignore_malloc_size_of = "Arc"] Arc<Locked<PropertyDeclarationBlock>>,
+ ),
+}
+
+/// Shared implementation to parse an integer according to
+/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-integers> or
+/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-negative-integers>
+fn do_parse_integer<T: Iterator<Item = char>>(input: T) -> Result<i64, ()> {
+ let mut input = input
+ .skip_while(|c| HTML_SPACE_CHARACTERS.iter().any(|s| s == c))
+ .peekable();
+
+ let sign = match input.peek() {
+ None => return Err(()),
+ Some(&'-') => {
+ input.next();
+ -1
+ },
+ Some(&'+') => {
+ input.next();
+ 1
+ },
+ Some(_) => 1,
+ };
+
+ let (value, _) = read_numbers(input);
+
+ value.and_then(|value| value.checked_mul(sign)).ok_or(())
+}
+
+/// Parse an integer according to
+/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-integers>.
+pub fn parse_integer<T: Iterator<Item = char>>(input: T) -> Result<i32, ()> {
+ do_parse_integer(input).and_then(|result| result.to_i32().ok_or(()))
+}
+
+/// Parse an integer according to
+/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-negative-integers>
+pub fn parse_unsigned_integer<T: Iterator<Item = char>>(input: T) -> Result<u32, ()> {
+ do_parse_integer(input).and_then(|result| result.to_u32().ok_or(()))
+}
+
+/// Parse a floating-point number according to
+/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-floating-point-number-values>
+pub fn parse_double(string: &str) -> Result<f64, ()> {
+ let trimmed = string.trim_matches(HTML_SPACE_CHARACTERS);
+ let mut input = trimmed.chars().peekable();
+
+ let (value, divisor, chars_skipped) = match input.peek() {
+ None => return Err(()),
+ Some(&'-') => {
+ input.next();
+ (-1f64, -1f64, 1)
+ },
+ Some(&'+') => {
+ input.next();
+ (1f64, 1f64, 1)
+ },
+ _ => (1f64, 1f64, 0),
+ };
+
+ let (value, value_digits) = if let Some(&'.') = input.peek() {
+ (0f64, 0)
+ } else {
+ let (read_val, read_digits) = read_numbers(input);
+ (
+ value * read_val.and_then(|result| result.to_f64()).unwrap_or(1f64),
+ read_digits,
+ )
+ };
+
+ let input = trimmed
+ .chars()
+ .skip(value_digits + chars_skipped)
+ .peekable();
+
+ let (mut value, fraction_digits) = read_fraction(input, divisor, value);
+
+ let input = trimmed
+ .chars()
+ .skip(value_digits + chars_skipped + fraction_digits)
+ .peekable();
+
+ if let Some(exp) = read_exponent(input) {
+ value *= 10f64.powi(exp)
+ };
+
+ Ok(value)
+}
+
+impl AttrValue {
+ pub fn from_serialized_tokenlist(tokens: String) -> AttrValue {
+ let atoms =
+ split_html_space_chars(&tokens)
+ .map(Atom::from)
+ .fold(vec![], |mut acc, atom| {
+ if !acc.contains(&atom) {
+ acc.push(atom)
+ }
+ acc
+ });
+ AttrValue::TokenList(tokens, atoms)
+ }
+
+ pub fn from_comma_separated_tokenlist(tokens: String) -> AttrValue {
+ let atoms = split_commas(&tokens)
+ .map(Atom::from)
+ .fold(vec![], |mut acc, atom| {
+ if !acc.contains(&atom) {
+ acc.push(atom)
+ }
+ acc
+ });
+ AttrValue::TokenList(tokens, atoms)
+ }
+
+ pub fn from_atomic_tokens(atoms: Vec<Atom>) -> AttrValue {
+ // TODO(ajeffrey): effecient conversion of Vec<Atom> to String
+ let tokens = String::from(str_join(&atoms, "\x20"));
+ AttrValue::TokenList(tokens, atoms)
+ }
+
+ // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-unsigned-long
+ pub fn from_u32(string: String, default: u32) -> AttrValue {
+ let result = parse_unsigned_integer(string.chars()).unwrap_or(default);
+ let result = if result > UNSIGNED_LONG_MAX {
+ default
+ } else {
+ result
+ };
+ AttrValue::UInt(string, result)
+ }
+
+ pub fn from_i32(string: String, default: i32) -> AttrValue {
+ let result = parse_integer(string.chars()).unwrap_or(default);
+ AttrValue::Int(string, result)
+ }
+
+ // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-double
+ pub fn from_double(string: String, default: f64) -> AttrValue {
+ let result = parse_double(&string).unwrap_or(default);
+
+ if result.is_normal() {
+ AttrValue::Double(string, result)
+ } else {
+ AttrValue::Double(string, default)
+ }
+ }
+
+ // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers
+ pub fn from_limited_i32(string: String, default: i32) -> AttrValue {
+ let result = parse_integer(string.chars()).unwrap_or(default);
+
+ if result < 0 {
+ AttrValue::Int(string, default)
+ } else {
+ AttrValue::Int(string, result)
+ }
+ }
+
+ // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers-greater-than-zero
+ pub fn from_limited_u32(string: String, default: u32) -> AttrValue {
+ let result = parse_unsigned_integer(string.chars()).unwrap_or(default);
+ let result = if result == 0 || result > UNSIGNED_LONG_MAX {
+ default
+ } else {
+ result
+ };
+ AttrValue::UInt(string, result)
+ }
+
+ pub fn from_atomic(string: String) -> AttrValue {
+ let value = Atom::from(string);
+ AttrValue::Atom(value)
+ }
+
+ pub fn from_resolved_url(base: &ServoUrl, url: String) -> AttrValue {
+ let joined = base.join(&url).ok();
+ AttrValue::ResolvedUrl(url, joined)
+ }
+
+ pub fn from_legacy_color(string: String) -> AttrValue {
+ let parsed = parse_legacy_color(&string).ok();
+ AttrValue::Color(string, parsed)
+ }
+
+ pub fn from_dimension(string: String) -> AttrValue {
+ let parsed = parse_length(&string);
+ AttrValue::Dimension(string, parsed)
+ }
+
+ pub fn from_nonzero_dimension(string: String) -> AttrValue {
+ let parsed = parse_nonzero_length(&string);
+ AttrValue::Dimension(string, parsed)
+ }
+
+ /// Assumes the `AttrValue` is a `TokenList` and returns its tokens
+ ///
+ /// ## Panics
+ ///
+ /// Panics if the `AttrValue` is not a `TokenList`
+ pub fn as_tokens(&self) -> &[Atom] {
+ match *self {
+ AttrValue::TokenList(_, ref tokens) => tokens,
+ _ => panic!("Tokens not found"),
+ }
+ }
+
+ /// Assumes the `AttrValue` is an `Atom` and returns its value
+ ///
+ /// ## Panics
+ ///
+ /// Panics if the `AttrValue` is not an `Atom`
+ pub fn as_atom(&self) -> &Atom {
+ match *self {
+ AttrValue::Atom(ref value) => value,
+ _ => panic!("Atom not found"),
+ }
+ }
+
+ /// Assumes the `AttrValue` is a `Color` and returns its value
+ ///
+ /// ## Panics
+ ///
+ /// Panics if the `AttrValue` is not a `Color`
+ pub fn as_color(&self) -> Option<&RGBA> {
+ match *self {
+ AttrValue::Color(_, ref color) => color.as_ref(),
+ _ => panic!("Color not found"),
+ }
+ }
+
+ /// Assumes the `AttrValue` is a `Dimension` and returns its value
+ ///
+ /// ## Panics
+ ///
+ /// Panics if the `AttrValue` is not a `Dimension`
+ pub fn as_dimension(&self) -> &LengthOrPercentageOrAuto {
+ match *self {
+ AttrValue::Dimension(_, ref l) => l,
+ _ => panic!("Dimension not found"),
+ }
+ }
+
+ /// Assumes the `AttrValue` is a `ResolvedUrl` and returns its value.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if the `AttrValue` is not a `ResolvedUrl`
+ pub fn as_resolved_url(&self) -> Option<&ServoUrl> {
+ match *self {
+ AttrValue::ResolvedUrl(_, ref url) => url.as_ref(),
+ _ => panic!("Url not found"),
+ }
+ }
+
+ /// Return the AttrValue as its integer representation, if any.
+ /// This corresponds to attribute values returned as `AttrValue::UInt(_)`
+ /// by `VirtualMethods::parse_plain_attribute()`.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if the `AttrValue` is not a `UInt`
+ pub fn as_uint(&self) -> u32 {
+ if let AttrValue::UInt(_, value) = *self {
+ value
+ } else {
+ panic!("Uint not found");
+ }
+ }
+
+ /// Return the AttrValue as a dimension computed from its integer
+ /// representation, assuming that integer representation specifies pixels.
+ ///
+ /// This corresponds to attribute values returned as `AttrValue::UInt(_)`
+ /// by `VirtualMethods::parse_plain_attribute()`.
+ ///
+ /// ## Panics
+ ///
+ /// Panics if the `AttrValue` is not a `UInt`
+ pub fn as_uint_px_dimension(&self) -> LengthOrPercentageOrAuto {
+ if let AttrValue::UInt(_, value) = *self {
+ LengthOrPercentageOrAuto::Length(Au::from_px(value as i32))
+ } else {
+ panic!("Uint not found");
+ }
+ }
+
+ pub fn eval_selector(&self, selector: &AttrSelectorOperation<&AtomString>) -> bool {
+ // FIXME(SimonSapin) this can be more efficient by matching on `(self, selector)` variants
+ // and doing Atom comparisons instead of string comparisons where possible,
+ // with SelectorImpl::AttrValue changed to Atom.
+ selector.eval_str(self)
+ }
+}
+
+impl ::std::ops::Deref for AttrValue {
+ type Target = str;
+
+ fn deref(&self) -> &str {
+ match *self {
+ AttrValue::String(ref value) |
+ AttrValue::TokenList(ref value, _) |
+ AttrValue::UInt(ref value, _) |
+ AttrValue::Double(ref value, _) |
+ AttrValue::Length(ref value, _) |
+ AttrValue::Color(ref value, _) |
+ AttrValue::Int(ref value, _) |
+ AttrValue::ResolvedUrl(ref value, _) |
+ AttrValue::Declaration(ref value, _) |
+ AttrValue::Dimension(ref value, _) => &value,
+ AttrValue::Atom(ref value) => &value,
+ }
+ }
+}
+
+impl PartialEq<Atom> for AttrValue {
+ fn eq(&self, other: &Atom) -> bool {
+ match *self {
+ AttrValue::Atom(ref value) => value == other,
+ _ => other == &**self,
+ }
+ }
+}
+
+/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-zero-dimension-values>
+pub fn parse_nonzero_length(value: &str) -> LengthOrPercentageOrAuto {
+ match parse_length(value) {
+ LengthOrPercentageOrAuto::Length(x) if x == Au::zero() => LengthOrPercentageOrAuto::Auto,
+ LengthOrPercentageOrAuto::Percentage(x) if x == 0. => LengthOrPercentageOrAuto::Auto,
+ x => x,
+ }
+}
+
+/// Parses a [legacy color][color]. If unparseable, `Err` is returned.
+///
+/// [color]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-colour-value
+pub fn parse_legacy_color(mut input: &str) -> Result<RGBA, ()> {
+ // Steps 1 and 2.
+ if input.is_empty() {
+ return Err(());
+ }
+
+ // Step 3.
+ input = input.trim_matches(HTML_SPACE_CHARACTERS);
+
+ // Step 4.
+ if input.eq_ignore_ascii_case("transparent") {
+ return Err(());
+ }
+
+ // Step 5.
+ if let Ok(Color::RGBA(rgba)) = cssparser::parse_color_keyword(input) {
+ return Ok(rgba);
+ }
+
+ // Step 6.
+ if input.len() == 4 {
+ if let (b'#', Ok(r), Ok(g), Ok(b)) = (
+ input.as_bytes()[0],
+ hex(input.as_bytes()[1] as char),
+ hex(input.as_bytes()[2] as char),
+ hex(input.as_bytes()[3] as char),
+ ) {
+ return Ok(RGBA::new(r * 17, g * 17, b * 17, 255));
+ }
+ }
+
+ // Step 7.
+ let mut new_input = String::new();
+ for ch in input.chars() {
+ if ch as u32 > 0xffff {
+ new_input.push_str("00")
+ } else {
+ new_input.push(ch)
+ }
+ }
+ let mut input = &*new_input;
+
+ // Step 8.
+ for (char_count, (index, _)) in input.char_indices().enumerate() {
+ if char_count == 128 {
+ input = &input[..index];
+ break;
+ }
+ }
+
+ // Step 9.
+ if input.as_bytes()[0] == b'#' {
+ input = &input[1..]
+ }
+
+ // Step 10.
+ let mut new_input = Vec::new();
+ for ch in input.chars() {
+ if hex(ch).is_ok() {
+ new_input.push(ch as u8)
+ } else {
+ new_input.push(b'0')
+ }
+ }
+ let mut input = new_input;
+
+ // Step 11.
+ while input.is_empty() || (input.len() % 3) != 0 {
+ input.push(b'0')
+ }
+
+ // Step 12.
+ let mut length = input.len() / 3;
+ let (mut red, mut green, mut blue) = (
+ &input[..length],
+ &input[length..length * 2],
+ &input[length * 2..],
+ );
+
+ // Step 13.
+ if length > 8 {
+ red = &red[length - 8..];
+ green = &green[length - 8..];
+ blue = &blue[length - 8..];
+ length = 8
+ }
+
+ // Step 14.
+ while length > 2 && red[0] == b'0' && green[0] == b'0' && blue[0] == b'0' {
+ red = &red[1..];
+ green = &green[1..];
+ blue = &blue[1..];
+ length -= 1
+ }
+
+ // Steps 15-20.
+ return Ok(RGBA::new(
+ hex_string(red).unwrap(),
+ hex_string(green).unwrap(),
+ hex_string(blue).unwrap(),
+ 255,
+ ));
+
+ fn hex(ch: char) -> Result<u8, ()> {
+ match ch {
+ '0'..='9' => Ok((ch as u8) - b'0'),
+ 'a'..='f' => Ok((ch as u8) - b'a' + 10),
+ 'A'..='F' => Ok((ch as u8) - b'A' + 10),
+ _ => Err(()),
+ }
+ }
+
+ fn hex_string(string: &[u8]) -> Result<u8, ()> {
+ match string.len() {
+ 0 => Err(()),
+ 1 => hex(string[0] as char),
+ _ => {
+ let upper = hex(string[0] as char)?;
+ let lower = hex(string[1] as char)?;
+ Ok((upper << 4) | lower)
+ },
+ }
+ }
+}
+
+/// Parses a [dimension value][dim]. If unparseable, `Auto` is returned.
+///
+/// [dim]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-dimension-values
+// TODO: this function can be rewritten to return Result<LengthPercentage, _>
+pub fn parse_length(mut value: &str) -> LengthOrPercentageOrAuto {
+ // Steps 1 & 2 are not relevant
+
+ // Step 3
+ value = value.trim_start_matches(HTML_SPACE_CHARACTERS);
+
+ // Step 4
+ match value.chars().nth(0) {
+ Some('0'..='9') => {},
+ _ => return LengthOrPercentageOrAuto::Auto,
+ }
+
+ // Steps 5 to 8
+ // We trim the string length to the minimum of:
+ // 1. the end of the string
+ // 2. the first occurence of a '%' (U+0025 PERCENT SIGN)
+ // 3. the second occurrence of a '.' (U+002E FULL STOP)
+ // 4. the occurrence of a character that is neither a digit nor '%' nor '.'
+ // Note: Step 7.4 is directly subsumed by FromStr::from_str
+ let mut end_index = value.len();
+ let (mut found_full_stop, mut found_percent) = (false, false);
+ for (i, ch) in value.chars().enumerate() {
+ match ch {
+ '0'..='9' => continue,
+ '%' => {
+ found_percent = true;
+ end_index = i;
+ break;
+ },
+ '.' if !found_full_stop => {
+ found_full_stop = true;
+ continue;
+ },
+ _ => {
+ end_index = i;
+ break;
+ },
+ }
+ }
+ value = &value[..end_index];
+
+ if found_percent {
+ let result: Result<f32, _> = FromStr::from_str(value);
+ match result {
+ Ok(number) => return LengthOrPercentageOrAuto::Percentage((number as f32) / 100.0),
+ Err(_) => return LengthOrPercentageOrAuto::Auto,
+ }
+ }
+
+ match FromStr::from_str(value) {
+ Ok(number) => LengthOrPercentageOrAuto::Length(Au::from_f64_px(number)),
+ Err(_) => LengthOrPercentageOrAuto::Auto,
+ }
+}
+
+/// A struct that uniquely identifies an element's attribute.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct AttrIdentifier {
+ pub local_name: LocalName,
+ pub name: LocalName,
+ pub namespace: Namespace,
+ pub prefix: Option<Prefix>,
+}
diff --git a/servo/components/style/author_styles.rs b/servo/components/style/author_styles.rs
new file mode 100644
index 0000000000..a0223dcecc
--- /dev/null
+++ b/servo/components/style/author_styles.rs
@@ -0,0 +1,70 @@
+/* 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/. */
+
+//! A set of author stylesheets and their computed representation, such as the
+//! ones used for ShadowRoot.
+
+use crate::dom::TElement;
+use crate::invalidation::media_queries::ToMediaListKey;
+use crate::shared_lock::SharedRwLockReadGuard;
+use crate::stylesheet_set::AuthorStylesheetSet;
+use crate::stylesheets::StylesheetInDocument;
+use crate::stylist::CascadeData;
+use crate::stylist::Stylist;
+use servo_arc::Arc;
+
+/// A set of author stylesheets and their computed representation, such as the
+/// ones used for ShadowRoot.
+#[derive(MallocSizeOf)]
+pub struct GenericAuthorStyles<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The sheet collection, which holds the sheet pointers, the invalidations,
+ /// and all that stuff.
+ pub stylesheets: AuthorStylesheetSet<S>,
+ /// The actual cascade data computed from the stylesheets.
+ #[ignore_malloc_size_of = "Measured as part of the stylist"]
+ pub data: Arc<CascadeData>,
+}
+
+pub use self::GenericAuthorStyles as AuthorStyles;
+
+lazy_static! {
+ static ref EMPTY_CASCADE_DATA: Arc<CascadeData> = Arc::new_leaked(CascadeData::new());
+}
+
+impl<S> GenericAuthorStyles<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Create an empty AuthorStyles.
+ #[inline]
+ pub fn new() -> Self {
+ Self {
+ stylesheets: AuthorStylesheetSet::new(),
+ data: EMPTY_CASCADE_DATA.clone(),
+ }
+ }
+
+ /// Flush the pending sheet changes, updating `data` as appropriate.
+ ///
+ /// TODO(emilio): Need a host element and a snapshot map to do invalidation
+ /// properly.
+ #[inline]
+ pub fn flush<E>(&mut self, stylist: &mut Stylist, guard: &SharedRwLockReadGuard)
+ where
+ E: TElement,
+ S: ToMediaListKey,
+ {
+ let flusher = self
+ .stylesheets
+ .flush::<E>(/* host = */ None, /* snapshot_map = */ None);
+
+ let result = stylist.rebuild_author_data(&self.data, flusher.sheets, guard);
+ if let Ok(Some(new_data)) = result {
+ self.data = new_data;
+ }
+ }
+}
diff --git a/servo/components/style/bezier.rs b/servo/components/style/bezier.rs
new file mode 100644
index 0000000000..dd520ac0ed
--- /dev/null
+++ b/servo/components/style/bezier.rs
@@ -0,0 +1,176 @@
+/* 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/. */
+
+//! Parametric Bézier curves.
+//!
+//! This is based on `WebCore/platform/graphics/UnitBezier.h` in WebKit.
+
+#![deny(missing_docs)]
+
+use crate::values::CSSFloat;
+
+const NEWTON_METHOD_ITERATIONS: u8 = 8;
+
+/// A unit cubic Bézier curve, used for timing functions in CSS transitions and animations.
+pub struct Bezier {
+ ax: f64,
+ bx: f64,
+ cx: f64,
+ ay: f64,
+ by: f64,
+ cy: f64,
+}
+
+impl Bezier {
+ /// Calculate the output of a unit cubic Bézier curve from the two middle control points.
+ ///
+ /// X coordinate is time, Y coordinate is function advancement.
+ /// The nominal range for both is 0 to 1.
+ ///
+ /// The start and end points are always (0, 0) and (1, 1) so that a transition or animation
+ /// starts at 0% and ends at 100%.
+ pub fn calculate_bezier_output(
+ progress: f64,
+ epsilon: f64,
+ x1: f32,
+ y1: f32,
+ x2: f32,
+ y2: f32,
+ ) -> f64 {
+ // Check for a linear curve.
+ if x1 == y1 && x2 == y2 {
+ return progress;
+ }
+
+ // Ensure that we return 0 or 1 on both edges.
+ if progress == 0.0 {
+ return 0.0;
+ }
+ if progress == 1.0 {
+ return 1.0;
+ }
+
+ // For negative values, try to extrapolate with tangent (p1 - p0) or,
+ // if p1 is coincident with p0, with (p2 - p0).
+ if progress < 0.0 {
+ if x1 > 0.0 {
+ return progress * y1 as f64 / x1 as f64;
+ }
+ if y1 == 0.0 && x2 > 0.0 {
+ return progress * y2 as f64 / x2 as f64;
+ }
+ // If we can't calculate a sensible tangent, don't extrapolate at all.
+ return 0.0;
+ }
+
+ // For values greater than 1, try to extrapolate with tangent (p2 - p3) or,
+ // if p2 is coincident with p3, with (p1 - p3).
+ if progress > 1.0 {
+ if x2 < 1.0 {
+ return 1.0 + (progress - 1.0) * (y2 as f64 - 1.0) / (x2 as f64 - 1.0);
+ }
+ if y2 == 1.0 && x1 < 1.0 {
+ return 1.0 + (progress - 1.0) * (y1 as f64 - 1.0) / (x1 as f64 - 1.0);
+ }
+ // If we can't calculate a sensible tangent, don't extrapolate at all.
+ return 1.0;
+ }
+
+ Bezier::new(x1, y1, x2, y2).solve(progress, epsilon)
+ }
+
+ #[inline]
+ fn new(x1: CSSFloat, y1: CSSFloat, x2: CSSFloat, y2: CSSFloat) -> Bezier {
+ let cx = 3. * x1 as f64;
+ let bx = 3. * (x2 as f64 - x1 as f64) - cx;
+
+ let cy = 3. * y1 as f64;
+ let by = 3. * (y2 as f64 - y1 as f64) - cy;
+
+ Bezier {
+ ax: 1.0 - cx - bx,
+ bx: bx,
+ cx: cx,
+ ay: 1.0 - cy - by,
+ by: by,
+ cy: cy,
+ }
+ }
+
+ #[inline]
+ fn sample_curve_x(&self, t: f64) -> f64 {
+ // ax * t^3 + bx * t^2 + cx * t
+ ((self.ax * t + self.bx) * t + self.cx) * t
+ }
+
+ #[inline]
+ fn sample_curve_y(&self, t: f64) -> f64 {
+ ((self.ay * t + self.by) * t + self.cy) * t
+ }
+
+ #[inline]
+ fn sample_curve_derivative_x(&self, t: f64) -> f64 {
+ (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx
+ }
+
+ #[inline]
+ fn solve_curve_x(&self, x: f64, epsilon: f64) -> f64 {
+ // Fast path: Use Newton's method.
+ let mut t = x;
+ for _ in 0..NEWTON_METHOD_ITERATIONS {
+ let x2 = self.sample_curve_x(t);
+ if x2.approx_eq(x, epsilon) {
+ return t;
+ }
+ let dx = self.sample_curve_derivative_x(t);
+ if dx.approx_eq(0.0, 1e-6) {
+ break;
+ }
+ t -= (x2 - x) / dx;
+ }
+
+ // Slow path: Use bisection.
+ let (mut lo, mut hi, mut t) = (0.0, 1.0, x);
+
+ if t < lo {
+ return lo;
+ }
+ if t > hi {
+ return hi;
+ }
+
+ while lo < hi {
+ let x2 = self.sample_curve_x(t);
+ if x2.approx_eq(x, epsilon) {
+ return t;
+ }
+ if x > x2 {
+ lo = t
+ } else {
+ hi = t
+ }
+ t = (hi - lo) / 2.0 + lo
+ }
+
+ t
+ }
+
+ /// Solve the bezier curve for a given `x` and an `epsilon`, that should be
+ /// between zero and one.
+ #[inline]
+ fn solve(&self, x: f64, epsilon: f64) -> f64 {
+ self.sample_curve_y(self.solve_curve_x(x, epsilon))
+ }
+}
+
+trait ApproxEq {
+ fn approx_eq(self, value: Self, epsilon: Self) -> bool;
+}
+
+impl ApproxEq for f64 {
+ #[inline]
+ fn approx_eq(self, value: f64, epsilon: f64) -> bool {
+ (self - value).abs() < epsilon
+ }
+}
diff --git a/servo/components/style/bloom.rs b/servo/components/style/bloom.rs
new file mode 100644
index 0000000000..824acb7114
--- /dev/null
+++ b/servo/components/style/bloom.rs
@@ -0,0 +1,401 @@
+/* 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/. */
+
+//! The style bloom filter is used as an optimization when matching deep
+//! descendant selectors.
+
+#![deny(missing_docs)]
+
+use crate::dom::{SendElement, TElement};
+use atomic_refcell::{AtomicRefCell, AtomicRefMut};
+use owning_ref::OwningHandle;
+use selectors::bloom::BloomFilter;
+use servo_arc::Arc;
+use smallvec::SmallVec;
+use std::mem::ManuallyDrop;
+
+thread_local! {
+ /// Bloom filters are large allocations, so we store them in thread-local storage
+ /// such that they can be reused across style traversals. StyleBloom is responsible
+ /// for ensuring that the bloom filter is zeroed when it is dropped.
+ ///
+ /// We intentionally leak this from TLS because we don't have the guarantee
+ /// of TLS destructors to run in worker threads.
+ ///
+ /// We could change this once https://github.com/rayon-rs/rayon/issues/688
+ /// is fixed, hopefully.
+ static BLOOM_KEY: ManuallyDrop<Arc<AtomicRefCell<BloomFilter>>> =
+ ManuallyDrop::new(Arc::new_leaked(Default::default()));
+}
+
+/// A struct that allows us to fast-reject deep descendant selectors avoiding
+/// selector-matching.
+///
+/// This is implemented using a counting bloom filter, and it's a standard
+/// optimization. See Gecko's `AncestorFilter`, and Blink's and WebKit's
+/// `SelectorFilter`.
+///
+/// The constraints for Servo's style system are a bit different compared to
+/// traditional style systems given Servo does a parallel breadth-first
+/// traversal instead of a sequential depth-first traversal.
+///
+/// This implies that we need to track a bit more state than other browsers to
+/// ensure we're doing the correct thing during the traversal, and being able to
+/// apply this optimization effectively.
+///
+/// Concretely, we have a bloom filter instance per worker thread, and we track
+/// the current DOM depth in order to find a common ancestor when it doesn't
+/// match the previous element we've styled.
+///
+/// This is usually a pretty fast operation (we use to be one level deeper than
+/// the previous one), but in the case of work-stealing, we may needed to push
+/// and pop multiple elements.
+///
+/// See the `insert_parents_recovering`, where most of the magic happens.
+///
+/// Regarding thread-safety, this struct is safe because:
+///
+/// * We clear this after a restyle.
+/// * The DOM shape and attributes (and every other thing we access here) are
+/// immutable during a restyle.
+///
+pub struct StyleBloom<E: TElement> {
+ /// A handle to the bloom filter from the thread upon which this StyleBloom
+ /// was created. We use AtomicRefCell so that this is all |Send|, which allows
+ /// StyleBloom to live in ThreadLocalStyleContext, which is dropped from the
+ /// parent thread.
+ filter: OwningHandle<Arc<AtomicRefCell<BloomFilter>>, AtomicRefMut<'static, BloomFilter>>,
+
+ /// The stack of elements that this bloom filter contains, along with the
+ /// number of hashes pushed for each element.
+ elements: SmallVec<[PushedElement<E>; 16]>,
+
+ /// Stack of hashes that have been pushed onto this filter.
+ pushed_hashes: SmallVec<[u32; 64]>,
+}
+
+/// The very rough benchmarks in the selectors crate show clear()
+/// costing about 25 times more than remove_hash(). We use this to implement
+/// clear() more efficiently when only a small number of hashes have been
+/// pushed.
+///
+/// One subtly to note is that remove_hash() will not touch the value
+/// if the filter overflowed. However, overflow can only occur if we
+/// get 255 collisions on the same hash value, and 25 < 255.
+const MEMSET_CLEAR_THRESHOLD: usize = 25;
+
+struct PushedElement<E: TElement> {
+ /// The element that was pushed.
+ element: SendElement<E>,
+
+ /// The number of hashes pushed for the element.
+ num_hashes: usize,
+}
+
+impl<E: TElement> PushedElement<E> {
+ fn new(el: E, num_hashes: usize) -> Self {
+ PushedElement {
+ element: unsafe { SendElement::new(el) },
+ num_hashes,
+ }
+ }
+}
+
+/// Returns whether the attribute name is excluded from the bloom filter.
+///
+/// We do this for attributes that are very common but not commonly used in
+/// selectors.
+#[inline]
+pub fn is_attr_name_excluded_from_filter(atom: &crate::Atom) -> bool {
+ *atom == atom!("class") || *atom == atom!("id") || *atom == atom!("style")
+}
+
+/// Gather all relevant hash for fast-reject filters from an element.
+pub fn each_relevant_element_hash<E, F>(element: E, mut f: F)
+where
+ E: TElement,
+ F: FnMut(u32),
+{
+ f(element.local_name().get_hash());
+ f(element.namespace().get_hash());
+
+ if let Some(id) = element.id() {
+ f(id.get_hash());
+ }
+
+ element.each_class(|class| f(class.get_hash()));
+
+ element.each_attr_name(|name| {
+ if !is_attr_name_excluded_from_filter(name) {
+ f(name.get_hash())
+ }
+ });
+}
+
+impl<E: TElement> Drop for StyleBloom<E> {
+ fn drop(&mut self) {
+ // Leave the reusable bloom filter in a zeroed state.
+ self.clear();
+ }
+}
+
+impl<E: TElement> StyleBloom<E> {
+ /// Create an empty `StyleBloom`. Because StyleBloom acquires the thread-
+ /// local filter buffer, creating multiple live StyleBloom instances at
+ /// the same time on the same thread will panic.
+
+ // Forced out of line to limit stack frame sizes after extra inlining from
+ // https://github.com/rust-lang/rust/pull/43931
+ //
+ // See https://github.com/servo/servo/pull/18420#issuecomment-328769322
+ #[inline(never)]
+ pub fn new() -> Self {
+ let bloom_arc = BLOOM_KEY.with(|b| Arc::clone(&*b));
+ let filter =
+ OwningHandle::new_with_fn(bloom_arc, |x| unsafe { x.as_ref() }.unwrap().borrow_mut());
+ debug_assert!(
+ filter.is_zeroed(),
+ "Forgot to zero the bloom filter last time"
+ );
+ StyleBloom {
+ filter,
+ elements: Default::default(),
+ pushed_hashes: Default::default(),
+ }
+ }
+
+ /// Return the bloom filter used properly by the `selectors` crate.
+ pub fn filter(&self) -> &BloomFilter {
+ &*self.filter
+ }
+
+ /// Push an element to the bloom filter, knowing that it's a child of the
+ /// last element parent.
+ pub fn push(&mut self, element: E) {
+ if cfg!(debug_assertions) {
+ if self.elements.is_empty() {
+ assert!(element.traversal_parent().is_none());
+ }
+ }
+ self.push_internal(element);
+ }
+
+ /// Same as `push`, but without asserting, in order to use it from
+ /// `rebuild`.
+ fn push_internal(&mut self, element: E) {
+ let mut count = 0;
+ each_relevant_element_hash(element, |hash| {
+ count += 1;
+ self.filter.insert_hash(hash);
+ self.pushed_hashes.push(hash);
+ });
+ self.elements.push(PushedElement::new(element, count));
+ }
+
+ /// Pop the last element in the bloom filter and return it.
+ #[inline]
+ fn pop(&mut self) -> Option<E> {
+ let PushedElement {
+ element,
+ num_hashes,
+ } = self.elements.pop()?;
+ let popped_element = *element;
+
+ // Verify that the pushed hashes match the ones we'd get from the element.
+ let mut expected_hashes = vec![];
+ if cfg!(debug_assertions) {
+ each_relevant_element_hash(popped_element, |hash| expected_hashes.push(hash));
+ }
+
+ for _ in 0..num_hashes {
+ let hash = self.pushed_hashes.pop().unwrap();
+ debug_assert_eq!(expected_hashes.pop().unwrap(), hash);
+ self.filter.remove_hash(hash);
+ }
+
+ Some(popped_element)
+ }
+
+ /// Returns the DOM depth of elements that can be correctly
+ /// matched against the bloom filter (that is, the number of
+ /// elements in our list).
+ pub fn matching_depth(&self) -> usize {
+ self.elements.len()
+ }
+
+ /// Clears the bloom filter.
+ pub fn clear(&mut self) {
+ self.elements.clear();
+
+ if self.pushed_hashes.len() > MEMSET_CLEAR_THRESHOLD {
+ self.filter.clear();
+ self.pushed_hashes.clear();
+ } else {
+ for hash in self.pushed_hashes.drain(..) {
+ self.filter.remove_hash(hash);
+ }
+ debug_assert!(self.filter.is_zeroed());
+ }
+ }
+
+ /// Rebuilds the bloom filter up to the parent of the given element.
+ pub fn rebuild(&mut self, mut element: E) {
+ self.clear();
+
+ let mut parents_to_insert = SmallVec::<[E; 16]>::new();
+ while let Some(parent) = element.traversal_parent() {
+ parents_to_insert.push(parent);
+ element = parent;
+ }
+
+ for parent in parents_to_insert.drain(..).rev() {
+ self.push(parent);
+ }
+ }
+
+ /// In debug builds, asserts that all the parents of `element` are in the
+ /// bloom filter.
+ ///
+ /// Goes away in release builds.
+ pub fn assert_complete(&self, mut element: E) {
+ if cfg!(debug_assertions) {
+ let mut checked = 0;
+ while let Some(parent) = element.traversal_parent() {
+ assert_eq!(
+ parent,
+ *(self.elements[self.elements.len() - 1 - checked].element)
+ );
+ element = parent;
+ checked += 1;
+ }
+ assert_eq!(checked, self.elements.len());
+ }
+ }
+
+ /// Get the element that represents the chain of things inserted
+ /// into the filter right now. That chain is the given element
+ /// (if any) and its ancestors.
+ #[inline]
+ pub fn current_parent(&self) -> Option<E> {
+ self.elements.last().map(|ref el| *el.element)
+ }
+
+ /// Insert the parents of an element in the bloom filter, trying to recover
+ /// the filter if the last element inserted doesn't match.
+ ///
+ /// Gets the element depth in the dom, to make it efficient, or if not
+ /// provided always rebuilds the filter from scratch.
+ ///
+ /// Returns the new bloom filter depth, that the traversal code is
+ /// responsible to keep around if it wants to get an effective filter.
+ pub fn insert_parents_recovering(&mut self, element: E, element_depth: usize) {
+ // Easy case, we're in a different restyle, or we're empty.
+ if self.elements.is_empty() {
+ self.rebuild(element);
+ return;
+ }
+
+ let traversal_parent = match element.traversal_parent() {
+ Some(parent) => parent,
+ None => {
+ // Yay, another easy case.
+ self.clear();
+ return;
+ },
+ };
+
+ if self.current_parent() == Some(traversal_parent) {
+ // Ta da, cache hit, we're all done.
+ return;
+ }
+
+ if element_depth == 0 {
+ self.clear();
+ return;
+ }
+
+ // We should've early exited above.
+ debug_assert!(
+ element_depth != 0,
+ "We should have already cleared the bloom filter"
+ );
+ debug_assert!(!self.elements.is_empty(), "How! We should've just rebuilt!");
+
+ // Now the fun begins: We have the depth of the dom and the depth of the
+ // last element inserted in the filter, let's try to find a common
+ // parent.
+ //
+ // The current depth, that is, the depth of the last element inserted in
+ // the bloom filter, is the number of elements _minus one_, that is: if
+ // there's one element, it must be the root -> depth zero.
+ let mut current_depth = self.elements.len() - 1;
+
+ // If the filter represents an element too deep in the dom, we need to
+ // pop ancestors.
+ while current_depth > element_depth - 1 {
+ self.pop().expect("Emilio is bad at math");
+ current_depth -= 1;
+ }
+
+ // Now let's try to find a common parent in the bloom filter chain,
+ // starting with traversal_parent.
+ let mut common_parent = traversal_parent;
+ let mut common_parent_depth = element_depth - 1;
+
+ // Let's collect the parents we are going to need to insert once we've
+ // found the common one.
+ let mut parents_to_insert = SmallVec::<[E; 16]>::new();
+
+ // If the bloom filter still doesn't have enough elements, the common
+ // parent is up in the dom.
+ while common_parent_depth > current_depth {
+ // TODO(emilio): Seems like we could insert parents here, then
+ // reverse the slice.
+ parents_to_insert.push(common_parent);
+ common_parent = common_parent.traversal_parent().expect("We were lied to");
+ common_parent_depth -= 1;
+ }
+
+ // Now the two depths are the same.
+ debug_assert_eq!(common_parent_depth, current_depth);
+
+ // Happy case: The parents match, we only need to push the ancestors
+ // we've collected and we'll never enter in this loop.
+ //
+ // Not-so-happy case: Parent's don't match, so we need to keep going up
+ // until we find a common ancestor.
+ //
+ // Gecko currently models native anonymous content that conceptually
+ // hangs off the document (such as scrollbars) as a separate subtree
+ // from the document root.
+ //
+ // Thus it's possible with Gecko that we do not find any common
+ // ancestor.
+ while *(self.elements.last().unwrap().element) != common_parent {
+ parents_to_insert.push(common_parent);
+ self.pop().unwrap();
+ common_parent = match common_parent.traversal_parent() {
+ Some(parent) => parent,
+ None => {
+ debug_assert!(self.elements.is_empty());
+ if cfg!(feature = "gecko") {
+ break;
+ } else {
+ panic!("should have found a common ancestor");
+ }
+ },
+ }
+ }
+
+ // Now the parents match, so insert the stack of elements we have been
+ // collecting so far.
+ for parent in parents_to_insert.drain(..).rev() {
+ self.push(parent);
+ }
+
+ debug_assert_eq!(self.elements.len(), element_depth);
+
+ // We're done! Easy.
+ }
+}
diff --git a/servo/components/style/build.rs b/servo/components/style/build.rs
new file mode 100644
index 0000000000..2247e87618
--- /dev/null
+++ b/servo/components/style/build.rs
@@ -0,0 +1,91 @@
+/* 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/. */
+
+#[macro_use]
+extern crate lazy_static;
+
+use std::env;
+use std::path::Path;
+use std::process::{exit, Command};
+use walkdir::WalkDir;
+
+#[cfg(feature = "gecko")]
+mod build_gecko;
+
+#[cfg(not(feature = "gecko"))]
+mod build_gecko {
+ pub fn generate() {}
+}
+
+lazy_static! {
+ pub static ref PYTHON: String = env::var("PYTHON3").ok().unwrap_or_else(|| {
+ let candidates = if cfg!(windows) {
+ ["python3.exe"]
+ } else {
+ ["python3"]
+ };
+ for &name in &candidates {
+ if Command::new(name)
+ .arg("--version")
+ .output()
+ .ok()
+ .map_or(false, |out| out.status.success())
+ {
+ return name.to_owned();
+ }
+ }
+ panic!(
+ "Can't find python (tried {})! Try fixing PATH or setting the PYTHON3 env var",
+ candidates.join(", ")
+ )
+ });
+}
+
+fn generate_properties(engine: &str) {
+ for entry in WalkDir::new("properties") {
+ let entry = entry.unwrap();
+ match entry.path().extension().and_then(|e| e.to_str()) {
+ Some("mako") | Some("rs") | Some("py") | Some("zip") => {
+ println!("cargo:rerun-if-changed={}", entry.path().display());
+ },
+ _ => {},
+ }
+ }
+
+ let script = Path::new(&env::var_os("CARGO_MANIFEST_DIR").unwrap())
+ .join("properties")
+ .join("build.py");
+
+ let status = Command::new(&*PYTHON)
+ .arg(&script)
+ .arg(engine)
+ .arg("style-crate")
+ .status()
+ .unwrap();
+ if !status.success() {
+ exit(1)
+ }
+}
+
+fn main() {
+ let gecko = cfg!(feature = "gecko");
+ let servo = cfg!(feature = "servo");
+ let l2013 = cfg!(feature = "servo-layout-2013");
+ let l2020 = cfg!(feature = "servo-layout-2020");
+ let engine = match (gecko, servo, l2013, l2020) {
+ (true, false, false, false) => "gecko",
+ (false, true, true, false) => "servo-2013",
+ (false, true, false, true) => "servo-2020",
+ _ => panic!(
+ "\n\n\
+ The style crate requires enabling one of its 'servo' or 'gecko' feature flags \
+ and, in the 'servo' case, one of 'servo-layout-2013' or 'servo-layout-2020'.\
+ \n\n"
+ ),
+ };
+ println!("cargo:rerun-if-changed=build.rs");
+ println!("cargo:out_dir={}", env::var("OUT_DIR").unwrap());
+ generate_properties(engine);
+ build_gecko::generate();
+}
diff --git a/servo/components/style/build_gecko.rs b/servo/components/style/build_gecko.rs
new file mode 100644
index 0000000000..a83c5dbc6d
--- /dev/null
+++ b/servo/components/style/build_gecko.rs
@@ -0,0 +1,400 @@
+/* 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/. */
+
+use super::PYTHON;
+use bindgen::{Builder, CodegenConfig};
+use regex::Regex;
+use std::cmp;
+use std::collections::HashSet;
+use std::env;
+use std::fs::{self, File};
+use std::io::{Read, Write};
+use std::path::{Path, PathBuf};
+use std::process::{exit, Command};
+use std::slice;
+use std::sync::Mutex;
+use std::time::SystemTime;
+use toml;
+use toml::value::Table;
+
+lazy_static! {
+ static ref OUTDIR_PATH: PathBuf = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("gecko");
+}
+
+const STRUCTS_FILE: &'static str = "structs.rs";
+
+fn read_config(path: &PathBuf) -> Table {
+ println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
+ update_last_modified(&path);
+
+ let mut contents = String::new();
+ File::open(path)
+ .expect("Failed to open config file")
+ .read_to_string(&mut contents)
+ .expect("Failed to read config file");
+ match toml::from_str::<Table>(&contents) {
+ Ok(result) => result,
+ Err(e) => panic!("Failed to parse config file: {}", e),
+ }
+}
+
+lazy_static! {
+ static ref CONFIG: Table = {
+ // Load Gecko's binding generator config from the source tree.
+ let path = mozbuild::TOPSRCDIR.join("layout/style/ServoBindings.toml");
+ read_config(&path)
+ };
+ static ref BINDGEN_FLAGS: Vec<String> = {
+ // Load build-specific config overrides.
+ let path = mozbuild::TOPOBJDIR.join("layout/style/extra-bindgen-flags");
+ println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
+ fs::read_to_string(path).expect("Failed to read extra-bindgen-flags file")
+ .split_whitespace()
+ .map(std::borrow::ToOwned::to_owned)
+ .collect()
+ };
+ static ref INCLUDE_RE: Regex = Regex::new(r#"#include\s*"(.+?)""#).unwrap();
+ static ref DISTDIR_PATH: PathBuf = mozbuild::TOPOBJDIR.join("dist");
+ static ref SEARCH_PATHS: Vec<PathBuf> = vec![
+ DISTDIR_PATH.join("include"),
+ DISTDIR_PATH.join("include/nspr"),
+ ];
+ static ref ADDED_PATHS: Mutex<HashSet<PathBuf>> = Mutex::new(HashSet::new());
+ static ref LAST_MODIFIED: Mutex<SystemTime> =
+ Mutex::new(get_modified_time(&env::current_exe().unwrap())
+ .expect("Failed to get modified time of executable"));
+}
+
+fn get_modified_time(file: &Path) -> Option<SystemTime> {
+ file.metadata().and_then(|m| m.modified()).ok()
+}
+
+fn update_last_modified(file: &Path) {
+ let modified = get_modified_time(file).expect("Couldn't get file modification time");
+ let mut last_modified = LAST_MODIFIED.lock().unwrap();
+ *last_modified = cmp::max(modified, *last_modified);
+}
+
+fn search_include(name: &str) -> Option<PathBuf> {
+ for path in SEARCH_PATHS.iter() {
+ let file = path.join(name);
+ if file.is_file() {
+ update_last_modified(&file);
+ return Some(file);
+ }
+ }
+ None
+}
+
+fn add_headers_recursively(path: PathBuf, added_paths: &mut HashSet<PathBuf>) {
+ if added_paths.contains(&path) {
+ return;
+ }
+ let mut file = File::open(&path).unwrap();
+ let mut content = String::new();
+ file.read_to_string(&mut content).unwrap();
+ added_paths.insert(path);
+ // Find all includes and add them recursively
+ for cap in INCLUDE_RE.captures_iter(&content) {
+ if let Some(path) = search_include(cap.get(1).unwrap().as_str()) {
+ add_headers_recursively(path, added_paths);
+ }
+ }
+}
+
+fn add_include(name: &str) -> String {
+ let mut added_paths = ADDED_PATHS.lock().unwrap();
+ let file = match search_include(name) {
+ Some(file) => file,
+ None => panic!("Include not found: {}", name),
+ };
+ let result = String::from(file.to_str().unwrap());
+ add_headers_recursively(file, &mut *added_paths);
+ result
+}
+
+trait BuilderExt {
+ fn get_initial_builder() -> Builder;
+ fn include<T: Into<String>>(self, file: T) -> Builder;
+}
+
+impl BuilderExt for Builder {
+ fn get_initial_builder() -> Builder {
+ // Disable rust unions, because we replace some types inside of
+ // them.
+ let mut builder = Builder::default()
+ .size_t_is_usize(true)
+ .disable_untagged_union();
+
+ let rustfmt_path = env::var_os("RUSTFMT")
+ // This can be replaced with
+ // > .filter(|p| !p.is_empty()).map(PathBuf::from)
+ // once we can use 1.27+.
+ .and_then(|p| {
+ if p.is_empty() {
+ None
+ } else {
+ Some(PathBuf::from(p))
+ }
+ });
+ if let Some(path) = rustfmt_path {
+ builder = builder.with_rustfmt(path);
+ }
+
+ for dir in SEARCH_PATHS.iter() {
+ builder = builder.clang_arg("-I").clang_arg(dir.to_str().unwrap());
+ }
+
+ builder = builder.include(add_include("mozilla-config.h"));
+
+ if env::var("CARGO_FEATURE_GECKO_DEBUG").is_ok() {
+ builder = builder.clang_arg("-DDEBUG=1").clang_arg("-DJS_DEBUG=1");
+ }
+
+ for item in &*BINDGEN_FLAGS {
+ builder = builder.clang_arg(item);
+ }
+
+ builder
+ }
+ fn include<T: Into<String>>(self, file: T) -> Builder {
+ self.clang_arg("-include").clang_arg(file)
+ }
+}
+
+struct Fixup {
+ pat: String,
+ rep: String,
+}
+
+fn write_binding_file(builder: Builder, file: &str, fixups: &[Fixup]) {
+ let out_file = OUTDIR_PATH.join(file);
+ if let Some(modified) = get_modified_time(&out_file) {
+ // Don't generate the file if nothing it depends on was modified.
+ let last_modified = LAST_MODIFIED.lock().unwrap();
+ if *last_modified <= modified {
+ return;
+ }
+ }
+ let command_line_opts = builder.command_line_flags();
+ let result = builder.generate();
+ let mut result = match result {
+ Ok(bindings) => bindings.to_string(),
+ Err(_) => {
+ panic!(
+ "Failed to generate bindings, flags: {:?}",
+ command_line_opts
+ );
+ },
+ };
+
+ for fixup in fixups.iter() {
+ result = Regex::new(&fixup.pat)
+ .unwrap()
+ .replace_all(&result, &*fixup.rep)
+ .into_owned()
+ .into();
+ }
+ let bytes = result.into_bytes();
+ File::create(&out_file)
+ .unwrap()
+ .write_all(&bytes)
+ .expect("Unable to write output");
+}
+
+struct BuilderWithConfig<'a> {
+ builder: Builder,
+ config: &'a Table,
+ used_keys: HashSet<&'static str>,
+}
+impl<'a> BuilderWithConfig<'a> {
+ fn new(builder: Builder, config: &'a Table) -> Self {
+ BuilderWithConfig {
+ builder,
+ config,
+ used_keys: HashSet::new(),
+ }
+ }
+
+ fn handle_list<F>(self, key: &'static str, func: F) -> BuilderWithConfig<'a>
+ where
+ F: FnOnce(Builder, slice::Iter<'a, toml::Value>) -> Builder,
+ {
+ let mut builder = self.builder;
+ let config = self.config;
+ let mut used_keys = self.used_keys;
+ if let Some(list) = config.get(key) {
+ used_keys.insert(key);
+ builder = func(builder, list.as_array().unwrap().as_slice().iter());
+ }
+ BuilderWithConfig {
+ builder,
+ config,
+ used_keys,
+ }
+ }
+ fn handle_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a>
+ where
+ F: FnMut(Builder, &'a toml::Value) -> Builder,
+ {
+ self.handle_list(key, |b, iter| iter.fold(b, |b, item| func(b, item)))
+ }
+ fn handle_str_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a>
+ where
+ F: FnMut(Builder, &'a str) -> Builder,
+ {
+ self.handle_items(key, |b, item| func(b, item.as_str().unwrap()))
+ }
+ fn handle_table_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a>
+ where
+ F: FnMut(Builder, &'a Table) -> Builder,
+ {
+ self.handle_items(key, |b, item| func(b, item.as_table().unwrap()))
+ }
+ fn handle_common(self, fixups: &mut Vec<Fixup>) -> BuilderWithConfig<'a> {
+ self.handle_str_items("headers", |b, item| b.header(add_include(item)))
+ .handle_str_items("raw-lines", |b, item| b.raw_line(item))
+ .handle_str_items("hide-types", |b, item| b.blocklist_type(item))
+ .handle_table_items("fixups", |builder, item| {
+ fixups.push(Fixup {
+ pat: item["pat"].as_str().unwrap().into(),
+ rep: item["rep"].as_str().unwrap().into(),
+ });
+ builder
+ })
+ }
+
+ fn get_builder(self) -> Builder {
+ for key in self.config.keys() {
+ if !self.used_keys.contains(key.as_str()) {
+ panic!("Unknown key: {}", key);
+ }
+ }
+ self.builder
+ }
+}
+
+fn generate_structs() {
+ let builder = Builder::get_initial_builder()
+ .enable_cxx_namespaces()
+ .with_codegen_config(CodegenConfig::TYPES | CodegenConfig::VARS | CodegenConfig::FUNCTIONS);
+ let mut fixups = vec![];
+ let builder = BuilderWithConfig::new(builder, CONFIG["structs"].as_table().unwrap())
+ .handle_common(&mut fixups)
+ .handle_str_items("allowlist-functions", |b, item| b.allowlist_function(item))
+ .handle_str_items("bitfield-enums", |b, item| b.bitfield_enum(item))
+ .handle_str_items("rusty-enums", |b, item| b.rustified_enum(item))
+ .handle_str_items("allowlist-vars", |b, item| b.allowlist_var(item))
+ .handle_str_items("allowlist-types", |b, item| b.allowlist_type(item))
+ .handle_str_items("opaque-types", |b, item| b.opaque_type(item))
+ .handle_table_items("cbindgen-types", |b, item| {
+ let gecko = item["gecko"].as_str().unwrap();
+ let servo = item["servo"].as_str().unwrap();
+ b.blocklist_type(format!("mozilla::{}", gecko))
+ .module_raw_line("root::mozilla", format!("pub use {} as {};", servo, gecko))
+ })
+ .handle_table_items("mapped-generic-types", |builder, item| {
+ let generic = item["generic"].as_bool().unwrap();
+ let gecko = item["gecko"].as_str().unwrap();
+ let servo = item["servo"].as_str().unwrap();
+ let gecko_name = gecko.rsplit("::").next().unwrap();
+ let gecko = gecko
+ .split("::")
+ .map(|s| format!("\\s*{}\\s*", s))
+ .collect::<Vec<_>>()
+ .join("::");
+
+ fixups.push(Fixup {
+ pat: format!("\\broot\\s*::\\s*{}\\b", gecko),
+ rep: format!("crate::gecko_bindings::structs::{}", gecko_name),
+ });
+ builder.blocklist_type(gecko).raw_line(format!(
+ "pub type {0}{2} = {1}{2};",
+ gecko_name,
+ servo,
+ if generic { "<T>" } else { "" }
+ ))
+ })
+ .get_builder();
+ write_binding_file(builder, STRUCTS_FILE, &fixups);
+}
+
+fn setup_logging() -> bool {
+ struct BuildLogger {
+ file: Option<Mutex<fs::File>>,
+ filter: String,
+ }
+
+ impl log::Log for BuildLogger {
+ fn enabled(&self, meta: &log::Metadata) -> bool {
+ self.file.is_some() && meta.target().contains(&self.filter)
+ }
+
+ fn log(&self, record: &log::Record) {
+ if !self.enabled(record.metadata()) {
+ return;
+ }
+
+ let mut file = self.file.as_ref().unwrap().lock().unwrap();
+ let _ = writeln!(
+ file,
+ "{} - {} - {} @ {}:{}",
+ record.level(),
+ record.target(),
+ record.args(),
+ record.file().unwrap_or("<unknown>"),
+ record.line().unwrap_or(0)
+ );
+ }
+
+ fn flush(&self) {
+ if let Some(ref file) = self.file {
+ file.lock().unwrap().flush().unwrap();
+ }
+ }
+ }
+
+ if let Some(path) = env::var_os("STYLO_BUILD_LOG") {
+ log::set_max_level(log::LevelFilter::Debug);
+ log::set_boxed_logger(Box::new(BuildLogger {
+ file: fs::File::create(path).ok().map(Mutex::new),
+ filter: env::var("STYLO_BUILD_FILTER")
+ .ok()
+ .unwrap_or_else(|| "bindgen".to_owned()),
+ }))
+ .expect("Failed to set logger.");
+
+ true
+ } else {
+ false
+ }
+}
+
+fn generate_atoms() {
+ let script = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap())
+ .join("gecko")
+ .join("regen_atoms.py");
+ println!("cargo:rerun-if-changed={}", script.display());
+ let status = Command::new(&*PYTHON)
+ .arg(&script)
+ .arg(DISTDIR_PATH.as_os_str())
+ .arg(OUTDIR_PATH.as_os_str())
+ .status()
+ .unwrap();
+ if !status.success() {
+ exit(1);
+ }
+}
+
+pub fn generate() {
+ println!("cargo:rerun-if-changed=build_gecko.rs");
+ fs::create_dir_all(&*OUTDIR_PATH).unwrap();
+ setup_logging();
+ generate_structs();
+ generate_atoms();
+
+ for path in ADDED_PATHS.lock().unwrap().iter() {
+ println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
+ }
+}
diff --git a/servo/components/style/color/convert.rs b/servo/components/style/color/convert.rs
new file mode 100644
index 0000000000..a6274db39a
--- /dev/null
+++ b/servo/components/style/color/convert.rs
@@ -0,0 +1,902 @@
+/* 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/. */
+
+//! Color conversion algorithms.
+//!
+//! Algorithms, matrices and constants are from the [color-4] specification,
+//! unless otherwise specified:
+//!
+//! https://drafts.csswg.org/css-color-4/#color-conversion-code
+//!
+//! NOTE: Matrices has to be transposed from the examples in the spec for use
+//! with the `euclid` library.
+
+use crate::color::ColorComponents;
+use crate::values::normalize;
+
+type Transform = euclid::default::Transform3D<f32>;
+type Vector = euclid::default::Vector3D<f32>;
+
+/// Normalize hue into [0, 360).
+#[inline]
+pub fn normalize_hue(hue: f32) -> f32 {
+ hue - 360. * (hue / 360.).floor()
+}
+
+/// Calculate the hue from RGB components and return it along with the min and
+/// max RGB values.
+#[inline]
+fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) {
+ let max = red.max(green).max(blue);
+ let min = red.min(green).min(blue);
+
+ let delta = max - min;
+
+ let hue = if delta != 0.0 {
+ 60.0 * if max == red {
+ (green - blue) / delta + if green < blue { 6.0 } else { 0.0 }
+ } else if max == green {
+ (blue - red) / delta + 2.0
+ } else {
+ (red - green) / delta + 4.0
+ }
+ } else {
+ f32::NAN
+ };
+
+ (hue, min, max)
+}
+
+/// Convert from HSL notation to RGB notation.
+/// https://drafts.csswg.org/css-color-4/#hsl-to-rgb
+#[inline]
+pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents {
+ fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 {
+ let hue = normalize_hue(hue);
+
+ if hue * 6.0 < 360.0 {
+ t1 + (t2 - t1) * hue / 60.0
+ } else if hue * 2.0 < 360.0 {
+ t2
+ } else if hue * 3.0 < 720.0 {
+ t1 + (t2 - t1) * (240.0 - hue) / 60.0
+ } else {
+ t1
+ }
+ }
+
+ // Convert missing components to 0.0.
+ let ColorComponents(hue, saturation, lightness) = from.map(normalize);
+ let saturation = saturation / 100.0;
+ let lightness = lightness / 100.0;
+
+ let t2 = if lightness <= 0.5 {
+ lightness * (saturation + 1.0)
+ } else {
+ lightness + saturation - lightness * saturation
+ };
+ let t1 = lightness * 2.0 - t2;
+
+ ColorComponents(
+ hue_to_rgb(t1, t2, hue + 120.0),
+ hue_to_rgb(t1, t2, hue),
+ hue_to_rgb(t1, t2, hue - 120.0),
+ )
+}
+
+/// Convert from RGB notation to HSL notation.
+/// https://drafts.csswg.org/css-color-4/#rgb-to-hsl
+pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(red, green, blue) = *from;
+
+ let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
+
+ let lightness = (min + max) / 2.0;
+ let delta = max - min;
+
+ let saturation = if delta != 0.0 {
+ if lightness == 0.0 || lightness == 1.0 {
+ 0.0
+ } else {
+ (max - lightness) / lightness.min(1.0 - lightness)
+ }
+ } else {
+ 0.0
+ };
+
+ ColorComponents(hue, saturation * 100.0, lightness * 100.0)
+}
+
+/// Convert from HWB notation to RGB notation.
+/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb
+#[inline]
+pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents {
+ // Convert missing components to 0.0.
+ let ColorComponents(hue, whiteness, blackness) = from.map(normalize);
+
+ let whiteness = whiteness / 100.0;
+ let blackness = blackness / 100.0;
+
+ if whiteness + blackness >= 1.0 {
+ let gray = whiteness / (whiteness + blackness);
+ return ColorComponents(gray, gray, gray);
+ }
+
+ let x = 1.0 - whiteness - blackness;
+ hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness)
+}
+
+/// Convert from RGB notation to HWB notation.
+/// https://drafts.csswg.org/css-color-4/#rgb-to-hwb
+#[inline]
+pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(red, green, blue) = *from;
+
+ let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
+
+ let whiteness = min;
+ let blackness = 1.0 - max;
+
+ ColorComponents(hue, whiteness * 100.0, blackness * 100.0)
+}
+
+/// Convert from the rectangular orthogonal to the cylindrical polar coordinate
+/// system. This is used to convert (ok)lab to (ok)lch.
+/// <https://drafts.csswg.org/css-color-4/#lab-to-lch>
+#[inline]
+pub fn orthogonal_to_polar(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(lightness, a, b) = *from;
+
+ let chroma = (a * a + b * b).sqrt();
+
+ // Very small chroma values make the hue component powerless.
+ let hue = if chroma.abs() < 1.0e-6 {
+ f32::NAN
+ } else {
+ normalize_hue(b.atan2(a).to_degrees())
+ };
+
+ ColorComponents(lightness, chroma, hue)
+}
+
+/// Convert from the cylindrical polar to the rectangular orthogonal coordinate
+/// system. This is used to convert (ok)lch to (ok)lab.
+/// <https://drafts.csswg.org/css-color-4/#lch-to-lab>
+#[inline]
+pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(lightness, chroma, hue) = *from;
+
+ // A missing hue component results in an achromatic color.
+ if hue.is_nan() {
+ return ColorComponents(lightness, 0.0, 0.0);
+ }
+
+ let hue = hue.to_radians();
+ let a = chroma * hue.cos();
+ let b = chroma * hue.sin();
+
+ ColorComponents(lightness, a, b)
+}
+
+#[inline]
+fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents {
+ let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2));
+ ColorComponents(result.x, result.y, result.z)
+}
+
+fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents {
+ #[rustfmt::skip]
+ const MAT: Transform = Transform::new(
+ 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0,
+ 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0,
+ -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ transform(from, &MAT)
+}
+
+fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents {
+ #[rustfmt::skip]
+ const MAT: Transform = Transform::new(
+ 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0,
+ -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0,
+ 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ transform(from, &MAT)
+}
+
+/// A reference white that is used during color conversion.
+pub enum WhitePoint {
+ /// D50 white reference.
+ D50,
+ /// D65 white reference.
+ D65,
+}
+
+impl WhitePoint {
+ const fn values(&self) -> ColorComponents {
+ // <https://drafts.csswg.org/css-color-4/#color-conversion-code>
+ match self {
+ // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585]
+ WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602),
+ // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290]
+ WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784),
+ }
+ }
+}
+
+fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) {
+ match (from, to) {
+ (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components),
+ (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components),
+ _ => {},
+ }
+}
+
+/// A trait that allows conversion of color spaces to and from XYZ coordinate
+/// space with a specified white point.
+///
+/// Allows following the specified method of converting between color spaces:
+/// - Convert to values to sRGB linear light.
+/// - Convert to XYZ coordinate space.
+/// - Adjust white point to target white point.
+/// - Convert to sRGB linear light in target color space.
+/// - Convert to sRGB gamma encoded in target color space.
+///
+/// https://drafts.csswg.org/css-color-4/#color-conversion
+pub trait ColorSpaceConversion {
+ /// The white point that the implementer is represented in.
+ const WHITE_POINT: WhitePoint;
+
+ /// Convert the components from sRGB gamma encoded values to sRGB linear
+ /// light values.
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents;
+
+ /// Convert the components from sRGB linear light values to XYZ coordinate
+ /// space.
+ fn to_xyz(from: &ColorComponents) -> ColorComponents;
+
+ /// Convert the components from XYZ coordinate space to sRGB linear light
+ /// values.
+ fn from_xyz(from: &ColorComponents) -> ColorComponents;
+
+ /// Convert the components from sRGB linear light values to sRGB gamma
+ /// encoded values.
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents;
+}
+
+/// Convert the color components from the specified color space to XYZ and
+/// return the components and the white point they are in.
+pub fn to_xyz<From: ColorSpaceConversion>(from: &ColorComponents) -> (ColorComponents, WhitePoint) {
+ // Convert the color components where in-gamut values are in the range
+ // [0 - 1] to linear light (un-companded) form.
+ let result = From::to_linear_light(from);
+
+ // Convert the color components from the source color space to XYZ.
+ (From::to_xyz(&result), From::WHITE_POINT)
+}
+
+/// Convert the color components from XYZ at the given white point to the
+/// specified color space.
+pub fn from_xyz<To: ColorSpaceConversion>(
+ from: &ColorComponents,
+ white_point: WhitePoint,
+) -> ColorComponents {
+ let mut xyz = from.clone();
+
+ // Convert the white point if needed.
+ convert_white_point(white_point, To::WHITE_POINT, &mut xyz);
+
+ // Convert the color from XYZ to the target color space.
+ let result = To::from_xyz(&xyz);
+
+ // Convert the color components of linear-light values in the range
+ // [0 - 1] to a gamma corrected form.
+ To::to_gamma_encoded(&result)
+}
+
+/// The sRGB color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-sRGB
+pub struct Srgb;
+
+impl Srgb {
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0,
+ 0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0,
+ 0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0,
+ -1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0,
+ -0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for Srgb {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|value| {
+ let abs = value.abs();
+
+ if abs < 0.04045 {
+ value / 12.92
+ } else {
+ value.signum() * ((abs + 0.055) / 1.055).powf(2.4)
+ }
+ })
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|value| {
+ let abs = value.abs();
+
+ if abs > 0.0031308 {
+ value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055)
+ } else {
+ 12.92 * value
+ }
+ })
+ }
+}
+
+/// Color specified with hue, saturation and lightness components.
+pub struct Hsl;
+
+impl ColorSpaceConversion for Hsl {
+ const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_linear_light(&hsl_to_rgb(from))
+ }
+
+ #[inline]
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_xyz(from)
+ }
+
+ #[inline]
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::from_xyz(from)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ rgb_to_hsl(&Srgb::to_gamma_encoded(from))
+ }
+}
+
+/// Color specified with hue, whiteness and blackness components.
+pub struct Hwb;
+
+impl ColorSpaceConversion for Hwb {
+ const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_linear_light(&hwb_to_rgb(from))
+ }
+
+ #[inline]
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_xyz(from)
+ }
+
+ #[inline]
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::from_xyz(from)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ rgb_to_hwb(&Srgb::to_gamma_encoded(from))
+ }
+}
+
+/// The same as sRGB color space, except the transfer function is linear light.
+/// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear
+pub struct SrgbLinear;
+
+impl ColorSpaceConversion for SrgbLinear {
+ const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // Already in linear light form.
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_xyz(from)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::from_xyz(from)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // Stay in linear light form.
+ from.clone()
+ }
+}
+
+/// The Display-P3 color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-display-p3
+pub struct DisplayP3;
+
+impl DisplayP3 {
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.48657094864821626, 0.22897456406974884, 0.0, 0.0,
+ 0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0,
+ 0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0,
+ -0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0,
+ -0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for DisplayP3 {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_linear_light(from)
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_gamma_encoded(from)
+ }
+}
+
+/// The a98-rgb color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb
+pub struct A98Rgb;
+
+impl A98Rgb {
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0,
+ 0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0,
+ 0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0,
+ -0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0,
+ -0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for A98Rgb {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|v| v.signum() * v.abs().powf(2.19921875))
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ .map(|v| v.signum() * v.abs().powf(0.4547069271758437))
+ }
+}
+
+/// The ProPhoto RGB color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb
+pub struct ProphotoRgb;
+
+impl ProphotoRgb {
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.7977604896723027, 0.2880711282292934, 0.0, 0.0,
+ 0.13518583717574031, 0.7118432178101014, 0.0, 0.0,
+ 0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 1.3457989731028281, -0.5446224939028347, 0.0, 0.0,
+ -0.25558010007997534, 1.5082327413132781, 0.0, 0.0,
+ -0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for ProphotoRgb {
+ const WHITE_POINT: WhitePoint = WhitePoint::D50;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|value| {
+ const ET2: f32 = 16.0 / 512.0;
+
+ let abs = value.abs();
+
+ if abs <= ET2 {
+ value / 16.0
+ } else {
+ value.signum() * abs.powf(1.8)
+ }
+ })
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ const ET: f32 = 1.0 / 512.0;
+
+ from.clone().map(|v| {
+ let abs = v.abs();
+ if abs >= ET {
+ v.signum() * abs.powf(1.0 / 1.8)
+ } else {
+ 16.0 * v
+ }
+ })
+ }
+}
+
+/// The Rec.2020 color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-rec2020
+pub struct Rec2020;
+
+impl Rec2020 {
+ const ALPHA: f32 = 1.09929682680944;
+ const BETA: f32 = 0.018053968510807;
+
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.6369580483012913, 0.26270021201126703, 0.0, 0.0,
+ 0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0,
+ 0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0,
+ -0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0,
+ -0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for Rec2020 {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|value| {
+ let abs = value.abs();
+
+ if abs < Self::BETA * 4.5 {
+ value / 4.5
+ } else {
+ value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45)
+ }
+ })
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|v| {
+ let abs = v.abs();
+
+ if abs > Self::BETA {
+ v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0))
+ } else {
+ 4.5 * v
+ }
+ })
+ }
+}
+
+/// A color in the XYZ coordinate space with a D50 white reference.
+/// https://drafts.csswg.org/css-color-4/#predefined-xyz
+pub struct XyzD50;
+
+impl ColorSpaceConversion for XyzD50 {
+ const WHITE_POINT: WhitePoint = WhitePoint::D50;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+}
+
+/// A color in the XYZ coordinate space with a D65 white reference.
+/// https://drafts.csswg.org/css-color-4/#predefined-xyz
+pub struct XyzD65;
+
+impl ColorSpaceConversion for XyzD65 {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+}
+
+/// The Lab color space.
+/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch
+pub struct Lab;
+
+impl Lab {
+ const KAPPA: f32 = 24389.0 / 27.0;
+ const EPSILON: f32 = 216.0 / 24389.0;
+}
+
+impl ColorSpaceConversion for Lab {
+ const WHITE_POINT: WhitePoint = WhitePoint::D50;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+
+ /// Convert a CIELAB color to XYZ as specified in [1] and [2].
+ ///
+ /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined
+ /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(lightness, a, b) = *from;
+
+ let f1 = (lightness + 16.0) / 116.0;
+ let f0 = f1 + a / 500.0;
+ let f2 = f1 - b / 200.0;
+
+ let f0_cubed = f0 * f0 * f0;
+ let x = if f0_cubed > Self::EPSILON {
+ f0_cubed
+ } else {
+ (116.0 * f0 - 16.0) / Self::KAPPA
+ };
+
+ let y = if lightness > Self::KAPPA * Self::EPSILON {
+ let v = (lightness + 16.0) / 116.0;
+ v * v * v
+ } else {
+ lightness / Self::KAPPA
+ };
+
+ let f2_cubed = f2 * f2 * f2;
+ let z = if f2_cubed > Self::EPSILON {
+ f2_cubed
+ } else {
+ (116.0 * f2 - 16.0) / Self::KAPPA
+ };
+
+ ColorComponents(x, y, z) * Self::WHITE_POINT.values()
+ }
+
+ /// Convert an XYZ color to LAB as specified in [1] and [2].
+ ///
+ /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab
+ /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ let adapted = *from / Self::WHITE_POINT.values();
+
+ // 4. Convert D50-adapted XYZ to Lab.
+ let ColorComponents(f0, f1, f2) = adapted.map(|v| {
+ if v > Self::EPSILON {
+ v.cbrt()
+ } else {
+ (Self::KAPPA * v + 16.0) / 116.0
+ }
+ });
+
+ let lightness = 116.0 * f1 - 16.0;
+ let a = 500.0 * (f0 - f1);
+ let b = 200.0 * (f1 - f2);
+
+ ColorComponents(lightness, a, b)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+}
+
+/// The Lch color space.
+/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch
+pub struct Lch;
+
+impl ColorSpaceConversion for Lch {
+ const WHITE_POINT: WhitePoint = Lab::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ // Convert LCH to Lab first.
+ let lab = polar_to_orthogonal(from);
+
+ // Then convert the Lab to XYZ.
+ Lab::to_xyz(&lab)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ // First convert the XYZ to LAB.
+ let lab = Lab::from_xyz(&from);
+
+ // Then convert the Lab to LCH.
+ orthogonal_to_polar(&lab)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+}
+
+/// The Oklab color space.
+/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
+pub struct Oklab;
+
+impl Oklab {
+ #[rustfmt::skip]
+ const XYZ_TO_LMS: Transform = Transform::new(
+ 0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0,
+ 0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0,
+ -0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const LMS_TO_OKLAB: Transform = Transform::new(
+ 0.2104542553, 1.9779984951, 0.0259040371, 0.0,
+ 0.7936177850, -2.4285922050, 0.7827717662, 0.0,
+ -0.0040720468, 0.4505937099, -0.8086757660, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const LMS_TO_XYZ: Transform = Transform::new(
+ 1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0,
+ -0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0,
+ 0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const OKLAB_TO_LMS: Transform = Transform::new(
+ 0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0,
+ 0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0,
+ 0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for Oklab {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ let lms = transform(&from, &Self::OKLAB_TO_LMS);
+ let lms = lms.map(|v| v * v * v);
+ transform(&lms, &Self::LMS_TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ let lms = transform(&from, &Self::XYZ_TO_LMS);
+ let lms = lms.map(|v| v.cbrt());
+ transform(&lms, &Self::LMS_TO_OKLAB)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+}
+
+/// The Oklch color space.
+/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
+pub struct Oklch;
+
+impl ColorSpaceConversion for Oklch {
+ const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ // First convert OkLCH to Oklab.
+ let oklab = polar_to_orthogonal(from);
+
+ // Then convert Oklab to XYZ.
+ Oklab::to_xyz(&oklab)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ // First convert XYZ to Oklab.
+ let lab = Oklab::from_xyz(&from);
+
+ // Then convert Oklab to OkLCH.
+ orthogonal_to_polar(&lab)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+}
diff --git a/servo/components/style/color/mix.rs b/servo/components/style/color/mix.rs
new file mode 100644
index 0000000000..bcc4628575
--- /dev/null
+++ b/servo/components/style/color/mix.rs
@@ -0,0 +1,558 @@
+/* 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/. */
+
+//! Color mixing/interpolation.
+
+use super::{AbsoluteColor, ColorFlags, ColorSpace};
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::color::ColorMixFlags;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A hue-interpolation-method as defined in [1].
+///
+/// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum HueInterpolationMethod {
+ /// https://drafts.csswg.org/css-color-4/#shorter
+ Shorter,
+ /// https://drafts.csswg.org/css-color-4/#longer
+ Longer,
+ /// https://drafts.csswg.org/css-color-4/#increasing
+ Increasing,
+ /// https://drafts.csswg.org/css-color-4/#decreasing
+ Decreasing,
+ /// https://drafts.csswg.org/css-color-4/#specified
+ Specified,
+}
+
+/// https://drafts.csswg.org/css-color-4/#color-interpolation-method
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToShmem,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+)]
+#[repr(C)]
+pub struct ColorInterpolationMethod {
+ /// The color-space the interpolation should be done in.
+ pub space: ColorSpace,
+ /// The hue interpolation method.
+ pub hue: HueInterpolationMethod,
+}
+
+impl ColorInterpolationMethod {
+ /// Returns the srgb interpolation method.
+ pub const fn srgb() -> Self {
+ Self {
+ space: ColorSpace::Srgb,
+ hue: HueInterpolationMethod::Shorter,
+ }
+ }
+
+ /// Return the oklab interpolation method used for default color
+ /// interpolcation.
+ pub const fn oklab() -> Self {
+ Self {
+ space: ColorSpace::Oklab,
+ hue: HueInterpolationMethod::Shorter,
+ }
+ }
+
+ /// Decides the best method for interpolating between the given colors.
+ /// https://drafts.csswg.org/css-color-4/#interpolation-space
+ pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self {
+ // The preferred color space to use for interpolating colors is Oklab.
+ // However, if either of the colors are in legacy rgb(), hsl() or hwb(),
+ // then interpolation is done in sRGB.
+ if !left.is_legacy_syntax() || !right.is_legacy_syntax() {
+ Self::oklab()
+ } else {
+ Self::srgb()
+ }
+ }
+}
+
+impl Parse for ColorInterpolationMethod {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_ident_matching("in")?;
+ let space = ColorSpace::parse(input)?;
+ // https://drafts.csswg.org/css-color-4/#hue-interpolation
+ // Unless otherwise specified, if no specific hue interpolation
+ // algorithm is selected by the host syntax, the default is shorter.
+ let hue = if space.is_polar() {
+ input
+ .try_parse(|input| -> Result<_, ParseError<'i>> {
+ let hue = HueInterpolationMethod::parse(input)?;
+ input.expect_ident_matching("hue")?;
+ Ok(hue)
+ })
+ .unwrap_or(HueInterpolationMethod::Shorter)
+ } else {
+ HueInterpolationMethod::Shorter
+ };
+ Ok(Self { space, hue })
+ }
+}
+
+impl ToCss for ColorInterpolationMethod {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("in ")?;
+ self.space.to_css(dest)?;
+ if self.hue != HueInterpolationMethod::Shorter {
+ dest.write_char(' ')?;
+ self.hue.to_css(dest)?;
+ dest.write_str(" hue")?;
+ }
+ Ok(())
+ }
+}
+
+/// Mix two colors into one.
+pub fn mix(
+ interpolation: ColorInterpolationMethod,
+ left_color: &AbsoluteColor,
+ mut left_weight: f32,
+ right_color: &AbsoluteColor,
+ mut right_weight: f32,
+ flags: ColorMixFlags,
+) -> AbsoluteColor {
+ // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
+ let mut alpha_multiplier = 1.0;
+ if flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS) {
+ let sum = left_weight + right_weight;
+ if sum != 1.0 {
+ let scale = 1.0 / sum;
+ left_weight *= scale;
+ right_weight *= scale;
+ if sum < 1.0 {
+ alpha_multiplier = sum;
+ }
+ }
+ }
+
+ let result = mix_in(
+ interpolation.space,
+ left_color,
+ left_weight,
+ right_color,
+ right_weight,
+ interpolation.hue,
+ alpha_multiplier,
+ );
+
+ if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
+ // If the result *MUST* be in modern syntax, then make sure it is in a
+ // color space that allows the modern syntax. So hsl and hwb will be
+ // converted to srgb.
+ if result.is_legacy_syntax() {
+ result.to_color_space(ColorSpace::Srgb)
+ } else {
+ result
+ }
+ } else if left_color.is_legacy_syntax() && right_color.is_legacy_syntax() {
+ // If both sides of the mix is legacy then convert the result back into
+ // legacy.
+ result.into_srgb_legacy()
+ } else {
+ result
+ }
+}
+
+/// What the outcome of each component should be in a mix result.
+#[derive(Clone, Copy)]
+#[repr(u8)]
+enum ComponentMixOutcome {
+ /// Mix the left and right sides to give the result.
+ Mix,
+ /// Carry the left side forward to the result.
+ UseLeft,
+ /// Carry the right side forward to the result.
+ UseRight,
+ /// The resulting component should also be none.
+ None,
+}
+
+impl ComponentMixOutcome {
+ fn from_colors(
+ left: &AbsoluteColor,
+ right: &AbsoluteColor,
+ flags_to_check: ColorFlags,
+ ) -> Self {
+ match (
+ left.flags.contains(flags_to_check),
+ right.flags.contains(flags_to_check),
+ ) {
+ (true, true) => Self::None,
+ (true, false) => Self::UseRight,
+ (false, true) => Self::UseLeft,
+ (false, false) => Self::Mix,
+ }
+ }
+}
+
+/// Calculate the flags that should be carried forward a color before converting
+/// it to the interpolation color space according to:
+/// <https://drafts.csswg.org/css-color-4/#interpolation-missing>
+fn carry_forward_analogous_missing_components(
+ from: ColorSpace,
+ to: ColorSpace,
+ flags: ColorFlags,
+) -> ColorFlags {
+ use ColorFlags as F;
+ use ColorSpace as S;
+
+ if from == to {
+ return flags;
+ }
+
+ // Reds r, x
+ // Greens g, y
+ // Blues b, z
+ if from.is_rgb_or_xyz_like() && to.is_rgb_or_xyz_like() {
+ return flags;
+ }
+
+ let mut result = flags;
+
+ // Lightness L
+ if matches!(from, S::Lab | S::Lch | S::Oklab | S::Oklch) {
+ if matches!(to, S::Lab | S::Lch | S::Oklab | S::Oklch) {
+ result.set(F::C0_IS_NONE, flags.contains(F::C0_IS_NONE));
+ } else if matches!(to, S::Hsl) {
+ result.set(F::C2_IS_NONE, flags.contains(F::C0_IS_NONE));
+ }
+ } else if matches!(from, S::Hsl) && matches!(to, S::Lab | S::Lch | S::Oklab | S::Oklch) {
+ result.set(F::C0_IS_NONE, flags.contains(F::C2_IS_NONE));
+ }
+
+ // Colorfulness C, S
+ if matches!(from, S::Hsl | S::Lch | S::Oklch) && matches!(to, S::Hsl | S::Lch | S::Oklch) {
+ result.set(F::C1_IS_NONE, flags.contains(F::C1_IS_NONE));
+ }
+
+ // Hue H
+ if matches!(from, S::Hsl | S::Hwb) {
+ if matches!(to, S::Hsl | S::Hwb) {
+ result.set(F::C0_IS_NONE, flags.contains(F::C0_IS_NONE));
+ } else if matches!(to, S::Lch | S::Oklch) {
+ result.set(F::C2_IS_NONE, flags.contains(F::C0_IS_NONE));
+ }
+ } else if matches!(from, S::Lch | S::Oklch) {
+ if matches!(to, S::Hsl | S::Hwb) {
+ result.set(F::C0_IS_NONE, flags.contains(F::C2_IS_NONE));
+ } else if matches!(to, S::Lch | S::Oklch) {
+ result.set(F::C2_IS_NONE, flags.contains(F::C2_IS_NONE));
+ }
+ }
+
+ // Opponent a, a
+ // Opponent b, b
+ if matches!(from, S::Lab | S::Oklab) && matches!(to, S::Lab | S::Oklab) {
+ result.set(F::C1_IS_NONE, flags.contains(F::C1_IS_NONE));
+ result.set(F::C2_IS_NONE, flags.contains(F::C2_IS_NONE));
+ }
+
+ result
+}
+
+fn mix_in(
+ color_space: ColorSpace,
+ left_color: &AbsoluteColor,
+ left_weight: f32,
+ right_color: &AbsoluteColor,
+ right_weight: f32,
+ hue_interpolation: HueInterpolationMethod,
+ alpha_multiplier: f32,
+) -> AbsoluteColor {
+ // Convert both colors into the interpolation color space.
+ let mut left = left_color.to_color_space(color_space);
+ left.flags =
+ carry_forward_analogous_missing_components(left_color.color_space, color_space, left.flags);
+ let mut right = right_color.to_color_space(color_space);
+ right.flags = carry_forward_analogous_missing_components(
+ right_color.color_space,
+ color_space,
+ right.flags,
+ );
+
+ let outcomes = [
+ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE),
+ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE),
+ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE),
+ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE),
+ ];
+
+ // Convert both sides into just components.
+ let left = left.raw_components();
+ let right = right.raw_components();
+
+ let (result, result_flags) = interpolate_premultiplied(
+ &left,
+ left_weight,
+ &right,
+ right_weight,
+ color_space.hue_index(),
+ hue_interpolation,
+ &outcomes,
+ );
+
+ let alpha = if alpha_multiplier != 1.0 {
+ result[3] * alpha_multiplier
+ } else {
+ result[3]
+ };
+
+ // FIXME: In rare cases we end up with 0.999995 in the alpha channel,
+ // so we reduce the precision to avoid serializing to
+ // rgba(?, ?, ?, 1). This is not ideal, so we should look into
+ // ways to avoid it. Maybe pre-multiply all color components and
+ // then divide after calculations?
+ let alpha = (alpha * 1000.0).round() / 1000.0;
+
+ let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], alpha);
+
+ result.flags = result_flags;
+
+ result
+}
+
+fn interpolate_premultiplied_component(
+ left: f32,
+ left_weight: f32,
+ left_alpha: f32,
+ right: f32,
+ right_weight: f32,
+ right_alpha: f32,
+) -> f32 {
+ left * left_weight * left_alpha + right * right_weight * right_alpha
+}
+
+// Normalize hue into [0, 360)
+#[inline]
+fn normalize_hue(v: f32) -> f32 {
+ v - 360. * (v / 360.).floor()
+}
+
+fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) {
+ // Adjust the hue angle as per
+ // https://drafts.csswg.org/css-color/#hue-interpolation.
+ //
+ // If both hue angles are NAN, they should be set to 0. Otherwise, if a
+ // single hue angle is NAN, it should use the other hue angle.
+ if left.is_nan() {
+ if right.is_nan() {
+ *left = 0.;
+ *right = 0.;
+ } else {
+ *left = *right;
+ }
+ } else if right.is_nan() {
+ *right = *left;
+ }
+
+ if hue_interpolation == HueInterpolationMethod::Specified {
+ // Angles are not adjusted. They are interpolated like any other
+ // component.
+ return;
+ }
+
+ *left = normalize_hue(*left);
+ *right = normalize_hue(*right);
+
+ match hue_interpolation {
+ // https://drafts.csswg.org/css-color/#shorter
+ HueInterpolationMethod::Shorter => {
+ let delta = *right - *left;
+
+ if delta > 180. {
+ *left += 360.;
+ } else if delta < -180. {
+ *right += 360.;
+ }
+ },
+ // https://drafts.csswg.org/css-color/#longer
+ HueInterpolationMethod::Longer => {
+ let delta = *right - *left;
+ if 0. < delta && delta < 180. {
+ *left += 360.;
+ } else if -180. < delta && delta <= 0. {
+ *right += 360.;
+ }
+ },
+ // https://drafts.csswg.org/css-color/#increasing
+ HueInterpolationMethod::Increasing => {
+ if *right < *left {
+ *right += 360.;
+ }
+ },
+ // https://drafts.csswg.org/css-color/#decreasing
+ HueInterpolationMethod::Decreasing => {
+ if *left < *right {
+ *left += 360.;
+ }
+ },
+ HueInterpolationMethod::Specified => unreachable!("Handled above"),
+ }
+}
+
+fn interpolate_hue(
+ mut left: f32,
+ left_weight: f32,
+ mut right: f32,
+ right_weight: f32,
+ hue_interpolation: HueInterpolationMethod,
+) -> f32 {
+ adjust_hue(&mut left, &mut right, hue_interpolation);
+ left * left_weight + right * right_weight
+}
+
+struct InterpolatedAlpha {
+ /// The adjusted left alpha value.
+ left: f32,
+ /// The adjusted right alpha value.
+ right: f32,
+ /// The interpolated alpha value.
+ interpolated: f32,
+ /// Whether the alpha component should be `none`.
+ is_none: bool,
+}
+
+fn interpolate_alpha(
+ left: f32,
+ left_weight: f32,
+ right: f32,
+ right_weight: f32,
+ outcome: ComponentMixOutcome,
+) -> InterpolatedAlpha {
+ // <https://drafts.csswg.org/css-color-4/#interpolation-missing>
+ let mut result = match outcome {
+ ComponentMixOutcome::Mix => {
+ let interpolated = left * left_weight + right * right_weight;
+ InterpolatedAlpha {
+ left,
+ right,
+ interpolated,
+ is_none: false,
+ }
+ },
+ ComponentMixOutcome::UseLeft => InterpolatedAlpha {
+ left,
+ right: left,
+ interpolated: left,
+ is_none: false,
+ },
+ ComponentMixOutcome::UseRight => InterpolatedAlpha {
+ left: right,
+ right,
+ interpolated: right,
+ is_none: false,
+ },
+ ComponentMixOutcome::None => InterpolatedAlpha {
+ left: 1.0,
+ right: 1.0,
+ interpolated: 0.0,
+ is_none: true,
+ },
+ };
+
+ // Clip all alpha values to [0.0..1.0].
+ result.left = result.left.clamp(0.0, 1.0);
+ result.right = result.right.clamp(0.0, 1.0);
+ result.interpolated = result.interpolated.clamp(0.0, 1.0);
+
+ result
+}
+
+fn interpolate_premultiplied(
+ left: &[f32; 4],
+ left_weight: f32,
+ right: &[f32; 4],
+ right_weight: f32,
+ hue_index: Option<usize>,
+ hue_interpolation: HueInterpolationMethod,
+ outcomes: &[ComponentMixOutcome; 4],
+) -> ([f32; 4], ColorFlags) {
+ let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]);
+ let mut flags = if alpha.is_none {
+ ColorFlags::ALPHA_IS_NONE
+ } else {
+ ColorFlags::empty()
+ };
+
+ let mut result = [0.; 4];
+
+ for i in 0..3 {
+ match outcomes[i] {
+ ComponentMixOutcome::Mix => {
+ let is_hue = hue_index == Some(i);
+ result[i] = if is_hue {
+ normalize_hue(interpolate_hue(
+ left[i],
+ left_weight,
+ right[i],
+ right_weight,
+ hue_interpolation,
+ ))
+ } else {
+ let interpolated = interpolate_premultiplied_component(
+ left[i],
+ left_weight,
+ alpha.left,
+ right[i],
+ right_weight,
+ alpha.right,
+ );
+
+ if alpha.interpolated == 0.0 {
+ interpolated
+ } else {
+ interpolated / alpha.interpolated
+ }
+ };
+ },
+ ComponentMixOutcome::UseLeft => result[i] = left[i],
+ ComponentMixOutcome::UseRight => result[i] = right[i],
+ ComponentMixOutcome::None => {
+ result[i] = 0.0;
+ match i {
+ 0 => flags.insert(ColorFlags::C0_IS_NONE),
+ 1 => flags.insert(ColorFlags::C1_IS_NONE),
+ 2 => flags.insert(ColorFlags::C2_IS_NONE),
+ _ => unreachable!(),
+ }
+ },
+ }
+ }
+ result[3] = alpha.interpolated;
+
+ (result, flags)
+}
diff --git a/servo/components/style/color/mod.rs b/servo/components/style/color/mod.rs
new file mode 100644
index 0000000000..797a1cb00f
--- /dev/null
+++ b/servo/components/style/color/mod.rs
@@ -0,0 +1,613 @@
+/* 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/. */
+
+//! Color support functions.
+
+/// cbindgen:ignore
+pub mod convert;
+pub mod mix;
+pub mod parsing;
+
+use cssparser::color::PredefinedColorSpace;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// The 3 components that make up a color. (Does not include the alpha component)
+#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+#[repr(C)]
+pub struct ColorComponents(pub f32, pub f32, pub f32);
+
+impl ColorComponents {
+ /// Apply a function to each of the 3 components of the color.
+ #[must_use]
+ pub fn map(self, f: impl Fn(f32) -> f32) -> Self {
+ Self(f(self.0), f(self.1), f(self.2))
+ }
+}
+
+impl std::ops::Mul for ColorComponents {
+ type Output = Self;
+
+ fn mul(self, rhs: Self) -> Self::Output {
+ Self(self.0 * rhs.0, self.1 * rhs.1, self.2 * rhs.2)
+ }
+}
+
+impl std::ops::Div for ColorComponents {
+ type Output = Self;
+
+ fn div(self, rhs: Self) -> Self::Output {
+ Self(self.0 / rhs.0, self.1 / rhs.1, self.2 / rhs.2)
+ }
+}
+
+/// A color space representation in the CSS specification.
+///
+/// https://drafts.csswg.org/css-color-4/#typedef-color-space
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ColorSpace {
+ /// A color specified in the sRGB color space with either the rgb/rgba(..)
+ /// functions or the newer color(srgb ..) function. If the color(..)
+ /// function is used, the AS_COLOR_FUNCTION flag will be set. Examples:
+ /// "color(srgb 0.691 0.139 0.259)", "rgb(176, 35, 66)"
+ Srgb = 0,
+ /// A color specified in the Hsl notation in the sRGB color space, e.g.
+ /// "hsl(289.18 93.136% 65.531%)"
+ /// https://drafts.csswg.org/css-color-4/#the-hsl-notation
+ Hsl,
+ /// A color specified in the Hwb notation in the sRGB color space, e.g.
+ /// "hwb(740deg 20% 30%)"
+ /// https://drafts.csswg.org/css-color-4/#the-hwb-notation
+ Hwb,
+ /// A color specified in the Lab color format, e.g.
+ /// "lab(29.2345% 39.3825 20.0664)".
+ /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors
+ Lab,
+ /// A color specified in the Lch color format, e.g.
+ /// "lch(29.2345% 44.2 27)".
+ /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors
+ Lch,
+ /// A color specified in the Oklab color format, e.g.
+ /// "oklab(40.101% 0.1147 0.0453)".
+ /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors
+ Oklab,
+ /// A color specified in the Oklch color format, e.g.
+ /// "oklch(40.101% 0.12332 21.555)".
+ /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors
+ Oklch,
+ /// A color specified with the color(..) function and the "srgb-linear"
+ /// color space, e.g. "color(srgb-linear 0.435 0.017 0.055)".
+ SrgbLinear,
+ /// A color specified with the color(..) function and the "display-p3"
+ /// color space, e.g. "color(display-p3 0.84 0.19 0.72)".
+ DisplayP3,
+ /// A color specified with the color(..) function and the "a98-rgb" color
+ /// space, e.g. "color(a98-rgb 0.44091 0.49971 0.37408)".
+ A98Rgb,
+ /// A color specified with the color(..) function and the "prophoto-rgb"
+ /// color space, e.g. "color(prophoto-rgb 0.36589 0.41717 0.31333)".
+ ProphotoRgb,
+ /// A color specified with the color(..) function and the "rec2020" color
+ /// space, e.g. "color(rec2020 0.42210 0.47580 0.35605)".
+ Rec2020,
+ /// A color specified with the color(..) function and the "xyz-d50" color
+ /// space, e.g. "color(xyz-d50 0.2005 0.14089 0.4472)".
+ XyzD50,
+ /// A color specified with the color(..) function and the "xyz-d65" or "xyz"
+ /// color space, e.g. "color(xyz-d65 0.21661 0.14602 0.59452)".
+ /// NOTE: https://drafts.csswg.org/css-color-4/#resolving-color-function-values
+ /// specifies that `xyz` is an alias for the `xyz-d65` color space.
+ #[parse(aliases = "xyz")]
+ XyzD65,
+}
+
+impl ColorSpace {
+ /// Returns whether this is a `<rectangular-color-space>`.
+ #[inline]
+ pub fn is_rectangular(&self) -> bool {
+ !self.is_polar()
+ }
+
+ /// Returns whether this is a `<polar-color-space>`.
+ #[inline]
+ pub fn is_polar(&self) -> bool {
+ matches!(self, Self::Hsl | Self::Hwb | Self::Lch | Self::Oklch)
+ }
+
+ /// Returns true if the color has RGB or XYZ components.
+ #[inline]
+ pub fn is_rgb_or_xyz_like(&self) -> bool {
+ match self {
+ Self::Srgb |
+ Self::SrgbLinear |
+ Self::DisplayP3 |
+ Self::A98Rgb |
+ Self::ProphotoRgb |
+ Self::Rec2020 |
+ Self::XyzD50 |
+ Self::XyzD65 => true,
+ _ => false,
+ }
+ }
+
+ /// Returns an index of the hue component in the color space, otherwise
+ /// `None`.
+ #[inline]
+ pub fn hue_index(&self) -> Option<usize> {
+ match self {
+ Self::Hsl | Self::Hwb => Some(0),
+ Self::Lch | Self::Oklch => Some(2),
+
+ _ => {
+ debug_assert!(!self.is_polar());
+ None
+ },
+ }
+ }
+}
+
+/// Flags used when serializing colors.
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
+#[repr(C)]
+pub struct ColorFlags(u8);
+bitflags! {
+ impl ColorFlags : u8 {
+ /// Whether the 1st color component is `none`.
+ const C0_IS_NONE = 1 << 0;
+ /// Whether the 2nd color component is `none`.
+ const C1_IS_NONE = 1 << 1;
+ /// Whether the 3rd color component is `none`.
+ const C2_IS_NONE = 1 << 2;
+ /// Whether the alpha component is `none`.
+ const ALPHA_IS_NONE = 1 << 3;
+ /// Marks that this color is in the legacy color format. This flag is
+ /// only valid for the `Srgb` color space.
+ const IS_LEGACY_SRGB = 1 << 4;
+ }
+}
+
+/// An absolutely specified color, using either rgb(), rgba(), lab(), lch(),
+/// oklab(), oklch() or color().
+#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+#[repr(C)]
+pub struct AbsoluteColor {
+ /// The 3 components that make up colors in any color space.
+ pub components: ColorComponents,
+ /// The alpha component of the color.
+ pub alpha: f32,
+ /// The current color space that the components represent.
+ pub color_space: ColorSpace,
+ /// Extra flags used durring serialization of this color.
+ pub flags: ColorFlags,
+}
+
+/// Given an [`AbsoluteColor`], return the 4 float components as the type given,
+/// e.g.:
+///
+/// ```rust
+/// let srgb = AbsoluteColor::new(ColorSpace::Srgb, 1.0, 0.0, 0.0, 0.0);
+/// let floats = color_components_as!(&srgb, [f32; 4]); // [1.0, 0.0, 0.0, 0.0]
+/// ```
+macro_rules! color_components_as {
+ ($c:expr, $t:ty) => {{
+ // This macro is not an inline function, because we can't use the
+ // generic type ($t) in a constant expression as per:
+ // https://github.com/rust-lang/rust/issues/76560
+ const_assert_eq!(std::mem::size_of::<$t>(), std::mem::size_of::<[f32; 4]>());
+ const_assert_eq!(std::mem::align_of::<$t>(), std::mem::align_of::<[f32; 4]>());
+ const_assert!(std::mem::size_of::<AbsoluteColor>() >= std::mem::size_of::<$t>());
+ const_assert_eq!(
+ std::mem::align_of::<AbsoluteColor>(),
+ std::mem::align_of::<$t>()
+ );
+
+ std::mem::transmute::<&ColorComponents, &$t>(&$c.components)
+ }};
+}
+
+/// Holds details about each component passed into creating a new [`AbsoluteColor`].
+pub struct ComponentDetails {
+ value: f32,
+ is_none: bool,
+}
+
+impl From<f32> for ComponentDetails {
+ fn from(value: f32) -> Self {
+ Self {
+ value,
+ is_none: false,
+ }
+ }
+}
+
+impl From<u8> for ComponentDetails {
+ fn from(value: u8) -> Self {
+ Self {
+ value: value as f32 / 255.0,
+ is_none: false,
+ }
+ }
+}
+
+impl From<Option<f32>> for ComponentDetails {
+ fn from(value: Option<f32>) -> Self {
+ if let Some(value) = value {
+ Self {
+ value,
+ is_none: false,
+ }
+ } else {
+ Self {
+ value: 0.0,
+ is_none: true,
+ }
+ }
+ }
+}
+
+impl AbsoluteColor {
+ /// A fully transparent color in the legacy syntax.
+ pub const TRANSPARENT_BLACK: Self = Self {
+ components: ColorComponents(0.0, 0.0, 0.0),
+ alpha: 0.0,
+ color_space: ColorSpace::Srgb,
+ flags: ColorFlags::IS_LEGACY_SRGB,
+ };
+
+ /// An opaque black color in the legacy syntax.
+ pub const BLACK: Self = Self {
+ components: ColorComponents(0.0, 0.0, 0.0),
+ alpha: 1.0,
+ color_space: ColorSpace::Srgb,
+ flags: ColorFlags::IS_LEGACY_SRGB,
+ };
+
+ /// An opaque white color in the legacy syntax.
+ pub const WHITE: Self = Self {
+ components: ColorComponents(1.0, 1.0, 1.0),
+ alpha: 1.0,
+ color_space: ColorSpace::Srgb,
+ flags: ColorFlags::IS_LEGACY_SRGB,
+ };
+
+ /// Create a new [`AbsoluteColor`] with the given [`ColorSpace`] and
+ /// components.
+ pub fn new(
+ color_space: ColorSpace,
+ c1: impl Into<ComponentDetails>,
+ c2: impl Into<ComponentDetails>,
+ c3: impl Into<ComponentDetails>,
+ alpha: impl Into<ComponentDetails>,
+ ) -> Self {
+ let mut flags = ColorFlags::empty();
+
+ macro_rules! cd {
+ ($c:expr,$flag:expr) => {{
+ let component_details = $c.into();
+ if component_details.is_none {
+ flags |= $flag;
+ }
+ component_details.value
+ }};
+ }
+
+ let mut components = ColorComponents(
+ cd!(c1, ColorFlags::C0_IS_NONE),
+ cd!(c2, ColorFlags::C1_IS_NONE),
+ cd!(c3, ColorFlags::C2_IS_NONE),
+ );
+
+ let alpha = cd!(alpha, ColorFlags::ALPHA_IS_NONE);
+
+ // Lightness for Lab and Lch is clamped to [0..100].
+ if matches!(color_space, ColorSpace::Lab | ColorSpace::Lch) {
+ components.0 = components.0.clamp(0.0, 100.0);
+ }
+
+ // Lightness for Oklab and Oklch is clamped to [0..1].
+ if matches!(color_space, ColorSpace::Oklab | ColorSpace::Oklch) {
+ components.0 = components.0.clamp(0.0, 1.0);
+ }
+
+ // Chroma must not be less than 0.
+ if matches!(color_space, ColorSpace::Lch | ColorSpace::Oklch) {
+ components.1 = components.1.max(0.0);
+ }
+
+ // Alpha is always clamped to [0..1].
+ let alpha = alpha.clamp(0.0, 1.0);
+
+ Self {
+ components,
+ alpha,
+ color_space,
+ flags,
+ }
+ }
+
+ /// Convert this color into the sRGB color space and set it to the legacy
+ /// syntax.
+ #[inline]
+ #[must_use]
+ pub fn into_srgb_legacy(self) -> Self {
+ let mut result = if !matches!(self.color_space, ColorSpace::Srgb) {
+ self.to_color_space(ColorSpace::Srgb)
+ } else {
+ self
+ };
+
+ // Explicitly set the flags to IS_LEGACY_SRGB only to clear out the
+ // *_IS_NONE flags, because the legacy syntax doesn't allow "none".
+ result.flags = ColorFlags::IS_LEGACY_SRGB;
+
+ result
+ }
+
+ /// Create a new [`AbsoluteColor`] from rgba legacy syntax values in the sRGB color space.
+ pub fn srgb_legacy(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
+ let mut result = Self::new(ColorSpace::Srgb, red, green, blue, alpha);
+ result.flags = ColorFlags::IS_LEGACY_SRGB;
+ result
+ }
+
+ /// Return all the components of the color in an array. (Includes alpha)
+ #[inline]
+ pub fn raw_components(&self) -> &[f32; 4] {
+ unsafe { color_components_as!(self, [f32; 4]) }
+ }
+
+ /// Returns true if this color is in the legacy color syntax.
+ #[inline]
+ pub fn is_legacy_syntax(&self) -> bool {
+ // rgb(), rgba(), hsl(), hsla(), hwb(), hwba()
+ match self.color_space {
+ ColorSpace::Srgb => self.flags.contains(ColorFlags::IS_LEGACY_SRGB),
+ ColorSpace::Hsl | ColorSpace::Hwb => true,
+ _ => false,
+ }
+ }
+
+ /// Returns true if this color is fully transparent.
+ #[inline]
+ pub fn is_transparent(&self) -> bool {
+ self.flags.contains(ColorFlags::ALPHA_IS_NONE) || self.alpha == 0.0
+ }
+
+ /// Return an optional first component.
+ #[inline]
+ pub fn c0(&self) -> Option<f32> {
+ if self.flags.contains(ColorFlags::C0_IS_NONE) {
+ None
+ } else {
+ Some(self.components.0)
+ }
+ }
+
+ /// Return an optional second component.
+ #[inline]
+ pub fn c1(&self) -> Option<f32> {
+ if self.flags.contains(ColorFlags::C1_IS_NONE) {
+ None
+ } else {
+ Some(self.components.1)
+ }
+ }
+
+ /// Return an optional second component.
+ #[inline]
+ pub fn c2(&self) -> Option<f32> {
+ if self.flags.contains(ColorFlags::C2_IS_NONE) {
+ None
+ } else {
+ Some(self.components.2)
+ }
+ }
+
+ /// Return an optional alpha component.
+ #[inline]
+ pub fn alpha(&self) -> Option<f32> {
+ if self.flags.contains(ColorFlags::ALPHA_IS_NONE) {
+ None
+ } else {
+ Some(self.alpha)
+ }
+ }
+
+ /// Convert this color to the specified color space.
+ pub fn to_color_space(&self, color_space: ColorSpace) -> Self {
+ use ColorSpace::*;
+
+ if self.color_space == color_space {
+ return self.clone();
+ }
+
+ // Conversion functions doesn't handle NAN component values, so they are
+ // converted to 0.0. They do however need to know if a component is
+ // missing, so we use NAN as the marker for that.
+ macro_rules! missing_to_nan {
+ ($c:expr) => {{
+ if let Some(v) = $c {
+ crate::values::normalize(v)
+ } else {
+ f32::NAN
+ }
+ }};
+ }
+
+ let components = ColorComponents(
+ missing_to_nan!(self.c0()),
+ missing_to_nan!(self.c1()),
+ missing_to_nan!(self.c2()),
+ );
+
+ let result = match (self.color_space, color_space) {
+ // We have simplified conversions that do not need to convert to XYZ
+ // first. This improves performance, because it skips at least 2
+ // matrix multiplications and reduces float rounding errors.
+ (Srgb, Hsl) => convert::rgb_to_hsl(&components),
+ (Srgb, Hwb) => convert::rgb_to_hwb(&components),
+ (Hsl, Srgb) => convert::hsl_to_rgb(&components),
+ (Hwb, Srgb) => convert::hwb_to_rgb(&components),
+ (Lab, Lch) | (Oklab, Oklch) => convert::orthogonal_to_polar(&components),
+ (Lch, Lab) | (Oklch, Oklab) => convert::polar_to_orthogonal(&components),
+
+ // All other conversions need to convert to XYZ first.
+ _ => {
+ let (xyz, white_point) = match self.color_space {
+ Lab => convert::to_xyz::<convert::Lab>(&components),
+ Lch => convert::to_xyz::<convert::Lch>(&components),
+ Oklab => convert::to_xyz::<convert::Oklab>(&components),
+ Oklch => convert::to_xyz::<convert::Oklch>(&components),
+ Srgb => convert::to_xyz::<convert::Srgb>(&components),
+ Hsl => convert::to_xyz::<convert::Hsl>(&components),
+ Hwb => convert::to_xyz::<convert::Hwb>(&components),
+ SrgbLinear => convert::to_xyz::<convert::SrgbLinear>(&components),
+ DisplayP3 => convert::to_xyz::<convert::DisplayP3>(&components),
+ A98Rgb => convert::to_xyz::<convert::A98Rgb>(&components),
+ ProphotoRgb => convert::to_xyz::<convert::ProphotoRgb>(&components),
+ Rec2020 => convert::to_xyz::<convert::Rec2020>(&components),
+ XyzD50 => convert::to_xyz::<convert::XyzD50>(&components),
+ XyzD65 => convert::to_xyz::<convert::XyzD65>(&components),
+ };
+
+ match color_space {
+ Lab => convert::from_xyz::<convert::Lab>(&xyz, white_point),
+ Lch => convert::from_xyz::<convert::Lch>(&xyz, white_point),
+ Oklab => convert::from_xyz::<convert::Oklab>(&xyz, white_point),
+ Oklch => convert::from_xyz::<convert::Oklch>(&xyz, white_point),
+ Srgb => convert::from_xyz::<convert::Srgb>(&xyz, white_point),
+ Hsl => convert::from_xyz::<convert::Hsl>(&xyz, white_point),
+ Hwb => convert::from_xyz::<convert::Hwb>(&xyz, white_point),
+ SrgbLinear => convert::from_xyz::<convert::SrgbLinear>(&xyz, white_point),
+ DisplayP3 => convert::from_xyz::<convert::DisplayP3>(&xyz, white_point),
+ A98Rgb => convert::from_xyz::<convert::A98Rgb>(&xyz, white_point),
+ ProphotoRgb => convert::from_xyz::<convert::ProphotoRgb>(&xyz, white_point),
+ Rec2020 => convert::from_xyz::<convert::Rec2020>(&xyz, white_point),
+ XyzD50 => convert::from_xyz::<convert::XyzD50>(&xyz, white_point),
+ XyzD65 => convert::from_xyz::<convert::XyzD65>(&xyz, white_point),
+ }
+ },
+ };
+
+ // A NAN value coming from a conversion function means the the component
+ // is missing, so we convert it to None.
+ macro_rules! nan_to_missing {
+ ($v:expr) => {{
+ if $v.is_nan() {
+ None
+ } else {
+ Some($v)
+ }
+ }};
+ }
+
+ Self::new(
+ color_space,
+ nan_to_missing!(result.0),
+ nan_to_missing!(result.1),
+ nan_to_missing!(result.2),
+ self.alpha(),
+ )
+ }
+}
+
+impl From<PredefinedColorSpace> for ColorSpace {
+ fn from(value: PredefinedColorSpace) -> Self {
+ match value {
+ PredefinedColorSpace::Srgb => ColorSpace::Srgb,
+ PredefinedColorSpace::SrgbLinear => ColorSpace::SrgbLinear,
+ PredefinedColorSpace::DisplayP3 => ColorSpace::DisplayP3,
+ PredefinedColorSpace::A98Rgb => ColorSpace::A98Rgb,
+ PredefinedColorSpace::ProphotoRgb => ColorSpace::ProphotoRgb,
+ PredefinedColorSpace::Rec2020 => ColorSpace::Rec2020,
+ PredefinedColorSpace::XyzD50 => ColorSpace::XyzD50,
+ PredefinedColorSpace::XyzD65 => ColorSpace::XyzD65,
+ }
+ }
+}
+
+impl ToCss for AbsoluteColor {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match self.color_space {
+ ColorSpace::Srgb if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) => {
+ // The "none" keyword is not supported in the rgb/rgba legacy syntax.
+ cssparser::ToCss::to_css(
+ &parsing::RgbaLegacy::from_floats(
+ self.components.0,
+ self.components.1,
+ self.components.2,
+ self.alpha,
+ ),
+ dest,
+ )
+ },
+ ColorSpace::Hsl | ColorSpace::Hwb => self.into_srgb_legacy().to_css(dest),
+ ColorSpace::Lab => cssparser::ToCss::to_css(
+ &parsing::Lab::new(self.c0(), self.c1(), self.c2(), self.alpha()),
+ dest,
+ ),
+ ColorSpace::Lch => cssparser::ToCss::to_css(
+ &parsing::Lch::new(self.c0(), self.c1(), self.c2(), self.alpha()),
+ dest,
+ ),
+ ColorSpace::Oklab => cssparser::ToCss::to_css(
+ &parsing::Oklab::new(self.c0(), self.c1(), self.c2(), self.alpha()),
+ dest,
+ ),
+ ColorSpace::Oklch => cssparser::ToCss::to_css(
+ &parsing::Oklch::new(self.c0(), self.c1(), self.c2(), self.alpha()),
+ dest,
+ ),
+ _ => {
+ let color_space = match self.color_space {
+ ColorSpace::Srgb => {
+ debug_assert!(
+ !self.flags.contains(ColorFlags::IS_LEGACY_SRGB),
+ "legacy srgb is not a color function"
+ );
+ PredefinedColorSpace::Srgb
+ },
+ ColorSpace::SrgbLinear => PredefinedColorSpace::SrgbLinear,
+ ColorSpace::DisplayP3 => PredefinedColorSpace::DisplayP3,
+ ColorSpace::A98Rgb => PredefinedColorSpace::A98Rgb,
+ ColorSpace::ProphotoRgb => PredefinedColorSpace::ProphotoRgb,
+ ColorSpace::Rec2020 => PredefinedColorSpace::Rec2020,
+ ColorSpace::XyzD50 => PredefinedColorSpace::XyzD50,
+ ColorSpace::XyzD65 => PredefinedColorSpace::XyzD65,
+
+ _ => {
+ unreachable!("other color spaces do not support color() syntax")
+ },
+ };
+
+ let color_function = parsing::ColorFunction {
+ color_space,
+ c1: self.c0(),
+ c2: self.c1(),
+ c3: self.c2(),
+ alpha: self.alpha(),
+ };
+ let color = parsing::Color::ColorFunction(color_function);
+ cssparser::ToCss::to_css(&color, dest)
+ },
+ }
+ }
+}
diff --git a/servo/components/style/color/parsing.rs b/servo/components/style/color/parsing.rs
new file mode 100644
index 0000000000..f60b44c5b6
--- /dev/null
+++ b/servo/components/style/color/parsing.rs
@@ -0,0 +1,1246 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#![deny(missing_docs)]
+
+//! Fairly complete css-color implementation.
+//! Relative colors, color-mix, system colors, and other such things require better calc() support
+//! and integration.
+
+use super::{
+ convert::{hsl_to_rgb, hwb_to_rgb, normalize_hue},
+ ColorComponents,
+};
+use crate::values::normalize;
+use cssparser::color::{
+ clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, serialize_color_alpha,
+ PredefinedColorSpace, OPAQUE,
+};
+use cssparser::{match_ignore_ascii_case, CowRcStr, ParseError, Parser, ToCss, Token};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::f32::consts::PI;
+use std::fmt;
+use std::str::FromStr;
+
+/// Return the named color with the given name.
+///
+/// Matching is case-insensitive in the ASCII range.
+/// CSS escaping (if relevant) should be resolved before calling this function.
+/// (For example, the value of an `Ident` token is fine.)
+#[inline]
+pub fn parse_color_keyword<Output>(ident: &str) -> Result<Output, ()>
+where
+ Output: FromParsedColor,
+{
+ Ok(match_ignore_ascii_case! { ident ,
+ "transparent" => Output::from_rgba(0, 0, 0, 0.0),
+ "currentcolor" => Output::from_current_color(),
+ _ => {
+ let (r, g, b) = cssparser::color::parse_named_color(ident)?;
+ Output::from_rgba(r, g, b, OPAQUE)
+ }
+ })
+}
+
+/// Parse a CSS color using the specified [`ColorParser`] and return a new color
+/// value on success.
+pub fn parse_color_with<'i, 't, P>(
+ color_parser: &P,
+ input: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let location = input.current_source_location();
+ let token = input.next()?;
+ match *token {
+ Token::Hash(ref value) | Token::IDHash(ref value) => {
+ parse_hash_color(value.as_bytes()).map(|(r, g, b, a)| P::Output::from_rgba(r, g, b, a))
+ },
+ Token::Ident(ref value) => parse_color_keyword(value),
+ Token::Function(ref name) => {
+ let name = name.clone();
+ return input.parse_nested_block(|arguments| {
+ parse_color_function(color_parser, name, arguments)
+ });
+ },
+ _ => Err(()),
+ }
+ .map_err(|()| location.new_unexpected_token_error(token.clone()))
+}
+
+/// Parse one of the color functions: rgba(), lab(), color(), etc.
+#[inline]
+fn parse_color_function<'i, 't, P>(
+ color_parser: &P,
+ name: CowRcStr<'i>,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let color = match_ignore_ascii_case! { &name,
+ "rgb" | "rgba" => parse_rgb(color_parser, arguments),
+
+ "hsl" | "hsla" => parse_hsl(color_parser, arguments),
+
+ "hwb" => parse_hwb(color_parser, arguments),
+
+ // for L: 0% = 0.0, 100% = 100.0
+ // for a and b: -100% = -125, 100% = 125
+ "lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab),
+
+ // for L: 0% = 0.0, 100% = 100.0
+ // for C: 0% = 0, 100% = 150
+ "lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch),
+
+ // for L: 0% = 0.0, 100% = 1.0
+ // for a and b: -100% = -0.4, 100% = 0.4
+ "oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab),
+
+ // for L: 0% = 0.0, 100% = 1.0
+ // for C: 0% = 0.0 100% = 0.4
+ "oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklch),
+
+ "color" => parse_color_with_color_space(color_parser, arguments),
+
+ _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
+ }?;
+
+ arguments.expect_exhausted()?;
+
+ Ok(color)
+}
+
+/// Parse the alpha component by itself from either number or percentage,
+/// clipping the result to [0.0..1.0].
+#[inline]
+fn parse_alpha_component<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<f32, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ // Percent reference range for alpha: 0% = 0.0, 100% = 1.0
+ let alpha = color_parser
+ .parse_number_or_percentage(arguments)?
+ .to_number(1.0);
+ Ok(normalize(alpha).clamp(0.0, OPAQUE))
+}
+
+fn parse_legacy_alpha<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<f32, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ Ok(if !arguments.is_exhausted() {
+ arguments.expect_comma()?;
+ parse_alpha_component(color_parser, arguments)?
+ } else {
+ OPAQUE
+ })
+}
+
+fn parse_modern_alpha<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<Option<f32>, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ if !arguments.is_exhausted() {
+ arguments.expect_delim('/')?;
+ parse_none_or(arguments, |p| parse_alpha_component(color_parser, p))
+ } else {
+ Ok(Some(OPAQUE))
+ }
+}
+
+#[inline]
+fn parse_rgb<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?;
+
+ // If the first component is not "none" and is followed by a comma, then we
+ // are parsing the legacy syntax.
+ let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok();
+
+ let (red, green, blue, alpha) = if is_legacy_syntax {
+ let (red, green, blue) = match maybe_red.unwrap() {
+ NumberOrPercentage::Number { value } => {
+ let red = clamp_floor_256_f32(value);
+ let green = clamp_floor_256_f32(color_parser.parse_number(arguments)?);
+ arguments.expect_comma()?;
+ let blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?);
+ (red, green, blue)
+ },
+ NumberOrPercentage::Percentage { unit_value } => {
+ let red = clamp_unit_f32(unit_value);
+ let green = clamp_unit_f32(color_parser.parse_percentage(arguments)?);
+ arguments.expect_comma()?;
+ let blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?);
+ (red, green, blue)
+ },
+ };
+
+ let alpha = parse_legacy_alpha(color_parser, arguments)?;
+
+ (red, green, blue, alpha)
+ } else {
+ #[inline]
+ fn get_component_value(c: Option<NumberOrPercentage>) -> u8 {
+ c.map(|c| match c {
+ NumberOrPercentage::Number { value } => clamp_floor_256_f32(value),
+ NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value),
+ })
+ .unwrap_or(0)
+ }
+
+ let red = get_component_value(maybe_red);
+
+ let green = get_component_value(parse_none_or(arguments, |p| {
+ color_parser.parse_number_or_percentage(p)
+ })?);
+
+ let blue = get_component_value(parse_none_or(arguments, |p| {
+ color_parser.parse_number_or_percentage(p)
+ })?);
+
+ let alpha = parse_modern_alpha(color_parser, arguments)?.unwrap_or(0.0);
+
+ (red, green, blue, alpha)
+ };
+
+ Ok(P::Output::from_rgba(red, green, blue, alpha))
+}
+
+/// Parses hsl syntax.
+///
+/// <https://drafts.csswg.org/css-color/#the-hsl-notation>
+#[inline]
+fn parse_hsl<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ // Percent reference range for S and L: 0% = 0.0, 100% = 100.0
+ const LIGHTNESS_RANGE: f32 = 100.0;
+ const SATURATION_RANGE: f32 = 100.0;
+
+ let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?;
+
+ // If the hue is not "none" and is followed by a comma, then we are parsing
+ // the legacy syntax.
+ let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok();
+
+ let saturation: Option<f32>;
+ let lightness: Option<f32>;
+
+ let alpha = if is_legacy_syntax {
+ saturation = Some(color_parser.parse_percentage(arguments)? * SATURATION_RANGE);
+ arguments.expect_comma()?;
+ lightness = Some(color_parser.parse_percentage(arguments)? * LIGHTNESS_RANGE);
+ Some(parse_legacy_alpha(color_parser, arguments)?)
+ } else {
+ saturation = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?
+ .map(|v| v.to_number(SATURATION_RANGE));
+ lightness = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?
+ .map(|v| v.to_number(LIGHTNESS_RANGE));
+ parse_modern_alpha(color_parser, arguments)?
+ };
+
+ let hue = maybe_hue.map(|h| normalize_hue(h.degrees()));
+ let saturation = saturation.map(|s| s.clamp(0.0, SATURATION_RANGE));
+ let lightness = lightness.map(|l| l.clamp(0.0, LIGHTNESS_RANGE));
+
+ Ok(P::Output::from_hsl(hue, saturation, lightness, alpha))
+}
+
+/// Parses hwb syntax.
+///
+/// <https://drafts.csswg.org/css-color/#the-hbw-notation>
+#[inline]
+fn parse_hwb<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ // Percent reference range for W and B: 0% = 0.0, 100% = 100.0
+ const WHITENESS_RANGE: f32 = 100.0;
+ const BLACKNESS_RANGE: f32 = 100.0;
+
+ let (hue, whiteness, blackness, alpha) = parse_components(
+ color_parser,
+ arguments,
+ P::parse_angle_or_number,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ )?;
+
+ let hue = hue.map(|h| normalize_hue(h.degrees()));
+ let whiteness = whiteness.map(|w| w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE));
+ let blackness = blackness.map(|b| b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE));
+
+ Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha))
+}
+
+type IntoColorFn<Output> =
+ fn(l: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) -> Output;
+
+#[inline]
+fn parse_lab_like<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+ lightness_range: f32,
+ a_b_range: f32,
+ into_color: IntoColorFn<P::Output>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let (lightness, a, b, alpha) = parse_components(
+ color_parser,
+ arguments,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ )?;
+
+ let lightness = lightness.map(|l| l.to_number(lightness_range));
+ let a = a.map(|a| a.to_number(a_b_range));
+ let b = b.map(|b| b.to_number(a_b_range));
+
+ Ok(into_color(lightness, a, b, alpha))
+}
+
+#[inline]
+fn parse_lch_like<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+ lightness_range: f32,
+ chroma_range: f32,
+ into_color: IntoColorFn<P::Output>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let (lightness, chroma, hue, alpha) = parse_components(
+ color_parser,
+ arguments,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ P::parse_angle_or_number,
+ )?;
+
+ let lightness = lightness.map(|l| l.to_number(lightness_range));
+ let chroma = chroma.map(|c| c.to_number(chroma_range));
+ let hue = hue.map(|h| normalize_hue(h.degrees()));
+
+ Ok(into_color(lightness, chroma, hue, alpha))
+}
+
+/// Parse the color() function.
+#[inline]
+fn parse_color_with_color_space<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let color_space = {
+ let location = arguments.current_source_location();
+
+ let ident = arguments.expect_ident()?;
+ PredefinedColorSpace::from_str(ident)
+ .map_err(|_| location.new_unexpected_token_error(Token::Ident(ident.clone())))?
+ };
+
+ let (c1, c2, c3, alpha) = parse_components(
+ color_parser,
+ arguments,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ )?;
+
+ let c1 = c1.map(|c| c.to_number(1.0));
+ let c2 = c2.map(|c| c.to_number(1.0));
+ let c3 = c3.map(|c| c.to_number(1.0));
+
+ Ok(P::Output::from_color_function(
+ color_space,
+ c1,
+ c2,
+ c3,
+ alpha,
+ ))
+}
+
+type ComponentParseResult<'i, R1, R2, R3, Error> =
+ Result<(Option<R1>, Option<R2>, Option<R3>, Option<f32>), ParseError<'i, Error>>;
+
+/// Parse the color components and alpha with the modern [color-4] syntax.
+pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>(
+ color_parser: &P,
+ input: &mut Parser<'i, 't>,
+ f1: F1,
+ f2: F2,
+ f3: F3,
+) -> ComponentParseResult<'i, R1, R2, R3, P::Error>
+where
+ P: ColorParser<'i>,
+ F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R1, ParseError<'i, P::Error>>,
+ F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R2, ParseError<'i, P::Error>>,
+ F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R3, ParseError<'i, P::Error>>,
+{
+ let r1 = parse_none_or(input, |p| f1(color_parser, p))?;
+ let r2 = parse_none_or(input, |p| f2(color_parser, p))?;
+ let r3 = parse_none_or(input, |p| f3(color_parser, p))?;
+
+ let alpha = parse_modern_alpha(color_parser, input)?;
+
+ Ok((r1, r2, r3, alpha))
+}
+
+fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result<Option<T>, E>
+where
+ F: FnOnce(&mut Parser<'i, 't>) -> Result<T, E>,
+{
+ match input.try_parse(|p| p.expect_ident_matching("none")) {
+ Ok(_) => Ok(None),
+ Err(_) => Ok(Some(thing(input)?)),
+ }
+}
+
+/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and
+/// floating point values.
+struct ModernComponent<'a>(&'a Option<f32>);
+
+impl<'a> ToCss for ModernComponent<'a> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ if let Some(value) = self.0 {
+ if value.is_finite() {
+ value.to_css(dest)
+ } else if value.is_nan() {
+ dest.write_str("calc(NaN)")
+ } else {
+ debug_assert!(value.is_infinite());
+ if value.is_sign_negative() {
+ dest.write_str("calc(-infinity)")
+ } else {
+ dest.write_str("calc(infinity)")
+ }
+ }
+ } else {
+ dest.write_str("none")
+ }
+ }
+}
+
+/// A color with red, green, blue, and alpha components, in a byte each.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct RgbaLegacy {
+ /// The red component.
+ pub red: u8,
+ /// The green component.
+ pub green: u8,
+ /// The blue component.
+ pub blue: u8,
+ /// The alpha component.
+ pub alpha: f32,
+}
+
+impl RgbaLegacy {
+ /// Constructs a new RGBA value from float components. It expects the red,
+ /// green, blue and alpha channels in that order, and all values will be
+ /// clamped to the 0.0 ... 1.0 range.
+ #[inline]
+ pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
+ Self::new(
+ clamp_unit_f32(red),
+ clamp_unit_f32(green),
+ clamp_unit_f32(blue),
+ alpha.clamp(0.0, OPAQUE),
+ )
+ }
+
+ /// Same thing, but with `u8` values instead of floats in the 0 to 1 range.
+ #[inline]
+ pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
+ Self {
+ red,
+ green,
+ blue,
+ alpha,
+ }
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for RgbaLegacy {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.red, self.green, self.blue, self.alpha).serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for RgbaLegacy {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (r, g, b, a) = Deserialize::deserialize(deserializer)?;
+ Ok(RgbaLegacy::new(r, g, b, a))
+ }
+}
+
+impl ToCss for RgbaLegacy {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let has_alpha = self.alpha != OPAQUE;
+
+ dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?;
+ self.red.to_css(dest)?;
+ dest.write_str(", ")?;
+ self.green.to_css(dest)?;
+ dest.write_str(", ")?;
+ self.blue.to_css(dest)?;
+
+ // Legacy syntax does not allow none components.
+ serialize_color_alpha(dest, Some(self.alpha), true)?;
+
+ dest.write_char(')')
+ }
+}
+
+/// Color specified by hue, saturation and lightness components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Hsl {
+ /// The hue component.
+ pub hue: Option<f32>,
+ /// The saturation component.
+ pub saturation: Option<f32>,
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+impl Hsl {
+ /// Construct a new HSL color from it's components.
+ pub fn new(
+ hue: Option<f32>,
+ saturation: Option<f32>,
+ lightness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ hue,
+ saturation,
+ lightness,
+ alpha,
+ }
+ }
+}
+
+impl ToCss for Hsl {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ // HSL serializes to RGB, so we have to convert it.
+ let ColorComponents(red, green, blue) = hsl_to_rgb(&ColorComponents(
+ self.hue.unwrap_or(0.0) / 360.0,
+ self.saturation.unwrap_or(0.0),
+ self.lightness.unwrap_or(0.0),
+ ));
+
+ RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Hsl {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for Hsl {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?;
+ Ok(Self::new(lightness, a, b, alpha))
+ }
+}
+
+/// Color specified by hue, whiteness and blackness components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Hwb {
+ /// The hue component.
+ pub hue: Option<f32>,
+ /// The whiteness component.
+ pub whiteness: Option<f32>,
+ /// The blackness component.
+ pub blackness: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+impl Hwb {
+ /// Construct a new HWB color from it's components.
+ pub fn new(
+ hue: Option<f32>,
+ whiteness: Option<f32>,
+ blackness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ hue,
+ whiteness,
+ blackness,
+ alpha,
+ }
+ }
+}
+
+impl ToCss for Hwb {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ // HWB serializes to RGB, so we have to convert it.
+ let ColorComponents(red, green, blue) = hwb_to_rgb(&ColorComponents(
+ self.hue.unwrap_or(0.0) / 360.0,
+ self.whiteness.unwrap_or(0.0),
+ self.blackness.unwrap_or(0.0),
+ ));
+
+ RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Hwb {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for Hwb {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?;
+ Ok(Self::new(lightness, whiteness, blackness, alpha))
+ }
+}
+
+// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro,
+// because it causes cbindgen to ignore them.
+
+/// Color specified by lightness, a- and b-axis components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Lab {
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The a-axis component.
+ pub a: Option<f32>,
+ /// The b-axis component.
+ pub b: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+/// Color specified by lightness, a- and b-axis components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Oklab {
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The a-axis component.
+ pub a: Option<f32>,
+ /// The b-axis component.
+ pub b: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+macro_rules! impl_lab_like {
+ ($cls:ident, $fname:literal) => {
+ impl $cls {
+ /// Construct a new Lab color format with lightness, a, b and alpha components.
+ pub fn new(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ lightness,
+ a,
+ b,
+ alpha,
+ }
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ impl Serialize for $cls {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.lightness, self.a, self.b, self.alpha).serialize(serializer)
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ impl<'de> Deserialize<'de> for $cls {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?;
+ Ok(Self::new(lightness, a, b, alpha))
+ }
+ }
+
+ impl ToCss for $cls {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str($fname)?;
+ dest.write_str("(")?;
+ ModernComponent(&self.lightness).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.a).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.b).to_css(dest)?;
+ serialize_color_alpha(dest, self.alpha, false)?;
+ dest.write_char(')')
+ }
+ }
+ };
+}
+
+impl_lab_like!(Lab, "lab");
+impl_lab_like!(Oklab, "oklab");
+
+// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro,
+// because it causes cbindgen to ignore them.
+
+/// Color specified by lightness, chroma and hue components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Lch {
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The chroma component.
+ pub chroma: Option<f32>,
+ /// The hue component.
+ pub hue: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+/// Color specified by lightness, chroma and hue components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Oklch {
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The chroma component.
+ pub chroma: Option<f32>,
+ /// The hue component.
+ pub hue: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+macro_rules! impl_lch_like {
+ ($cls:ident, $fname:literal) => {
+ impl $cls {
+ /// Construct a new color with lightness, chroma and hue components.
+ pub fn new(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ lightness,
+ chroma,
+ hue,
+ alpha,
+ }
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ impl Serialize for $cls {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.lightness, self.chroma, self.hue, self.alpha).serialize(serializer)
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ impl<'de> Deserialize<'de> for $cls {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (lightness, chroma, hue, alpha) = Deserialize::deserialize(deserializer)?;
+ Ok(Self::new(lightness, chroma, hue, alpha))
+ }
+ }
+
+ impl ToCss for $cls {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str($fname)?;
+ dest.write_str("(")?;
+ ModernComponent(&self.lightness).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.chroma).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.hue).to_css(dest)?;
+ serialize_color_alpha(dest, self.alpha, false)?;
+ dest.write_char(')')
+ }
+ }
+ };
+}
+
+impl_lch_like!(Lch, "lch");
+impl_lch_like!(Oklch, "oklch");
+
+/// A color specified by the color() function.
+/// <https://drafts.csswg.org/css-color-4/#color-function>
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct ColorFunction {
+ /// The color space for this color.
+ pub color_space: PredefinedColorSpace,
+ /// The first component of the color. Either red or x.
+ pub c1: Option<f32>,
+ /// The second component of the color. Either green or y.
+ pub c2: Option<f32>,
+ /// The third component of the color. Either blue or z.
+ pub c3: Option<f32>,
+ /// The alpha component of the color.
+ pub alpha: Option<f32>,
+}
+
+impl ColorFunction {
+ /// Construct a new color function definition with the given color space and
+ /// color components.
+ pub fn new(
+ color_space: PredefinedColorSpace,
+ c1: Option<f32>,
+ c2: Option<f32>,
+ c3: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ color_space,
+ c1,
+ c2,
+ c3,
+ alpha,
+ }
+ }
+}
+
+impl ToCss for ColorFunction {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str("color(")?;
+ self.color_space.to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.c1).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.c2).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.c3).to_css(dest)?;
+
+ serialize_color_alpha(dest, self.alpha, false)?;
+
+ dest.write_char(')')
+ }
+}
+
+/// Describes one of the value <color> values according to the CSS
+/// specification.
+///
+/// Most components are `Option<_>`, so when the value is `None`, that component
+/// serializes to the "none" keyword.
+///
+/// <https://drafts.csswg.org/css-color-4/#color-type>
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub enum Color {
+ /// The 'currentcolor' keyword.
+ CurrentColor,
+ /// Specify sRGB colors directly by their red/green/blue/alpha chanels.
+ Rgba(RgbaLegacy),
+ /// Specifies a color in sRGB using hue, saturation and lightness components.
+ Hsl(Hsl),
+ /// Specifies a color in sRGB using hue, whiteness and blackness components.
+ Hwb(Hwb),
+ /// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue
+ /// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB
+ /// rectangular coordinate model.
+ Lab(Lab),
+ /// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the
+ /// CIE LCH cylindrical coordinate model.
+ Lch(Lch),
+ /// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue
+ /// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab
+ /// rectangular coordinate model.
+ Oklab(Oklab),
+ /// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using
+ /// the OKLCH cylindrical coordinate model.
+ Oklch(Oklch),
+ /// Specifies a color in a predefined color space.
+ ColorFunction(ColorFunction),
+}
+
+impl ToCss for Color {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ Color::CurrentColor => dest.write_str("currentcolor"),
+ Color::Rgba(rgba) => rgba.to_css(dest),
+ Color::Hsl(hsl) => hsl.to_css(dest),
+ Color::Hwb(hwb) => hwb.to_css(dest),
+ Color::Lab(lab) => lab.to_css(dest),
+ Color::Lch(lch) => lch.to_css(dest),
+ Color::Oklab(lab) => lab.to_css(dest),
+ Color::Oklch(lch) => lch.to_css(dest),
+ Color::ColorFunction(color_function) => color_function.to_css(dest),
+ }
+ }
+}
+
+/// Either a number or a percentage.
+pub enum NumberOrPercentage {
+ /// `<number>`.
+ Number {
+ /// The numeric value parsed, as a float.
+ value: f32,
+ },
+ /// `<percentage>`
+ Percentage {
+ /// The value as a float, divided by 100 so that the nominal range is
+ /// 0.0 to 1.0.
+ unit_value: f32,
+ },
+}
+
+impl NumberOrPercentage {
+ /// Return the value as a number. Percentages will be adjusted to the range
+ /// [0..percent_basis].
+ pub fn to_number(&self, percentage_basis: f32) -> f32 {
+ match *self {
+ Self::Number { value } => value,
+ Self::Percentage { unit_value } => unit_value * percentage_basis,
+ }
+ }
+}
+
+/// Either an angle or a number.
+pub enum AngleOrNumber {
+ /// `<number>`.
+ Number {
+ /// The numeric value parsed, as a float.
+ value: f32,
+ },
+ /// `<angle>`
+ Angle {
+ /// The value as a number of degrees.
+ degrees: f32,
+ },
+}
+
+impl AngleOrNumber {
+ /// Return the angle in degrees. `AngleOrNumber::Number` is returned as
+ /// degrees, because it is the canonical unit.
+ pub fn degrees(&self) -> f32 {
+ match *self {
+ AngleOrNumber::Number { value } => value,
+ AngleOrNumber::Angle { degrees } => degrees,
+ }
+ }
+}
+
+/// A trait that can be used to hook into how `cssparser` parses color
+/// components, with the intention of implementing more complicated behavior.
+///
+/// For example, this is used by Servo to support calc() in color.
+pub trait ColorParser<'i> {
+ /// The type that the parser will construct on a successful parse.
+ type Output: FromParsedColor;
+
+ /// A custom error type that can be returned from the parsing functions.
+ type Error: 'i;
+
+ /// Parse an `<angle>` or `<number>`.
+ ///
+ /// Returns the result in degrees.
+ fn parse_angle_or_number<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> {
+ let location = input.current_source_location();
+ Ok(match *input.next()? {
+ Token::Number { value, .. } => AngleOrNumber::Number { value },
+ Token::Dimension {
+ value: v, ref unit, ..
+ } => {
+ let degrees = match_ignore_ascii_case! { unit,
+ "deg" => v,
+ "grad" => v * 360. / 400.,
+ "rad" => v * 360. / (2. * PI),
+ "turn" => v * 360.,
+ _ => {
+ return Err(location.new_unexpected_token_error(Token::Ident(unit.clone())))
+ }
+ };
+
+ AngleOrNumber::Angle { degrees }
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ })
+ }
+
+ /// Parse a `<percentage>` value.
+ ///
+ /// Returns the result in a number from 0.0 to 1.0.
+ fn parse_percentage<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<f32, ParseError<'i, Self::Error>> {
+ input.expect_percentage().map_err(From::from)
+ }
+
+ /// Parse a `<number>` value.
+ fn parse_number<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<f32, ParseError<'i, Self::Error>> {
+ input.expect_number().map_err(From::from)
+ }
+
+ /// Parse a `<number>` value or a `<percentage>` value.
+ fn parse_number_or_percentage<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> {
+ let location = input.current_source_location();
+ Ok(match *input.next()? {
+ Token::Number { value, .. } => NumberOrPercentage::Number { value },
+ Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ })
+ }
+}
+
+/// Default implementation of a [`ColorParser`]
+pub struct DefaultColorParser;
+
+impl<'i> ColorParser<'i> for DefaultColorParser {
+ type Output = Color;
+ type Error = ();
+}
+
+impl Color {
+ /// Parse a <color> value, per CSS Color Module Level 3.
+ ///
+ /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet.
+ pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i, ()>> {
+ parse_color_with(&DefaultColorParser, input)
+ }
+}
+
+/// This trait is used by the [`ColorParser`] to construct colors of any type.
+pub trait FromParsedColor {
+ /// Construct a new color from the CSS `currentcolor` keyword.
+ fn from_current_color() -> Self;
+
+ /// Construct a new color from red, green, blue and alpha components.
+ fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self;
+
+ /// Construct a new color from hue, saturation, lightness and alpha components.
+ fn from_hsl(
+ hue: Option<f32>,
+ saturation: Option<f32>,
+ lightness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color from hue, blackness, whiteness and alpha components.
+ fn from_hwb(
+ hue: Option<f32>,
+ whiteness: Option<f32>,
+ blackness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color from the `lab` notation.
+ fn from_lab(lightness: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>)
+ -> Self;
+
+ /// Construct a new color from the `lch` notation.
+ fn from_lch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color from the `oklab` notation.
+ fn from_oklab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color from the `oklch` notation.
+ fn from_oklch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color with a predefined color space.
+ fn from_color_function(
+ color_space: PredefinedColorSpace,
+ c1: Option<f32>,
+ c2: Option<f32>,
+ c3: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+}
+
+impl FromParsedColor for Color {
+ #[inline]
+ fn from_current_color() -> Self {
+ Color::CurrentColor
+ }
+
+ #[inline]
+ fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
+ Color::Rgba(RgbaLegacy::new(red, green, blue, alpha))
+ }
+
+ fn from_hsl(
+ hue: Option<f32>,
+ saturation: Option<f32>,
+ lightness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Hsl(Hsl::new(hue, saturation, lightness, alpha))
+ }
+
+ fn from_hwb(
+ hue: Option<f32>,
+ blackness: Option<f32>,
+ whiteness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha))
+ }
+
+ #[inline]
+ fn from_lab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Lab(Lab::new(lightness, a, b, alpha))
+ }
+
+ #[inline]
+ fn from_lch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Lch(Lch::new(lightness, chroma, hue, alpha))
+ }
+
+ #[inline]
+ fn from_oklab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Oklab(Oklab::new(lightness, a, b, alpha))
+ }
+
+ #[inline]
+ fn from_oklch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Oklch(Oklch::new(lightness, chroma, hue, alpha))
+ }
+
+ #[inline]
+ fn from_color_function(
+ color_space: PredefinedColorSpace,
+ c1: Option<f32>,
+ c2: Option<f32>,
+ c3: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha))
+ }
+}
diff --git a/servo/components/style/context.rs b/servo/components/style/context.rs
new file mode 100644
index 0000000000..a2c020475b
--- /dev/null
+++ b/servo/components/style/context.rs
@@ -0,0 +1,698 @@
+/* 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/. */
+
+//! The context within which style is calculated.
+
+#[cfg(feature = "servo")]
+use crate::animation::DocumentAnimationSet;
+use crate::bloom::StyleBloom;
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::data::{EagerPseudoStyles, ElementData};
+use crate::dom::{SendElement, TElement};
+#[cfg(feature = "gecko")]
+use crate::gecko_bindings::structs;
+use crate::parallel::{STACK_SAFETY_MARGIN_KB, STYLE_THREAD_STACK_SIZE_KB};
+use crate::properties::ComputedValues;
+#[cfg(feature = "servo")]
+use crate::properties::PropertyId;
+use crate::rule_cache::RuleCache;
+use crate::rule_tree::StrongRuleNode;
+use crate::selector_parser::{SnapshotMap, EAGER_PSEUDO_COUNT};
+use crate::shared_lock::StylesheetGuards;
+use crate::sharing::StyleSharingCache;
+use crate::stylist::Stylist;
+use crate::thread_state::{self, ThreadState};
+use crate::traversal::DomTraversal;
+use crate::traversal_flags::TraversalFlags;
+use app_units::Au;
+use euclid::default::Size2D;
+use euclid::Scale;
+#[cfg(feature = "servo")]
+use fxhash::FxHashMap;
+use selectors::context::SelectorCaches;
+#[cfg(feature = "gecko")]
+use servo_arc::Arc;
+#[cfg(feature = "servo")]
+use servo_atoms::Atom;
+use std::fmt;
+use std::ops;
+use style_traits::CSSPixel;
+use style_traits::DevicePixel;
+#[cfg(feature = "servo")]
+use style_traits::SpeculativePainter;
+use time;
+
+pub use selectors::matching::QuirksMode;
+
+/// A global options structure for the style system. We use this instead of
+/// opts to abstract across Gecko and Servo.
+#[derive(Clone)]
+pub struct StyleSystemOptions {
+ /// Whether the style sharing cache is disabled.
+ pub disable_style_sharing_cache: bool,
+ /// Whether we should dump statistics about the style system.
+ pub dump_style_statistics: bool,
+ /// The minimum number of elements that must be traversed to trigger a dump
+ /// of style statistics.
+ pub style_statistics_threshold: usize,
+}
+
+#[cfg(feature = "gecko")]
+fn get_env_bool(name: &str) -> bool {
+ use std::env;
+ match env::var(name) {
+ Ok(s) => !s.is_empty(),
+ Err(_) => false,
+ }
+}
+
+const DEFAULT_STATISTICS_THRESHOLD: usize = 50;
+
+#[cfg(feature = "gecko")]
+fn get_env_usize(name: &str) -> Option<usize> {
+ use std::env;
+ env::var(name).ok().map(|s| {
+ s.parse::<usize>()
+ .expect("Couldn't parse environmental variable as usize")
+ })
+}
+
+/// A global variable holding the state of
+/// `StyleSystemOptions::default().disable_style_sharing_cache`.
+/// See [#22854](https://github.com/servo/servo/issues/22854).
+#[cfg(feature = "servo")]
+pub static DEFAULT_DISABLE_STYLE_SHARING_CACHE: std::sync::atomic::AtomicBool =
+ std::sync::atomic::AtomicBool::new(false);
+
+/// A global variable holding the state of
+/// `StyleSystemOptions::default().dump_style_statistics`.
+/// See [#22854](https://github.com/servo/servo/issues/22854).
+#[cfg(feature = "servo")]
+pub static DEFAULT_DUMP_STYLE_STATISTICS: std::sync::atomic::AtomicBool =
+ std::sync::atomic::AtomicBool::new(false);
+
+impl Default for StyleSystemOptions {
+ #[cfg(feature = "servo")]
+ fn default() -> Self {
+ use std::sync::atomic::Ordering;
+
+ StyleSystemOptions {
+ disable_style_sharing_cache: DEFAULT_DISABLE_STYLE_SHARING_CACHE
+ .load(Ordering::Relaxed),
+ dump_style_statistics: DEFAULT_DUMP_STYLE_STATISTICS.load(Ordering::Relaxed),
+ style_statistics_threshold: DEFAULT_STATISTICS_THRESHOLD,
+ }
+ }
+
+ #[cfg(feature = "gecko")]
+ fn default() -> Self {
+ StyleSystemOptions {
+ disable_style_sharing_cache: get_env_bool("DISABLE_STYLE_SHARING_CACHE"),
+ dump_style_statistics: get_env_bool("DUMP_STYLE_STATISTICS"),
+ style_statistics_threshold: get_env_usize("STYLE_STATISTICS_THRESHOLD")
+ .unwrap_or(DEFAULT_STATISTICS_THRESHOLD),
+ }
+ }
+}
+
+/// A shared style context.
+///
+/// There's exactly one of these during a given restyle traversal, and it's
+/// shared among the worker threads.
+pub struct SharedStyleContext<'a> {
+ /// The CSS selector stylist.
+ pub stylist: &'a Stylist,
+
+ /// Whether visited styles are enabled.
+ ///
+ /// They may be disabled when Gecko's pref layout.css.visited_links_enabled
+ /// is false, or when in private browsing mode.
+ pub visited_styles_enabled: bool,
+
+ /// Configuration options.
+ pub options: StyleSystemOptions,
+
+ /// Guards for pre-acquired locks
+ pub guards: StylesheetGuards<'a>,
+
+ /// The current time for transitions and animations. This is needed to ensure
+ /// a consistent sampling time and also to adjust the time for testing.
+ pub current_time_for_animations: f64,
+
+ /// Flags controlling how we traverse the tree.
+ pub traversal_flags: TraversalFlags,
+
+ /// A map with our snapshots in order to handle restyle hints.
+ pub snapshot_map: &'a SnapshotMap,
+
+ /// The state of all animations for our styled elements.
+ #[cfg(feature = "servo")]
+ pub animations: DocumentAnimationSet,
+
+ /// Paint worklets
+ #[cfg(feature = "servo")]
+ pub registered_speculative_painters: &'a dyn RegisteredSpeculativePainters,
+}
+
+impl<'a> SharedStyleContext<'a> {
+ /// Return a suitable viewport size in order to be used for viewport units.
+ pub fn viewport_size(&self) -> Size2D<Au> {
+ self.stylist.device().au_viewport_size()
+ }
+
+ /// The device pixel ratio
+ pub fn device_pixel_ratio(&self) -> Scale<f32, CSSPixel, DevicePixel> {
+ self.stylist.device().device_pixel_ratio()
+ }
+
+ /// The quirks mode of the document.
+ pub fn quirks_mode(&self) -> QuirksMode {
+ self.stylist.quirks_mode()
+ }
+}
+
+/// The structure holds various intermediate inputs that are eventually used by
+/// by the cascade.
+///
+/// The matching and cascading process stores them in this format temporarily
+/// within the `CurrentElementInfo`. At the end of the cascade, they are folded
+/// down into the main `ComputedValues` to reduce memory usage per element while
+/// still remaining accessible.
+#[derive(Clone, Debug, Default)]
+pub struct CascadeInputs {
+ /// The rule node representing the ordered list of rules matched for this
+ /// node.
+ pub rules: Option<StrongRuleNode>,
+
+ /// The rule node representing the ordered list of rules matched for this
+ /// node if visited, only computed if there's a relevant link for this
+ /// element. A element's "relevant link" is the element being matched if it
+ /// is a link or the nearest ancestor link.
+ pub visited_rules: Option<StrongRuleNode>,
+
+ /// The set of flags from container queries that we need for invalidation.
+ pub flags: ComputedValueFlags,
+}
+
+impl CascadeInputs {
+ /// Construct inputs from previous cascade results, if any.
+ pub fn new_from_style(style: &ComputedValues) -> Self {
+ Self {
+ rules: style.rules.clone(),
+ visited_rules: style.visited_style().and_then(|v| v.rules.clone()),
+ flags: style.flags.for_cascade_inputs(),
+ }
+ }
+}
+
+/// A list of cascade inputs for eagerly-cascaded pseudo-elements.
+/// The list is stored inline.
+#[derive(Debug)]
+pub struct EagerPseudoCascadeInputs(Option<[Option<CascadeInputs>; EAGER_PSEUDO_COUNT]>);
+
+// Manually implement `Clone` here because the derived impl of `Clone` for
+// array types assumes the value inside is `Copy`.
+impl Clone for EagerPseudoCascadeInputs {
+ fn clone(&self) -> Self {
+ if self.0.is_none() {
+ return EagerPseudoCascadeInputs(None);
+ }
+ let self_inputs = self.0.as_ref().unwrap();
+ let mut inputs: [Option<CascadeInputs>; EAGER_PSEUDO_COUNT] = Default::default();
+ for i in 0..EAGER_PSEUDO_COUNT {
+ inputs[i] = self_inputs[i].clone();
+ }
+ EagerPseudoCascadeInputs(Some(inputs))
+ }
+}
+
+impl EagerPseudoCascadeInputs {
+ /// Construct inputs from previous cascade results, if any.
+ fn new_from_style(styles: &EagerPseudoStyles) -> Self {
+ EagerPseudoCascadeInputs(styles.as_optional_array().map(|styles| {
+ let mut inputs: [Option<CascadeInputs>; EAGER_PSEUDO_COUNT] = Default::default();
+ for i in 0..EAGER_PSEUDO_COUNT {
+ inputs[i] = styles[i].as_ref().map(|s| CascadeInputs::new_from_style(s));
+ }
+ inputs
+ }))
+ }
+
+ /// Returns the list of rules, if they exist.
+ pub fn into_array(self) -> Option<[Option<CascadeInputs>; EAGER_PSEUDO_COUNT]> {
+ self.0
+ }
+}
+
+/// The cascade inputs associated with a node, including those for any
+/// pseudo-elements.
+///
+/// The matching and cascading process stores them in this format temporarily
+/// within the `CurrentElementInfo`. At the end of the cascade, they are folded
+/// down into the main `ComputedValues` to reduce memory usage per element while
+/// still remaining accessible.
+#[derive(Clone, Debug)]
+pub struct ElementCascadeInputs {
+ /// The element's cascade inputs.
+ pub primary: CascadeInputs,
+ /// A list of the inputs for the element's eagerly-cascaded pseudo-elements.
+ pub pseudos: EagerPseudoCascadeInputs,
+}
+
+impl ElementCascadeInputs {
+ /// Construct inputs from previous cascade results, if any.
+ #[inline]
+ pub fn new_from_element_data(data: &ElementData) -> Self {
+ debug_assert!(data.has_styles());
+ ElementCascadeInputs {
+ primary: CascadeInputs::new_from_style(data.styles.primary()),
+ pseudos: EagerPseudoCascadeInputs::new_from_style(&data.styles.pseudos),
+ }
+ }
+}
+
+/// Statistics gathered during the traversal. We gather statistics on each
+/// thread and then combine them after the threads join via the Add
+/// implementation below.
+#[derive(AddAssign, Clone, Default)]
+pub struct PerThreadTraversalStatistics {
+ /// The total number of elements traversed.
+ pub elements_traversed: u32,
+ /// The number of elements where has_styles() went from false to true.
+ pub elements_styled: u32,
+ /// The number of elements for which we performed selector matching.
+ pub elements_matched: u32,
+ /// The number of cache hits from the StyleSharingCache.
+ pub styles_shared: u32,
+ /// The number of styles reused via rule node comparison from the
+ /// StyleSharingCache.
+ pub styles_reused: u32,
+}
+
+/// Statistics gathered during the traversal plus some information from
+/// other sources including stylist.
+#[derive(Default)]
+pub struct TraversalStatistics {
+ /// Aggregated statistics gathered during the traversal.
+ pub aggregated: PerThreadTraversalStatistics,
+ /// The number of selectors in the stylist.
+ pub selectors: u32,
+ /// The number of revalidation selectors.
+ pub revalidation_selectors: u32,
+ /// The number of state/attr dependencies in the dependency set.
+ pub dependency_selectors: u32,
+ /// The number of declarations in the stylist.
+ pub declarations: u32,
+ /// The number of times the stylist was rebuilt.
+ pub stylist_rebuilds: u32,
+ /// Time spent in the traversal, in milliseconds.
+ pub traversal_time_ms: f64,
+ /// Whether this was a parallel traversal.
+ pub is_parallel: bool,
+ /// Whether this is a "large" traversal.
+ pub is_large: bool,
+}
+
+/// Format the statistics in a way that the performance test harness understands.
+/// See https://bugzilla.mozilla.org/show_bug.cgi?id=1331856#c2
+impl fmt::Display for TraversalStatistics {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ debug_assert!(
+ self.traversal_time_ms != 0.0,
+ "should have set traversal time"
+ );
+ writeln!(f, "[PERF] perf block start")?;
+ writeln!(
+ f,
+ "[PERF],traversal,{}",
+ if self.is_parallel {
+ "parallel"
+ } else {
+ "sequential"
+ }
+ )?;
+ writeln!(
+ f,
+ "[PERF],elements_traversed,{}",
+ self.aggregated.elements_traversed
+ )?;
+ writeln!(
+ f,
+ "[PERF],elements_styled,{}",
+ self.aggregated.elements_styled
+ )?;
+ writeln!(
+ f,
+ "[PERF],elements_matched,{}",
+ self.aggregated.elements_matched
+ )?;
+ writeln!(f, "[PERF],styles_shared,{}", self.aggregated.styles_shared)?;
+ writeln!(f, "[PERF],styles_reused,{}", self.aggregated.styles_reused)?;
+ writeln!(f, "[PERF],selectors,{}", self.selectors)?;
+ writeln!(
+ f,
+ "[PERF],revalidation_selectors,{}",
+ self.revalidation_selectors
+ )?;
+ writeln!(
+ f,
+ "[PERF],dependency_selectors,{}",
+ self.dependency_selectors
+ )?;
+ writeln!(f, "[PERF],declarations,{}", self.declarations)?;
+ writeln!(f, "[PERF],stylist_rebuilds,{}", self.stylist_rebuilds)?;
+ writeln!(f, "[PERF],traversal_time_ms,{}", self.traversal_time_ms)?;
+ writeln!(f, "[PERF] perf block end")
+ }
+}
+
+impl TraversalStatistics {
+ /// Generate complete traversal statistics.
+ ///
+ /// The traversal time is computed given the start time in seconds.
+ pub fn new<E, D>(
+ aggregated: PerThreadTraversalStatistics,
+ traversal: &D,
+ parallel: bool,
+ start: f64,
+ ) -> TraversalStatistics
+ where
+ E: TElement,
+ D: DomTraversal<E>,
+ {
+ let threshold = traversal
+ .shared_context()
+ .options
+ .style_statistics_threshold;
+ let stylist = traversal.shared_context().stylist;
+ let is_large = aggregated.elements_traversed as usize >= threshold;
+ TraversalStatistics {
+ aggregated,
+ selectors: stylist.num_selectors() as u32,
+ revalidation_selectors: stylist.num_revalidation_selectors() as u32,
+ dependency_selectors: stylist.num_invalidations() as u32,
+ declarations: stylist.num_declarations() as u32,
+ stylist_rebuilds: stylist.num_rebuilds() as u32,
+ traversal_time_ms: (time::precise_time_s() - start) * 1000.0,
+ is_parallel: parallel,
+ is_large,
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+bitflags! {
+ /// Represents which tasks are performed in a SequentialTask of
+ /// UpdateAnimations which is a result of normal restyle.
+ pub struct UpdateAnimationsTasks: u8 {
+ /// Update CSS Animations.
+ const CSS_ANIMATIONS = structs::UpdateAnimationsTasks_CSSAnimations;
+ /// Update CSS Transitions.
+ const CSS_TRANSITIONS = structs::UpdateAnimationsTasks_CSSTransitions;
+ /// Update effect properties.
+ const EFFECT_PROPERTIES = structs::UpdateAnimationsTasks_EffectProperties;
+ /// Update animation cacade results for animations running on the compositor.
+ const CASCADE_RESULTS = structs::UpdateAnimationsTasks_CascadeResults;
+ /// Display property was changed from none.
+ /// Script animations keep alive on display:none elements, so we need to trigger
+ /// the second animation restyles for the script animations in the case where
+ /// the display property was changed from 'none' to others.
+ const DISPLAY_CHANGED_FROM_NONE = structs::UpdateAnimationsTasks_DisplayChangedFromNone;
+ /// Update CSS named scroll progress timelines.
+ const SCROLL_TIMELINES = structs::UpdateAnimationsTasks_ScrollTimelines;
+ /// Update CSS named view progress timelines.
+ const VIEW_TIMELINES = structs::UpdateAnimationsTasks_ViewTimelines;
+ }
+}
+
+#[cfg(feature = "gecko")]
+bitflags! {
+ /// Represents which tasks are performed in a SequentialTask as a result of
+ /// animation-only restyle.
+ pub struct PostAnimationTasks: u8 {
+ /// Display property was changed from none in animation-only restyle so
+ /// that we need to resolve styles for descendants in a subsequent
+ /// normal restyle.
+ const DISPLAY_CHANGED_FROM_NONE_FOR_SMIL = 0x01;
+ }
+}
+
+/// A task to be run in sequential mode on the parent (non-worker) thread. This
+/// is used by the style system to queue up work which is not safe to do during
+/// the parallel traversal.
+pub enum SequentialTask<E: TElement> {
+ /// Entry to avoid an unused type parameter error on servo.
+ Unused(SendElement<E>),
+
+ /// Performs one of a number of possible tasks related to updating
+ /// animations based on the |tasks| field. These include updating CSS
+ /// animations/transitions that changed as part of the non-animation style
+ /// traversal, and updating the computed effect properties.
+ #[cfg(feature = "gecko")]
+ UpdateAnimations {
+ /// The target element or pseudo-element.
+ el: SendElement<E>,
+ /// The before-change style for transitions. We use before-change style
+ /// as the initial value of its Keyframe. Required if |tasks| includes
+ /// CSSTransitions.
+ before_change_style: Option<Arc<ComputedValues>>,
+ /// The tasks which are performed in this SequentialTask.
+ tasks: UpdateAnimationsTasks,
+ },
+
+ /// Performs one of a number of possible tasks as a result of animation-only
+ /// restyle.
+ ///
+ /// Currently we do only process for resolving descendant elements that were
+ /// display:none subtree for SMIL animation.
+ #[cfg(feature = "gecko")]
+ PostAnimation {
+ /// The target element.
+ el: SendElement<E>,
+ /// The tasks which are performed in this SequentialTask.
+ tasks: PostAnimationTasks,
+ },
+}
+
+impl<E: TElement> SequentialTask<E> {
+ /// Executes this task.
+ pub fn execute(self) {
+ use self::SequentialTask::*;
+ debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT);
+ match self {
+ Unused(_) => unreachable!(),
+ #[cfg(feature = "gecko")]
+ UpdateAnimations {
+ el,
+ before_change_style,
+ tasks,
+ } => {
+ el.update_animations(before_change_style, tasks);
+ },
+ #[cfg(feature = "gecko")]
+ PostAnimation { el, tasks } => {
+ el.process_post_animation(tasks);
+ },
+ }
+ }
+
+ /// Creates a task to update various animation-related state on a given
+ /// (pseudo-)element.
+ #[cfg(feature = "gecko")]
+ pub fn update_animations(
+ el: E,
+ before_change_style: Option<Arc<ComputedValues>>,
+ tasks: UpdateAnimationsTasks,
+ ) -> Self {
+ use self::SequentialTask::*;
+ UpdateAnimations {
+ el: unsafe { SendElement::new(el) },
+ before_change_style,
+ tasks,
+ }
+ }
+
+ /// Creates a task to do post-process for a given element as a result of
+ /// animation-only restyle.
+ #[cfg(feature = "gecko")]
+ pub fn process_post_animation(el: E, tasks: PostAnimationTasks) -> Self {
+ use self::SequentialTask::*;
+ PostAnimation {
+ el: unsafe { SendElement::new(el) },
+ tasks,
+ }
+ }
+}
+
+/// A list of SequentialTasks that get executed on Drop.
+pub struct SequentialTaskList<E>(Vec<SequentialTask<E>>)
+where
+ E: TElement;
+
+impl<E> ops::Deref for SequentialTaskList<E>
+where
+ E: TElement,
+{
+ type Target = Vec<SequentialTask<E>>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<E> ops::DerefMut for SequentialTaskList<E>
+where
+ E: TElement,
+{
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl<E> Drop for SequentialTaskList<E>
+where
+ E: TElement,
+{
+ fn drop(&mut self) {
+ debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT);
+ for task in self.0.drain(..) {
+ task.execute()
+ }
+ }
+}
+
+/// A helper type for stack limit checking. This assumes that stacks grow
+/// down, which is true for all non-ancient CPU architectures.
+pub struct StackLimitChecker {
+ lower_limit: usize,
+}
+
+impl StackLimitChecker {
+ /// Create a new limit checker, for this thread, allowing further use
+ /// of up to |stack_size| bytes beyond (below) the current stack pointer.
+ #[inline(never)]
+ pub fn new(stack_size_limit: usize) -> Self {
+ StackLimitChecker {
+ lower_limit: StackLimitChecker::get_sp() - stack_size_limit,
+ }
+ }
+
+ /// Checks whether the previously stored stack limit has now been exceeded.
+ #[inline(never)]
+ pub fn limit_exceeded(&self) -> bool {
+ let curr_sp = StackLimitChecker::get_sp();
+
+ // Do some sanity-checking to ensure that our invariants hold, even in
+ // the case where we've exceeded the soft limit.
+ //
+ // The correctness of depends on the assumption that no stack wraps
+ // around the end of the address space.
+ if cfg!(debug_assertions) {
+ // Compute the actual bottom of the stack by subtracting our safety
+ // margin from our soft limit. Note that this will be slightly below
+ // the actual bottom of the stack, because there are a few initial
+ // frames on the stack before we do the measurement that computes
+ // the limit.
+ let stack_bottom = self.lower_limit - STACK_SAFETY_MARGIN_KB * 1024;
+
+ // The bottom of the stack should be below the current sp. If it
+ // isn't, that means we've either waited too long to check the limit
+ // and burned through our safety margin (in which case we probably
+ // would have segfaulted by now), or we're using a limit computed for
+ // a different thread.
+ debug_assert!(stack_bottom < curr_sp);
+
+ // Compute the distance between the current sp and the bottom of
+ // the stack, and compare it against the current stack. It should be
+ // no further from us than the total stack size. We allow some slop
+ // to handle the fact that stack_bottom is a bit further than the
+ // bottom of the stack, as discussed above.
+ let distance_to_stack_bottom = curr_sp - stack_bottom;
+ let max_allowable_distance = (STYLE_THREAD_STACK_SIZE_KB + 10) * 1024;
+ debug_assert!(distance_to_stack_bottom <= max_allowable_distance);
+ }
+
+ // The actual bounds check.
+ curr_sp <= self.lower_limit
+ }
+
+ // Technically, rustc can optimize this away, but shouldn't for now.
+ // We should fix this once black_box is stable.
+ #[inline(always)]
+ fn get_sp() -> usize {
+ let mut foo: usize = 42;
+ (&mut foo as *mut usize) as usize
+ }
+}
+
+/// A thread-local style context.
+///
+/// This context contains data that needs to be used during restyling, but is
+/// not required to be unique among worker threads, so we create one per worker
+/// thread in order to be able to mutate it without locking.
+pub struct ThreadLocalStyleContext<E: TElement> {
+ /// A cache to share style among siblings.
+ pub sharing_cache: StyleSharingCache<E>,
+ /// A cache from matched properties to elements that match those.
+ pub rule_cache: RuleCache,
+ /// The bloom filter used to fast-reject selector-matching.
+ pub bloom_filter: StyleBloom<E>,
+ /// A set of tasks to be run (on the parent thread) in sequential mode after
+ /// the rest of the styling is complete. This is useful for
+ /// infrequently-needed non-threadsafe operations.
+ ///
+ /// It's important that goes after the style sharing cache and the bloom
+ /// filter, to ensure they're dropped before we execute the tasks, which
+ /// could create another ThreadLocalStyleContext for style computation.
+ pub tasks: SequentialTaskList<E>,
+ /// Statistics about the traversal.
+ pub statistics: PerThreadTraversalStatistics,
+ /// A checker used to ensure that parallel.rs does not recurse indefinitely
+ /// even on arbitrarily deep trees. See Gecko bug 1376883.
+ pub stack_limit_checker: StackLimitChecker,
+ /// Collection of caches (And cache-likes) for speeding up expensive selector matches.
+ pub selector_caches: SelectorCaches,
+}
+
+impl<E: TElement> ThreadLocalStyleContext<E> {
+ /// Creates a new `ThreadLocalStyleContext`
+ pub fn new() -> Self {
+ ThreadLocalStyleContext {
+ sharing_cache: StyleSharingCache::new(),
+ rule_cache: RuleCache::new(),
+ bloom_filter: StyleBloom::new(),
+ tasks: SequentialTaskList(Vec::new()),
+ statistics: PerThreadTraversalStatistics::default(),
+ stack_limit_checker: StackLimitChecker::new(
+ (STYLE_THREAD_STACK_SIZE_KB - STACK_SAFETY_MARGIN_KB) * 1024,
+ ),
+ selector_caches: SelectorCaches::default(),
+ }
+ }
+}
+
+/// A `StyleContext` is just a simple container for a immutable reference to a
+/// shared style context, and a mutable reference to a local one.
+pub struct StyleContext<'a, E: TElement + 'a> {
+ /// The shared style context reference.
+ pub shared: &'a SharedStyleContext<'a>,
+ /// The thread-local style context (mutable) reference.
+ pub thread_local: &'a mut ThreadLocalStyleContext<E>,
+}
+
+/// A registered painter
+#[cfg(feature = "servo")]
+pub trait RegisteredSpeculativePainter: SpeculativePainter {
+ /// The name it was registered with
+ fn name(&self) -> Atom;
+ /// The properties it was registered with
+ fn properties(&self) -> &FxHashMap<Atom, PropertyId>;
+}
+
+/// A set of registered painters
+#[cfg(feature = "servo")]
+pub trait RegisteredSpeculativePainters: Sync {
+ /// Look up a speculative painter
+ fn get(&self, name: &Atom) -> Option<&dyn RegisteredSpeculativePainter>;
+}
diff --git a/servo/components/style/counter_style/mod.rs b/servo/components/style/counter_style/mod.rs
new file mode 100644
index 0000000000..fc7a0fb447
--- /dev/null
+++ b/servo/components/style/counter_style/mod.rs
@@ -0,0 +1,695 @@
+/* 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/. */
+
+//! The [`@counter-style`][counter-style] at-rule.
+//!
+//! [counter-style]: https://drafts.csswg.org/css-counter-styles/
+
+use crate::error_reporting::ContextualParseError;
+use crate::parser::{Parse, ParserContext};
+use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::values::specified::Integer;
+use crate::values::CustomIdent;
+use crate::Atom;
+use cssparser::{
+ AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser,
+};
+use cssparser::{CowRcStr, Parser, SourceLocation, Token};
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Write};
+use std::mem;
+use std::num::Wrapping;
+use style_traits::{Comma, CssWriter, OneOrMoreSeparated, ParseError};
+use style_traits::{StyleParseErrorKind, ToCss};
+
+/// Parse a counter style name reference.
+///
+/// This allows the reserved counter style names "decimal" and "disc".
+pub fn parse_counter_style_name<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<CustomIdent, ParseError<'i>> {
+ macro_rules! predefined {
+ ($($name: tt,)+) => {{
+ ascii_case_insensitive_phf_map! {
+ predefined -> Atom = {
+ $(
+ $name => atom!($name),
+ )+
+ }
+ }
+
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ // This effectively performs case normalization only on predefined names.
+ if let Some(lower_case) = predefined::get(&ident) {
+ Ok(CustomIdent(lower_case.clone()))
+ } else {
+ // none is always an invalid <counter-style> value.
+ CustomIdent::from_ident(location, ident, &["none"])
+ }
+ }}
+ }
+ include!("predefined.rs")
+}
+
+fn is_valid_name_definition(ident: &CustomIdent) -> bool {
+ ident.0 != atom!("decimal") &&
+ ident.0 != atom!("disc") &&
+ ident.0 != atom!("circle") &&
+ ident.0 != atom!("square") &&
+ ident.0 != atom!("disclosure-closed") &&
+ ident.0 != atom!("disclosure-open")
+}
+
+/// Parse the prelude of an @counter-style rule
+pub fn parse_counter_style_name_definition<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<CustomIdent, ParseError<'i>> {
+ parse_counter_style_name(input).and_then(|ident| {
+ if !is_valid_name_definition(&ident) {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(ident)
+ }
+ })
+}
+
+/// Parse the body (inside `{}`) of an @counter-style rule
+pub fn parse_counter_style_body<'i, 't>(
+ name: CustomIdent,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ location: SourceLocation,
+) -> Result<CounterStyleRuleData, ParseError<'i>> {
+ let start = input.current_source_location();
+ let mut rule = CounterStyleRuleData::empty(name, location);
+ {
+ let mut parser = CounterStyleRuleParser {
+ context,
+ rule: &mut rule,
+ };
+ let mut iter = RuleBodyParser::new(input, &mut parser);
+ while let Some(declaration) = iter.next() {
+ if let Err((error, slice)) = declaration {
+ let location = error.location;
+ let error = ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(
+ slice, error,
+ );
+ context.log_css_error(location, error)
+ }
+ }
+ }
+ let error = match *rule.resolved_system() {
+ ref system @ System::Cyclic |
+ ref system @ System::Fixed { .. } |
+ ref system @ System::Symbolic |
+ ref system @ System::Alphabetic |
+ ref system @ System::Numeric
+ if rule.symbols.is_none() =>
+ {
+ let system = system.to_css_string();
+ Some(ContextualParseError::InvalidCounterStyleWithoutSymbols(
+ system,
+ ))
+ },
+ ref system @ System::Alphabetic | ref system @ System::Numeric
+ if rule.symbols().unwrap().0.len() < 2 =>
+ {
+ let system = system.to_css_string();
+ Some(ContextualParseError::InvalidCounterStyleNotEnoughSymbols(
+ system,
+ ))
+ },
+ System::Additive if rule.additive_symbols.is_none() => {
+ Some(ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols)
+ },
+ System::Extends(_) if rule.symbols.is_some() => {
+ Some(ContextualParseError::InvalidCounterStyleExtendsWithSymbols)
+ },
+ System::Extends(_) if rule.additive_symbols.is_some() => {
+ Some(ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols)
+ },
+ _ => None,
+ };
+ if let Some(error) = error {
+ context.log_css_error(start, error);
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(rule)
+ }
+}
+
+struct CounterStyleRuleParser<'a, 'b: 'a> {
+ context: &'a ParserContext<'b>,
+ rule: &'a mut CounterStyleRuleData,
+}
+
+/// Default methods reject all at rules.
+impl<'a, 'b, 'i> AtRuleParser<'i> for CounterStyleRuleParser<'a, 'b> {
+ type Prelude = ();
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> QualifiedRuleParser<'i> for CounterStyleRuleParser<'a, 'b> {
+ type Prelude = ();
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>>
+ for CounterStyleRuleParser<'a, 'b>
+{
+ fn parse_qualified(&self) -> bool {
+ false
+ }
+ fn parse_declarations(&self) -> bool {
+ true
+ }
+}
+
+macro_rules! checker {
+ ($self:ident._($value:ident)) => {};
+ ($self:ident. $checker:ident($value:ident)) => {
+ if !$self.$checker(&$value) {
+ return false;
+ }
+ };
+}
+
+macro_rules! counter_style_descriptors {
+ (
+ $( #[$doc: meta] $name: tt $ident: ident / $setter: ident [$checker: tt]: $ty: ty, )+
+ ) => {
+ /// An @counter-style rule
+ #[derive(Clone, Debug, ToShmem)]
+ pub struct CounterStyleRuleData {
+ name: CustomIdent,
+ generation: Wrapping<u32>,
+ $(
+ #[$doc]
+ $ident: Option<$ty>,
+ )+
+ /// Line and column of the @counter-style rule source code.
+ pub source_location: SourceLocation,
+ }
+
+ impl CounterStyleRuleData {
+ fn empty(name: CustomIdent, source_location: SourceLocation) -> Self {
+ CounterStyleRuleData {
+ name: name,
+ generation: Wrapping(0),
+ $(
+ $ident: None,
+ )+
+ source_location,
+ }
+ }
+
+ $(
+ #[$doc]
+ pub fn $ident(&self) -> Option<&$ty> {
+ self.$ident.as_ref()
+ }
+ )+
+
+ $(
+ #[$doc]
+ pub fn $setter(&mut self, value: $ty) -> bool {
+ checker!(self.$checker(value));
+ self.$ident = Some(value);
+ self.generation += Wrapping(1);
+ true
+ }
+ )+
+ }
+
+ impl<'a, 'b, 'i> DeclarationParser<'i> for CounterStyleRuleParser<'a, 'b> {
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_value<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ match_ignore_ascii_case! { &*name,
+ $(
+ $name => {
+ // DeclarationParser also calls parse_entirely so we’d normally not
+ // need to, but in this case we do because we set the value as a side
+ // effect rather than returning it.
+ let value = input.parse_entirely(|i| Parse::parse(self.context, i))?;
+ self.rule.$ident = Some(value)
+ },
+ )*
+ _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
+ }
+ Ok(())
+ }
+ }
+
+ impl ToCssWithGuard for CounterStyleRuleData {
+ fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@counter-style ")?;
+ self.name.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str(" { ")?;
+ $(
+ if let Some(ref value) = self.$ident {
+ dest.write_str(concat!($name, ": "))?;
+ ToCss::to_css(value, &mut CssWriter::new(dest))?;
+ dest.write_str("; ")?;
+ }
+ )+
+ dest.write_char('}')
+ }
+ }
+ }
+}
+
+counter_style_descriptors! {
+ /// <https://drafts.csswg.org/css-counter-styles/#counter-style-system>
+ "system" system / set_system [check_system]: System,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#counter-style-negative>
+ "negative" negative / set_negative [_]: Negative,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#counter-style-prefix>
+ "prefix" prefix / set_prefix [_]: Symbol,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#counter-style-suffix>
+ "suffix" suffix / set_suffix [_]: Symbol,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#counter-style-range>
+ "range" range / set_range [_]: CounterRanges,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#counter-style-pad>
+ "pad" pad / set_pad [_]: Pad,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#counter-style-fallback>
+ "fallback" fallback / set_fallback [_]: Fallback,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-symbols>
+ "symbols" symbols / set_symbols [check_symbols]: Symbols,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-additive-symbols>
+ "additive-symbols" additive_symbols /
+ set_additive_symbols [check_additive_symbols]: AdditiveSymbols,
+
+ /// <https://drafts.csswg.org/css-counter-styles/#counter-style-speak-as>
+ "speak-as" speak_as / set_speak_as [_]: SpeakAs,
+}
+
+// Implements the special checkers for some setters.
+// See <https://drafts.csswg.org/css-counter-styles/#the-csscounterstylerule-interface>
+impl CounterStyleRuleData {
+ /// Check that the system is effectively not changed. Only params
+ /// of system descriptor is changeable.
+ fn check_system(&self, value: &System) -> bool {
+ mem::discriminant(self.resolved_system()) == mem::discriminant(value)
+ }
+
+ fn check_symbols(&self, value: &Symbols) -> bool {
+ match *self.resolved_system() {
+ // These two systems require at least two symbols.
+ System::Numeric | System::Alphabetic => value.0.len() >= 2,
+ // No symbols should be set for extends system.
+ System::Extends(_) => false,
+ _ => true,
+ }
+ }
+
+ fn check_additive_symbols(&self, _value: &AdditiveSymbols) -> bool {
+ match *self.resolved_system() {
+ // No additive symbols should be set for extends system.
+ System::Extends(_) => false,
+ _ => true,
+ }
+ }
+}
+
+impl CounterStyleRuleData {
+ /// Get the name of the counter style rule.
+ pub fn name(&self) -> &CustomIdent {
+ &self.name
+ }
+
+ /// Set the name of the counter style rule. Caller must ensure that
+ /// the name is valid.
+ pub fn set_name(&mut self, name: CustomIdent) {
+ debug_assert!(is_valid_name_definition(&name));
+ self.name = name;
+ }
+
+ /// Get the current generation of the counter style rule.
+ pub fn generation(&self) -> u32 {
+ self.generation.0
+ }
+
+ /// Get the system of this counter style rule, default to
+ /// `symbolic` if not specified.
+ pub fn resolved_system(&self) -> &System {
+ match self.system {
+ Some(ref system) => system,
+ None => &System::Symbolic,
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#counter-style-system>
+#[derive(Clone, Debug, ToShmem)]
+pub enum System {
+ /// 'cyclic'
+ Cyclic,
+ /// 'numeric'
+ Numeric,
+ /// 'alphabetic'
+ Alphabetic,
+ /// 'symbolic'
+ Symbolic,
+ /// 'additive'
+ Additive,
+ /// 'fixed <integer>?'
+ Fixed {
+ /// '<integer>?'
+ first_symbol_value: Option<Integer>,
+ },
+ /// 'extends <counter-style-name>'
+ Extends(CustomIdent),
+}
+
+impl Parse for System {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ try_match_ident_ignore_ascii_case! { input,
+ "cyclic" => Ok(System::Cyclic),
+ "numeric" => Ok(System::Numeric),
+ "alphabetic" => Ok(System::Alphabetic),
+ "symbolic" => Ok(System::Symbolic),
+ "additive" => Ok(System::Additive),
+ "fixed" => {
+ let first_symbol_value = input.try_parse(|i| Integer::parse(context, i)).ok();
+ Ok(System::Fixed { first_symbol_value })
+ },
+ "extends" => {
+ let other = parse_counter_style_name(input)?;
+ Ok(System::Extends(other))
+ },
+ }
+ }
+}
+
+impl ToCss for System {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ System::Cyclic => dest.write_str("cyclic"),
+ System::Numeric => dest.write_str("numeric"),
+ System::Alphabetic => dest.write_str("alphabetic"),
+ System::Symbolic => dest.write_str("symbolic"),
+ System::Additive => dest.write_str("additive"),
+ System::Fixed { first_symbol_value } => {
+ if let Some(value) = first_symbol_value {
+ dest.write_str("fixed ")?;
+ value.to_css(dest)
+ } else {
+ dest.write_str("fixed")
+ }
+ },
+ System::Extends(ref other) => {
+ dest.write_str("extends ")?;
+ other.to_css(dest)
+ },
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#typedef-symbol>
+#[derive(
+ Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem,
+)]
+#[repr(u8)]
+pub enum Symbol {
+ /// <string>
+ String(crate::OwnedStr),
+ /// <custom-ident>
+ Ident(CustomIdent),
+ // Not implemented:
+ // /// <image>
+ // Image(Image),
+}
+
+impl Parse for Symbol {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::QuotedString(ref s) => Ok(Symbol::String(s.as_ref().to_owned().into())),
+ Token::Ident(ref s) => Ok(Symbol::Ident(CustomIdent::from_ident(location, s, &[])?)),
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+}
+
+impl Symbol {
+ /// Returns whether this symbol is allowed in symbols() function.
+ pub fn is_allowed_in_symbols(&self) -> bool {
+ match self {
+ // Identifier is not allowed.
+ &Symbol::Ident(_) => false,
+ _ => true,
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#counter-style-negative>
+#[derive(Clone, Debug, ToCss, ToShmem)]
+pub struct Negative(pub Symbol, pub Option<Symbol>);
+
+impl Parse for Negative {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Negative(
+ Symbol::parse(context, input)?,
+ input.try_parse(|input| Symbol::parse(context, input)).ok(),
+ ))
+ }
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#counter-style-range>
+#[derive(Clone, Debug, ToCss, ToShmem)]
+pub struct CounterRange {
+ /// The start of the range.
+ pub start: CounterBound,
+ /// The end of the range.
+ pub end: CounterBound,
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#counter-style-range>
+///
+/// Empty represents 'auto'
+#[derive(Clone, Debug, ToCss, ToShmem)]
+#[css(comma)]
+pub struct CounterRanges(#[css(iterable, if_empty = "auto")] pub crate::OwnedSlice<CounterRange>);
+
+/// A bound found in `CounterRanges`.
+#[derive(Clone, Copy, Debug, ToCss, ToShmem)]
+pub enum CounterBound {
+ /// An integer bound.
+ Integer(Integer),
+ /// The infinite bound.
+ Infinite,
+}
+
+impl Parse for CounterRanges {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("auto"))
+ .is_ok()
+ {
+ return Ok(CounterRanges(Default::default()));
+ }
+
+ let ranges = input.parse_comma_separated(|input| {
+ let start = parse_bound(context, input)?;
+ let end = parse_bound(context, input)?;
+ if let (CounterBound::Integer(start), CounterBound::Integer(end)) = (start, end) {
+ if start > end {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+ Ok(CounterRange { start, end })
+ })?;
+
+ Ok(CounterRanges(ranges.into()))
+ }
+}
+
+fn parse_bound<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<CounterBound, ParseError<'i>> {
+ if let Ok(integer) = input.try_parse(|input| Integer::parse(context, input)) {
+ return Ok(CounterBound::Integer(integer));
+ }
+ input.expect_ident_matching("infinite")?;
+ Ok(CounterBound::Infinite)
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#counter-style-pad>
+#[derive(Clone, Debug, ToCss, ToShmem)]
+pub struct Pad(pub Integer, pub Symbol);
+
+impl Parse for Pad {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let pad_with = input.try_parse(|input| Symbol::parse(context, input));
+ let min_length = Integer::parse_non_negative(context, input)?;
+ let pad_with = pad_with.or_else(|_| Symbol::parse(context, input))?;
+ Ok(Pad(min_length, pad_with))
+ }
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#counter-style-fallback>
+#[derive(Clone, Debug, ToCss, ToShmem)]
+pub struct Fallback(pub CustomIdent);
+
+impl Parse for Fallback {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Fallback(parse_counter_style_name(input)?))
+ }
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-symbols>
+#[derive(
+ Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem,
+)]
+#[repr(C)]
+pub struct Symbols(#[css(iterable)] pub crate::OwnedSlice<Symbol>);
+
+impl Parse for Symbols {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut symbols = Vec::new();
+ while let Ok(s) = input.try_parse(|input| Symbol::parse(context, input)) {
+ symbols.push(s);
+ }
+ if symbols.is_empty() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(Symbols(symbols.into()))
+ }
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-additive-symbols>
+#[derive(Clone, Debug, ToCss, ToShmem)]
+#[css(comma)]
+pub struct AdditiveSymbols(#[css(iterable)] pub crate::OwnedSlice<AdditiveTuple>);
+
+impl Parse for AdditiveSymbols {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let tuples = Vec::<AdditiveTuple>::parse(context, input)?;
+ // FIXME maybe? https://github.com/w3c/csswg-drafts/issues/1220
+ if tuples
+ .windows(2)
+ .any(|window| window[0].weight <= window[1].weight)
+ {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(AdditiveSymbols(tuples.into()))
+ }
+}
+
+/// <integer> && <symbol>
+#[derive(Clone, Debug, ToCss, ToShmem)]
+pub struct AdditiveTuple {
+ /// <integer>
+ pub weight: Integer,
+ /// <symbol>
+ pub symbol: Symbol,
+}
+
+impl OneOrMoreSeparated for AdditiveTuple {
+ type S = Comma;
+}
+
+impl Parse for AdditiveTuple {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let symbol = input.try_parse(|input| Symbol::parse(context, input));
+ let weight = Integer::parse_non_negative(context, input)?;
+ let symbol = symbol.or_else(|_| Symbol::parse(context, input))?;
+ Ok(Self { weight, symbol })
+ }
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#counter-style-speak-as>
+#[derive(Clone, Debug, ToCss, ToShmem)]
+pub enum SpeakAs {
+ /// auto
+ Auto,
+ /// bullets
+ Bullets,
+ /// numbers
+ Numbers,
+ /// words
+ Words,
+ // /// spell-out, not supported, see bug 1024178
+ // SpellOut,
+ /// <counter-style-name>
+ Other(CustomIdent),
+}
+
+impl Parse for SpeakAs {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut is_spell_out = false;
+ let result = input.try_parse(|input| {
+ let ident = input.expect_ident().map_err(|_| ())?;
+ match_ignore_ascii_case! { &*ident,
+ "auto" => Ok(SpeakAs::Auto),
+ "bullets" => Ok(SpeakAs::Bullets),
+ "numbers" => Ok(SpeakAs::Numbers),
+ "words" => Ok(SpeakAs::Words),
+ "spell-out" => {
+ is_spell_out = true;
+ Err(())
+ },
+ _ => Err(()),
+ }
+ });
+ if is_spell_out {
+ // spell-out is not supported, but don’t parse it as a <counter-style-name>.
+ // See bug 1024178.
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ result.or_else(|_| Ok(SpeakAs::Other(parse_counter_style_name(input)?)))
+ }
+}
diff --git a/servo/components/style/counter_style/predefined.rs b/servo/components/style/counter_style/predefined.rs
new file mode 100644
index 0000000000..7243e3b3f3
--- /dev/null
+++ b/servo/components/style/counter_style/predefined.rs
@@ -0,0 +1,61 @@
+/* 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/. */
+
+predefined! {
+ "decimal",
+ "decimal-leading-zero",
+ "arabic-indic",
+ "armenian",
+ "upper-armenian",
+ "lower-armenian",
+ "bengali",
+ "cambodian",
+ "khmer",
+ "cjk-decimal",
+ "devanagari",
+ "georgian",
+ "gujarati",
+ "gurmukhi",
+ "hebrew",
+ "kannada",
+ "lao",
+ "malayalam",
+ "mongolian",
+ "myanmar",
+ "oriya",
+ "persian",
+ "lower-roman",
+ "upper-roman",
+ "tamil",
+ "telugu",
+ "thai",
+ "tibetan",
+ "lower-alpha",
+ "lower-latin",
+ "upper-alpha",
+ "upper-latin",
+ "cjk-earthly-branch",
+ "cjk-heavenly-stem",
+ "lower-greek",
+ "hiragana",
+ "hiragana-iroha",
+ "katakana",
+ "katakana-iroha",
+ "disc",
+ "circle",
+ "square",
+ "disclosure-open",
+ "disclosure-closed",
+ "japanese-informal",
+ "japanese-formal",
+ "korean-hangul-formal",
+ "korean-hanja-informal",
+ "korean-hanja-formal",
+ "simp-chinese-informal",
+ "simp-chinese-formal",
+ "trad-chinese-informal",
+ "trad-chinese-formal",
+ "cjk-ideographic",
+ "ethiopic-numeric",
+}
diff --git a/servo/components/style/counter_style/update_predefined.py b/servo/components/style/counter_style/update_predefined.py
new file mode 100755
index 0000000000..1523958ff3
--- /dev/null
+++ b/servo/components/style/counter_style/update_predefined.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+# 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 os.path
+import re
+import urllib
+
+
+def main(filename):
+ names = [
+ re.search('>([^>]+)(</dfn>|<a class="self-link")', line).group(1)
+ for line in urllib.urlopen("https://drafts.csswg.org/css-counter-styles/")
+ if 'data-dfn-for="<counter-style-name>"' in line
+ or 'data-dfn-for="<counter-style>"' in line
+ ]
+ with open(filename, "wb") as f:
+ f.write(
+ """\
+/* 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/. */
+
+predefined! {
+"""
+ )
+ for name in names:
+ f.write(' "%s",\n' % name)
+ f.write("}\n")
+
+
+if __name__ == "__main__":
+ main(os.path.join(os.path.dirname(__file__), "predefined.rs"))
diff --git a/servo/components/style/custom_properties.rs b/servo/components/style/custom_properties.rs
new file mode 100644
index 0000000000..cb3b9685ae
--- /dev/null
+++ b/servo/components/style/custom_properties.rs
@@ -0,0 +1,1959 @@
+/* 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/. */
+
+//! Support for [custom properties for cascading variables][custom].
+//!
+//! [custom]: https://drafts.csswg.org/css-variables/
+
+use crate::applicable_declarations::CascadePriority;
+use crate::custom_properties_map::CustomPropertiesMap;
+use crate::media_queries::Device;
+use crate::properties::{
+ CSSWideKeyword, CustomDeclaration, CustomDeclarationValue, LonghandId, LonghandIdSet,
+ VariableDeclaration,
+};
+use crate::properties_and_values::{
+ registry::PropertyRegistrationData,
+ value::{AllowComputationallyDependent, SpecifiedValue as SpecifiedRegisteredValue},
+};
+use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet};
+use crate::stylesheets::UrlExtraData;
+use crate::stylist::Stylist;
+use crate::values::computed;
+use crate::values::specified::FontRelativeLength;
+use crate::Atom;
+use cssparser::{
+ CowRcStr, Delimiter, Parser, ParserInput, SourcePosition, Token, TokenSerializationType,
+};
+use selectors::parser::SelectorParseErrorKind;
+use servo_arc::Arc;
+use smallvec::SmallVec;
+use std::borrow::Cow;
+use std::collections::hash_map::Entry;
+use std::fmt::{self, Write};
+use std::ops::{Index, IndexMut};
+use std::{cmp, num};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// The environment from which to get `env` function values.
+///
+/// TODO(emilio): If this becomes a bit more complex we should probably move it
+/// to the `media_queries` module, or something.
+#[derive(Debug, MallocSizeOf)]
+pub struct CssEnvironment;
+
+type EnvironmentEvaluator = fn(device: &Device, url_data: &UrlExtraData) -> VariableValue;
+
+struct EnvironmentVariable {
+ name: Atom,
+ evaluator: EnvironmentEvaluator,
+}
+
+macro_rules! make_variable {
+ ($name:expr, $evaluator:expr) => {{
+ EnvironmentVariable {
+ name: $name,
+ evaluator: $evaluator,
+ }
+ }};
+}
+
+fn get_safearea_inset_top(device: &Device, url_data: &UrlExtraData) -> VariableValue {
+ VariableValue::pixels(device.safe_area_insets().top, url_data)
+}
+
+fn get_safearea_inset_bottom(device: &Device, url_data: &UrlExtraData) -> VariableValue {
+ VariableValue::pixels(device.safe_area_insets().bottom, url_data)
+}
+
+fn get_safearea_inset_left(device: &Device, url_data: &UrlExtraData) -> VariableValue {
+ VariableValue::pixels(device.safe_area_insets().left, url_data)
+}
+
+fn get_safearea_inset_right(device: &Device, url_data: &UrlExtraData) -> VariableValue {
+ VariableValue::pixels(device.safe_area_insets().right, url_data)
+}
+
+fn get_content_preferred_color_scheme(device: &Device, url_data: &UrlExtraData) -> VariableValue {
+ use crate::gecko::media_features::PrefersColorScheme;
+ let prefers_color_scheme = unsafe {
+ crate::gecko_bindings::bindings::Gecko_MediaFeatures_PrefersColorScheme(
+ device.document(),
+ /* use_content = */ true,
+ )
+ };
+ VariableValue::ident(
+ match prefers_color_scheme {
+ PrefersColorScheme::Light => "light",
+ PrefersColorScheme::Dark => "dark",
+ },
+ url_data,
+ )
+}
+
+fn get_scrollbar_inline_size(device: &Device, url_data: &UrlExtraData) -> VariableValue {
+ VariableValue::pixels(device.scrollbar_inline_size().px(), url_data)
+}
+
+static ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [
+ make_variable!(atom!("safe-area-inset-top"), get_safearea_inset_top),
+ make_variable!(atom!("safe-area-inset-bottom"), get_safearea_inset_bottom),
+ make_variable!(atom!("safe-area-inset-left"), get_safearea_inset_left),
+ make_variable!(atom!("safe-area-inset-right"), get_safearea_inset_right),
+];
+
+macro_rules! lnf_int {
+ ($id:ident) => {
+ unsafe {
+ crate::gecko_bindings::bindings::Gecko_GetLookAndFeelInt(
+ crate::gecko_bindings::bindings::LookAndFeel_IntID::$id as i32,
+ )
+ }
+ };
+}
+
+macro_rules! lnf_int_variable {
+ ($atom:expr, $id:ident, $ctor:ident) => {{
+ fn __eval(_: &Device, url_data: &UrlExtraData) -> VariableValue {
+ VariableValue::$ctor(lnf_int!($id), url_data)
+ }
+ make_variable!($atom, __eval)
+ }};
+}
+
+static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 7] = [
+ lnf_int_variable!(
+ atom!("-moz-gtk-csd-titlebar-radius"),
+ TitlebarRadius,
+ int_pixels
+ ),
+ lnf_int_variable!(
+ atom!("-moz-gtk-csd-close-button-position"),
+ GTKCSDCloseButtonPosition,
+ integer
+ ),
+ lnf_int_variable!(
+ atom!("-moz-gtk-csd-minimize-button-position"),
+ GTKCSDMinimizeButtonPosition,
+ integer
+ ),
+ lnf_int_variable!(
+ atom!("-moz-gtk-csd-maximize-button-position"),
+ GTKCSDMaximizeButtonPosition,
+ integer
+ ),
+ lnf_int_variable!(
+ atom!("-moz-overlay-scrollbar-fade-duration"),
+ ScrollbarFadeDuration,
+ int_ms
+ ),
+ make_variable!(
+ atom!("-moz-content-preferred-color-scheme"),
+ get_content_preferred_color_scheme
+ ),
+ make_variable!(atom!("scrollbar-inline-size"), get_scrollbar_inline_size),
+];
+
+impl CssEnvironment {
+ #[inline]
+ fn get(&self, name: &Atom, device: &Device, url_data: &UrlExtraData) -> Option<VariableValue> {
+ if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) {
+ return Some((var.evaluator)(device, url_data));
+ }
+ if !url_data.chrome_rules_enabled() {
+ return None;
+ }
+ let var = CHROME_ENVIRONMENT_VARIABLES
+ .iter()
+ .find(|var| var.name == *name)?;
+ Some((var.evaluator)(device, url_data))
+ }
+}
+
+/// A custom property name is just an `Atom`.
+///
+/// Note that this does not include the `--` prefix
+pub type Name = Atom;
+
+/// Parse a custom property name.
+///
+/// <https://drafts.csswg.org/css-variables/#typedef-custom-property-name>
+pub fn parse_name(s: &str) -> Result<&str, ()> {
+ if s.starts_with("--") && s.len() > 2 {
+ Ok(&s[2..])
+ } else {
+ Err(())
+ }
+}
+
+/// A value for a custom property is just a set of tokens.
+///
+/// We preserve the original CSS for serialization, and also the variable
+/// references to other custom property names.
+#[derive(Clone, Debug, MallocSizeOf, ToShmem)]
+pub struct VariableValue {
+ /// The raw CSS string.
+ pub css: String,
+
+ /// The url data of the stylesheet where this value came from.
+ pub url_data: UrlExtraData,
+
+ first_token_type: TokenSerializationType,
+ last_token_type: TokenSerializationType,
+
+ /// var(), env(), or non-custom property (e.g. through `em`) references.
+ references: References,
+}
+
+trivial_to_computed_value!(VariableValue);
+
+// For all purposes, we want values to be considered equal if their css text is equal.
+impl PartialEq for VariableValue {
+ fn eq(&self, other: &Self) -> bool {
+ self.css == other.css
+ }
+}
+
+impl Eq for VariableValue {}
+
+impl ToCss for SpecifiedValue {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(&self.css)
+ }
+}
+
+/// A pair of separate CustomPropertiesMaps, split between custom properties
+/// that have the inherit flag set and those with the flag unset.
+#[repr(C)]
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct ComputedCustomProperties {
+ /// Map for custom properties with inherit flag set, including non-registered
+ /// ones.
+ pub inherited: CustomPropertiesMap,
+ /// Map for custom properties with inherit flag unset.
+ pub non_inherited: CustomPropertiesMap,
+}
+
+impl ComputedCustomProperties {
+ /// Return whether the inherited and non_inherited maps are none.
+ pub fn is_empty(&self) -> bool {
+ self.inherited.is_empty() && self.non_inherited.is_empty()
+ }
+
+ /// Return the name and value of the property at specified index, if any.
+ pub fn property_at(&self, index: usize) -> Option<(&Name, &Option<Arc<VariableValue>>)> {
+ // Just expose the custom property items from custom_properties.inherited, followed
+ // by custom property items from custom_properties.non_inherited.
+ self.inherited
+ .get_index(index)
+ .or_else(|| self.non_inherited.get_index(index - self.inherited.len()))
+ }
+
+ /// Insert a custom property in the corresponding inherited/non_inherited
+ /// map, depending on whether the inherit flag is set or unset.
+ fn insert(
+ &mut self,
+ registration: &PropertyRegistrationData,
+ name: &Name,
+ value: Arc<VariableValue>,
+ ) {
+ self.map_mut(registration).insert(name, value)
+ }
+
+ /// Remove a custom property from the corresponding inherited/non_inherited
+ /// map, depending on whether the inherit flag is set or unset.
+ fn remove(&mut self, registration: &PropertyRegistrationData, name: &Name) {
+ self.map_mut(registration).remove(name);
+ }
+
+ /// Shrink the capacity of the inherited maps as much as possible.
+ fn shrink_to_fit(&mut self) {
+ self.inherited.shrink_to_fit();
+ self.non_inherited.shrink_to_fit();
+ }
+
+ fn map_mut(&mut self, registration: &PropertyRegistrationData) -> &mut CustomPropertiesMap {
+ if registration.inherits() {
+ &mut self.inherited
+ } else {
+ &mut self.non_inherited
+ }
+ }
+
+ fn get(
+ &self,
+ registration: &PropertyRegistrationData,
+ name: &Name,
+ ) -> Option<&Arc<VariableValue>> {
+ if registration.inherits() {
+ self.inherited.get(name)
+ } else {
+ self.non_inherited.get(name)
+ }
+ }
+}
+
+/// Both specified and computed values are VariableValues, the difference is
+/// whether var() functions are expanded.
+pub type SpecifiedValue = VariableValue;
+/// Both specified and computed values are VariableValues, the difference is
+/// whether var() functions are expanded.
+pub type ComputedValue = VariableValue;
+
+/// Set of flags to non-custom references this custom property makes.
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, MallocSizeOf, ToShmem)]
+struct NonCustomReferences(u8);
+
+bitflags! {
+ impl NonCustomReferences: u8 {
+ /// At least one custom property depends on font-relative units.
+ const FONT_UNITS = 1 << 0;
+ /// At least one custom property depends on root element's font-relative units.
+ const ROOT_FONT_UNITS = 1 << 1;
+ /// At least one custom property depends on line height units.
+ const LH_UNITS = 1 << 2;
+ /// At least one custom property depends on root element's line height units.
+ const ROOT_LH_UNITS = 1 << 3;
+ /// All dependencies not depending on the root element.
+ const NON_ROOT_DEPENDENCIES = Self::FONT_UNITS.bits() | Self::LH_UNITS.bits();
+ /// All dependencies depending on the root element.
+ const ROOT_DEPENDENCIES = Self::ROOT_FONT_UNITS.bits() | Self::ROOT_LH_UNITS.bits();
+ }
+}
+
+impl NonCustomReferences {
+ fn for_each<F>(&self, mut f: F)
+ where
+ F: FnMut(SingleNonCustomReference),
+ {
+ for (_, r) in self.iter_names() {
+ let single = match r {
+ Self::FONT_UNITS => SingleNonCustomReference::FontUnits,
+ Self::ROOT_FONT_UNITS => SingleNonCustomReference::RootFontUnits,
+ Self::LH_UNITS => SingleNonCustomReference::LhUnits,
+ Self::ROOT_LH_UNITS => SingleNonCustomReference::RootLhUnits,
+ _ => unreachable!("Unexpected single bit value"),
+ };
+ f(single);
+ }
+ }
+
+ fn from_unit(value: &CowRcStr) -> Self {
+ // For registered properties, any reference to font-relative dimensions
+ // make it dependent on font-related properties.
+ // TODO(dshin): When we unit algebra gets implemented and handled -
+ // Is it valid to say that `calc(1em / 2em * 3px)` triggers this?
+ if value.eq_ignore_ascii_case(FontRelativeLength::LH) {
+ return Self::FONT_UNITS | Self::LH_UNITS;
+ }
+ if value.eq_ignore_ascii_case(FontRelativeLength::EM) ||
+ value.eq_ignore_ascii_case(FontRelativeLength::EX) ||
+ value.eq_ignore_ascii_case(FontRelativeLength::CAP) ||
+ value.eq_ignore_ascii_case(FontRelativeLength::CH) ||
+ value.eq_ignore_ascii_case(FontRelativeLength::IC)
+ {
+ return Self::FONT_UNITS;
+ }
+ if value.eq_ignore_ascii_case(FontRelativeLength::RLH) {
+ return Self::ROOT_FONT_UNITS | Self::ROOT_LH_UNITS;
+ }
+ if value.eq_ignore_ascii_case(FontRelativeLength::REM) {
+ return Self::ROOT_FONT_UNITS;
+ }
+ Self::empty()
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum SingleNonCustomReference {
+ FontUnits = 0,
+ RootFontUnits,
+ LhUnits,
+ RootLhUnits,
+}
+
+struct NonCustomReferenceMap<T>([Option<T>; 4]);
+
+impl<T> Default for NonCustomReferenceMap<T> {
+ fn default() -> Self {
+ NonCustomReferenceMap(Default::default())
+ }
+}
+
+impl<T> Index<SingleNonCustomReference> for NonCustomReferenceMap<T> {
+ type Output = Option<T>;
+
+ fn index(&self, reference: SingleNonCustomReference) -> &Self::Output {
+ &self.0[reference as usize]
+ }
+}
+
+impl<T> IndexMut<SingleNonCustomReference> for NonCustomReferenceMap<T> {
+ fn index_mut(&mut self, reference: SingleNonCustomReference) -> &mut Self::Output {
+ &mut self.0[reference as usize]
+ }
+}
+
+/// Whether to defer resolving custom properties referencing font relative units.
+#[derive(Clone, Copy, PartialEq, Eq)]
+#[allow(missing_docs)]
+pub enum DeferFontRelativeCustomPropertyResolution {
+ Yes,
+ No,
+}
+
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+struct VariableFallback {
+ start: num::NonZeroUsize,
+ first_token_type: TokenSerializationType,
+ last_token_type: TokenSerializationType,
+}
+
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+struct VarOrEnvReference {
+ name: Name,
+ start: usize,
+ end: usize,
+ fallback: Option<VariableFallback>,
+ prev_token_type: TokenSerializationType,
+ next_token_type: TokenSerializationType,
+ is_var: bool,
+}
+
+/// A struct holding information about the external references to that a custom
+/// property value may have.
+#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
+struct References {
+ refs: Vec<VarOrEnvReference>,
+ non_custom_references: NonCustomReferences,
+ any_env: bool,
+ any_var: bool,
+}
+
+impl References {
+ fn has_references(&self) -> bool {
+ !self.refs.is_empty()
+ }
+
+ fn get_non_custom_dependencies(&self, is_root_element: bool) -> NonCustomReferences {
+ let mask = NonCustomReferences::NON_ROOT_DEPENDENCIES;
+ let mask = if is_root_element {
+ mask | NonCustomReferences::ROOT_DEPENDENCIES
+ } else {
+ mask
+ };
+
+ self.non_custom_references & mask
+ }
+}
+
+impl VariableValue {
+ fn empty(url_data: &UrlExtraData) -> Self {
+ Self {
+ css: String::new(),
+ last_token_type: Default::default(),
+ first_token_type: Default::default(),
+ url_data: url_data.clone(),
+ references: Default::default(),
+ }
+ }
+
+ /// Create a new custom property without parsing if the CSS is known to be valid and contain no
+ /// references.
+ pub fn new(
+ css: String,
+ url_data: &UrlExtraData,
+ first_token_type: TokenSerializationType,
+ last_token_type: TokenSerializationType,
+ ) -> Self {
+ Self {
+ css,
+ url_data: url_data.clone(),
+ first_token_type,
+ last_token_type,
+ references: Default::default(),
+ }
+ }
+
+ fn push<'i>(
+ &mut self,
+ css: &str,
+ css_first_token_type: TokenSerializationType,
+ css_last_token_type: TokenSerializationType,
+ ) -> Result<(), ()> {
+ /// Prevent values from getting terribly big since you can use custom
+ /// properties exponentially.
+ ///
+ /// This number (2MB) is somewhat arbitrary, but silly enough that no
+ /// reasonable page should hit it. We could limit by number of total
+ /// substitutions, but that was very easy to work around in practice
+ /// (just choose a larger initial value and boom).
+ const MAX_VALUE_LENGTH_IN_BYTES: usize = 2 * 1024 * 1024;
+
+ if self.css.len() + css.len() > MAX_VALUE_LENGTH_IN_BYTES {
+ return Err(());
+ }
+
+ // This happens e.g. between two subsequent var() functions:
+ // `var(--a)var(--b)`.
+ //
+ // In that case, css_*_token_type is nonsensical.
+ if css.is_empty() {
+ return Ok(());
+ }
+
+ self.first_token_type.set_if_nothing(css_first_token_type);
+ // If self.first_token_type was nothing,
+ // self.last_token_type is also nothing and this will be false:
+ if self
+ .last_token_type
+ .needs_separator_when_before(css_first_token_type)
+ {
+ self.css.push_str("/**/")
+ }
+ self.css.push_str(css);
+ self.last_token_type = css_last_token_type;
+ Ok(())
+ }
+
+ /// Parse a custom property value.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ url_data: &UrlExtraData,
+ ) -> Result<Self, ParseError<'i>> {
+ input.skip_whitespace();
+
+ let mut references = References::default();
+ let mut missing_closing_characters = String::new();
+ let start_position = input.position();
+ let (first_token_type, last_token_type) = parse_declaration_value(
+ input,
+ start_position,
+ &mut references,
+ &mut missing_closing_characters,
+ )?;
+ let mut css = input.slice_from(start_position).to_owned();
+ if !missing_closing_characters.is_empty() {
+ // Unescaped backslash at EOF in a quoted string is ignored.
+ if css.ends_with("\\") &&
+ matches!(missing_closing_characters.as_bytes()[0], b'"' | b'\'')
+ {
+ css.pop();
+ }
+ css.push_str(&missing_closing_characters);
+ }
+
+ css.shrink_to_fit();
+ references.refs.shrink_to_fit();
+
+ Ok(Self {
+ css,
+ url_data: url_data.clone(),
+ first_token_type,
+ last_token_type,
+ references,
+ })
+ }
+
+ /// Create VariableValue from an int.
+ fn integer(number: i32, url_data: &UrlExtraData) -> Self {
+ Self::from_token(
+ Token::Number {
+ has_sign: false,
+ value: number as f32,
+ int_value: Some(number),
+ },
+ url_data,
+ )
+ }
+
+ /// Create VariableValue from an int.
+ fn ident(ident: &'static str, url_data: &UrlExtraData) -> Self {
+ Self::from_token(Token::Ident(ident.into()), url_data)
+ }
+
+ /// Create VariableValue from a float amount of CSS pixels.
+ fn pixels(number: f32, url_data: &UrlExtraData) -> Self {
+ // FIXME (https://github.com/servo/rust-cssparser/issues/266):
+ // No way to get TokenSerializationType::Dimension without creating
+ // Token object.
+ Self::from_token(
+ Token::Dimension {
+ has_sign: false,
+ value: number,
+ int_value: None,
+ unit: CowRcStr::from("px"),
+ },
+ url_data,
+ )
+ }
+
+ /// Create VariableValue from an integer amount of milliseconds.
+ fn int_ms(number: i32, url_data: &UrlExtraData) -> Self {
+ Self::from_token(
+ Token::Dimension {
+ has_sign: false,
+ value: number as f32,
+ int_value: Some(number),
+ unit: CowRcStr::from("ms"),
+ },
+ url_data,
+ )
+ }
+
+ /// Create VariableValue from an integer amount of CSS pixels.
+ fn int_pixels(number: i32, url_data: &UrlExtraData) -> Self {
+ Self::from_token(
+ Token::Dimension {
+ has_sign: false,
+ value: number as f32,
+ int_value: Some(number),
+ unit: CowRcStr::from("px"),
+ },
+ url_data,
+ )
+ }
+
+ fn from_token(token: Token, url_data: &UrlExtraData) -> Self {
+ let token_type = token.serialization_type();
+ let mut css = token.to_css_string();
+ css.shrink_to_fit();
+
+ VariableValue {
+ css,
+ url_data: url_data.clone(),
+ first_token_type: token_type,
+ last_token_type: token_type,
+ references: Default::default(),
+ }
+ }
+
+ /// Returns the raw CSS text from this VariableValue
+ pub fn css_text(&self) -> &str {
+ &self.css
+ }
+
+ /// Returns whether this variable value has any reference to the environment or other
+ /// variables.
+ pub fn has_references(&self) -> bool {
+ self.references.has_references()
+ }
+}
+
+/// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value>
+fn parse_declaration_value<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ input_start: SourcePosition,
+ references: &mut References,
+ missing_closing_characters: &mut String,
+) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> {
+ input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| {
+ parse_declaration_value_block(input, input_start, references, missing_closing_characters)
+ })
+}
+
+/// Like parse_declaration_value, but accept `!` and `;` since they are only invalid at the top level.
+fn parse_declaration_value_block<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ input_start: SourcePosition,
+ references: &mut References,
+ missing_closing_characters: &mut String,
+) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> {
+ let mut is_first = true;
+ let mut first_token_type = TokenSerializationType::Nothing;
+ let mut last_token_type = TokenSerializationType::Nothing;
+ let mut prev_reference_index: Option<usize> = None;
+ loop {
+ let token_start = input.position();
+ let Ok(token) = input.next_including_whitespace_and_comments() else { break };
+
+ let prev_token_type = last_token_type;
+ let serialization_type = token.serialization_type();
+ last_token_type = serialization_type;
+ if is_first {
+ first_token_type = last_token_type;
+ is_first = false;
+ }
+
+ macro_rules! nested {
+ () => {
+ input.parse_nested_block(|input| {
+ parse_declaration_value_block(
+ input,
+ input_start,
+ references,
+ missing_closing_characters,
+ )
+ })?
+ };
+ }
+ macro_rules! check_closed {
+ ($closing:expr) => {
+ if !input.slice_from(token_start).ends_with($closing) {
+ missing_closing_characters.push_str($closing)
+ }
+ };
+ }
+ if let Some(index) = prev_reference_index.take() {
+ references.refs[index].next_token_type = serialization_type;
+ }
+ match *token {
+ Token::Comment(_) => {
+ let token_slice = input.slice_from(token_start);
+ if !token_slice.ends_with("*/") {
+ missing_closing_characters.push_str(if token_slice.ends_with('*') {
+ "/"
+ } else {
+ "*/"
+ })
+ }
+ },
+ Token::BadUrl(ref u) => {
+ let e = StyleParseErrorKind::BadUrlInDeclarationValueBlock(u.clone());
+ return Err(input.new_custom_error(e));
+ },
+ Token::BadString(ref s) => {
+ let e = StyleParseErrorKind::BadStringInDeclarationValueBlock(s.clone());
+ return Err(input.new_custom_error(e));
+ },
+ Token::CloseParenthesis => {
+ let e = StyleParseErrorKind::UnbalancedCloseParenthesisInDeclarationValueBlock;
+ return Err(input.new_custom_error(e));
+ },
+ Token::CloseSquareBracket => {
+ let e = StyleParseErrorKind::UnbalancedCloseSquareBracketInDeclarationValueBlock;
+ return Err(input.new_custom_error(e));
+ },
+ Token::CloseCurlyBracket => {
+ let e = StyleParseErrorKind::UnbalancedCloseCurlyBracketInDeclarationValueBlock;
+ return Err(input.new_custom_error(e));
+ },
+ Token::Function(ref name) => {
+ let is_var = name.eq_ignore_ascii_case("var");
+ if is_var || name.eq_ignore_ascii_case("env") {
+ let our_ref_index = references.refs.len();
+ let fallback = input.parse_nested_block(|input| {
+ // TODO(emilio): For env() this should be <custom-ident> per spec, but no other browser does
+ // that, see https://github.com/w3c/csswg-drafts/issues/3262.
+ let name = input.expect_ident()?;
+ let name = Atom::from(if is_var {
+ match parse_name(name.as_ref()) {
+ Ok(name) => name,
+ Err(()) => {
+ let name = name.clone();
+ return Err(input.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(name),
+ ));
+ },
+ }
+ } else {
+ name.as_ref()
+ });
+
+ // We want the order of the references to match source order. So we need to reserve our slot
+ // now, _before_ parsing our fallback. Note that we don't care if parsing fails after all, since
+ // if this fails we discard the whole result anyways.
+ let start = token_start.byte_index() - input_start.byte_index();
+ references.refs.push(VarOrEnvReference {
+ name,
+ start,
+ // To be fixed up after parsing fallback and auto-closing via our_ref_index.
+ end: start,
+ prev_token_type,
+ // To be fixed up (if needed) on the next loop iteration via prev_reference_index.
+ next_token_type: TokenSerializationType::Nothing,
+ // To be fixed up after parsing fallback.
+ fallback: None,
+ is_var,
+ });
+
+ let mut fallback = None;
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ input.skip_whitespace();
+ let fallback_start = num::NonZeroUsize::new(
+ input.position().byte_index() - input_start.byte_index(),
+ )
+ .unwrap();
+ // NOTE(emilio): Intentionally using parse_declaration_value rather than
+ // parse_declaration_value_block, since that's what parse_fallback used to do.
+ let (first, last) = parse_declaration_value(
+ input,
+ input_start,
+ references,
+ missing_closing_characters,
+ )?;
+ fallback = Some(VariableFallback {
+ start: fallback_start,
+ first_token_type: first,
+ last_token_type: last,
+ });
+ } else {
+ let state = input.state();
+ // We still need to consume the rest of the potentially-unclosed
+ // tokens, but make sure to not consume tokens that would otherwise be
+ // invalid, by calling reset().
+ parse_declaration_value_block(
+ input,
+ input_start,
+ references,
+ missing_closing_characters,
+ )?;
+ input.reset(&state);
+ }
+ Ok(fallback)
+ })?;
+ check_closed!(")");
+ prev_reference_index = Some(our_ref_index);
+ let reference = &mut references.refs[our_ref_index];
+ reference.end = input.position().byte_index() - input_start.byte_index() + missing_closing_characters.len();
+ reference.fallback = fallback;
+ if is_var {
+ references.any_var = true;
+ } else {
+ references.any_env = true;
+ }
+ } else {
+ nested!();
+ check_closed!(")");
+ }
+ },
+ Token::ParenthesisBlock => {
+ nested!();
+ check_closed!(")");
+ },
+ Token::CurlyBracketBlock => {
+ nested!();
+ check_closed!("}");
+ },
+ Token::SquareBracketBlock => {
+ nested!();
+ check_closed!("]");
+ },
+ Token::QuotedString(_) => {
+ let token_slice = input.slice_from(token_start);
+ let quote = &token_slice[..1];
+ debug_assert!(matches!(quote, "\"" | "'"));
+ if !(token_slice.ends_with(quote) && token_slice.len() > 1) {
+ missing_closing_characters.push_str(quote)
+ }
+ },
+ Token::Ident(ref value) |
+ Token::AtKeyword(ref value) |
+ Token::Hash(ref value) |
+ Token::IDHash(ref value) |
+ Token::UnquotedUrl(ref value) |
+ Token::Dimension {
+ unit: ref value, ..
+ } => {
+ references
+ .non_custom_references
+ .insert(NonCustomReferences::from_unit(value));
+ let is_unquoted_url = matches!(token, Token::UnquotedUrl(_));
+ if value.ends_with("�") && input.slice_from(token_start).ends_with("\\") {
+ // Unescaped backslash at EOF in these contexts is interpreted as U+FFFD
+ // Check the value in case the final backslash was itself escaped.
+ // Serialize as escaped U+FFFD, which is also interpreted as U+FFFD.
+ // (Unescaped U+FFFD would also work, but removing the backslash is annoying.)
+ missing_closing_characters.push_str("�")
+ }
+ if is_unquoted_url {
+ check_closed!(")");
+ }
+ },
+ _ => {},
+ };
+ }
+ Ok((first_token_type, last_token_type))
+}
+
+/// A struct that takes care of encapsulating the cascade process for custom properties.
+pub struct CustomPropertiesBuilder<'a, 'b: 'a> {
+ seen: PrecomputedHashSet<&'a Name>,
+ may_have_cycles: bool,
+ custom_properties: ComputedCustomProperties,
+ reverted: PrecomputedHashMap<&'a Name, (CascadePriority, bool)>,
+ stylist: &'a Stylist,
+ computed_context: &'a mut computed::Context<'b>,
+ references_from_non_custom_properties: NonCustomReferenceMap<Vec<Name>>,
+}
+
+impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> {
+ /// Create a new builder, inheriting from a given custom properties map.
+ ///
+ /// We expose this publicly mostly for @keyframe blocks.
+ pub fn new_with_properties(stylist: &'a Stylist, custom_properties: ComputedCustomProperties, computed_context: &'a mut computed::Context<'b>) -> Self {
+ Self {
+ seen: PrecomputedHashSet::default(),
+ reverted: Default::default(),
+ may_have_cycles: false,
+ custom_properties,
+ stylist,
+ computed_context,
+ references_from_non_custom_properties: NonCustomReferenceMap::default(),
+ }
+ }
+
+ /// Create a new builder, inheriting from the right style given context.
+ pub fn new(stylist: &'a Stylist, context: &'a mut computed::Context<'b>) -> Self {
+ let is_root_element = context.is_root_element();
+
+ let inherited = context.inherited_custom_properties();
+ let initial_values = stylist.get_custom_property_initial_values();
+ let properties = ComputedCustomProperties {
+ inherited: if is_root_element {
+ debug_assert!(inherited.is_empty());
+ initial_values.inherited.clone()
+ } else {
+ inherited.inherited.clone()
+ },
+ non_inherited: initial_values.non_inherited.clone(),
+ };
+
+ // Reuse flags from computing registered custom properties initial values, such as
+ // whether they depend on viewport units.
+ context.style().add_flags(stylist.get_custom_property_initial_values_flags());
+ Self::new_with_properties(stylist, properties, context)
+ }
+
+ /// Cascade a given custom property declaration.
+ pub fn cascade(&mut self, declaration: &'a CustomDeclaration, priority: CascadePriority) {
+ let CustomDeclaration {
+ ref name,
+ ref value,
+ } = *declaration;
+
+ if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&name) {
+ if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) {
+ return;
+ }
+ }
+
+ let was_already_present = !self.seen.insert(name);
+ if was_already_present {
+ return;
+ }
+
+ if !self.value_may_affect_style(name, value) {
+ return;
+ }
+
+ let map = &mut self.custom_properties;
+ let registration = self.stylist.get_custom_property_registration(&name);
+ match *value {
+ CustomDeclarationValue::Value(ref unparsed_value) => {
+ let has_custom_property_references = unparsed_value.references.any_var;
+ let registered_length_property =
+ registration.syntax.may_reference_font_relative_length();
+ // Non-custom dependency is really relevant for registered custom properties
+ // that require computed value of such dependencies.
+ let has_non_custom_dependencies = registered_length_property &&
+ !unparsed_value
+ .references
+ .get_non_custom_dependencies(self.computed_context.is_root_element())
+ .is_empty();
+ self.may_have_cycles |=
+ has_custom_property_references || has_non_custom_dependencies;
+
+ // If the variable value has no references to other properties, perform
+ // substitution here instead of forcing a full traversal in `substitute_all`
+ // afterwards.
+ if !has_custom_property_references && !has_non_custom_dependencies {
+ return substitute_references_if_needed_and_apply(
+ name,
+ unparsed_value,
+ map,
+ self.stylist,
+ self.computed_context,
+ );
+ }
+ map.insert(registration, name, Arc::clone(unparsed_value));
+ },
+ CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword {
+ CSSWideKeyword::RevertLayer | CSSWideKeyword::Revert => {
+ let origin_revert = keyword == CSSWideKeyword::Revert;
+ self.seen.remove(name);
+ self.reverted.insert(name, (priority, origin_revert));
+ },
+ CSSWideKeyword::Initial => {
+ // For non-inherited custom properties, 'initial' was handled in value_may_affect_style.
+ debug_assert!(registration.inherits(), "Should've been handled earlier");
+ map.remove(registration, name);
+ if let Some(ref initial_value) = registration.initial_value {
+ map.insert(registration, name, initial_value.clone());
+ }
+ },
+ CSSWideKeyword::Inherit => {
+ // For inherited custom properties, 'inherit' was handled in value_may_affect_style.
+ debug_assert!(!registration.inherits(), "Should've been handled earlier");
+ if let Some(inherited_value) = self
+ .computed_context
+ .inherited_custom_properties()
+ .non_inherited
+ .get(name)
+ {
+ map.insert(registration, name, inherited_value.clone());
+ }
+ },
+ // handled in value_may_affect_style
+ CSSWideKeyword::Unset => unreachable!(),
+ },
+ }
+ }
+
+ /// Note a non-custom property with variable reference that may in turn depend on that property.
+ /// e.g. `font-size` depending on a custom property that may be a registered property using `em`.
+ pub fn note_potentially_cyclic_non_custom_dependency(&mut self, id: LonghandId, decl: &VariableDeclaration) {
+ // With unit algebra in `calc()`, references aren't limited to `font-size`.
+ // For example, `--foo: 100ex; font-weight: calc(var(--foo) / 1ex);`,
+ // or `--foo: 1em; zoom: calc(var(--foo) * 30px / 2em);`
+ let references = match id {
+ LonghandId::FontSize => {
+ if self.computed_context.is_root_element() {
+ NonCustomReferences::ROOT_FONT_UNITS
+ } else {
+ NonCustomReferences::FONT_UNITS
+ }
+ },
+ LonghandId::LineHeight => {
+ if self.computed_context.is_root_element() {
+ NonCustomReferences::ROOT_LH_UNITS |
+ NonCustomReferences::ROOT_FONT_UNITS
+ } else {
+ NonCustomReferences::LH_UNITS | NonCustomReferences::FONT_UNITS
+ }
+ },
+ _ => return,
+ };
+ let refs = &decl.value.variable_value.references;
+ if !refs.any_var {
+ return;
+ }
+
+ let variables: Vec<Atom> = refs.refs.iter().filter_map(|reference| {
+ if !reference.is_var {
+ return None;
+ }
+ if !self.stylist.get_custom_property_registration(&reference.name).syntax.may_compute_length() {
+ return None;
+ }
+ Some(reference.name.clone())
+ }).collect();
+ references.for_each(|idx| {
+ let entry = &mut self.references_from_non_custom_properties[idx];
+ let was_none = entry.is_none();
+ let v = entry.get_or_insert_with(|| variables.clone());
+ if was_none {
+ return;
+ }
+ v.extend(variables.clone().into_iter());
+ });
+ }
+
+ fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool {
+ let registration = self.stylist.get_custom_property_registration(&name);
+ match *value {
+ CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => {
+ // For inherited custom properties, explicit 'inherit' means we
+ // can just use any existing value in the inherited
+ // CustomPropertiesMap.
+ if registration.inherits() {
+ return false;
+ }
+ },
+ CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial) => {
+ // For non-inherited custom properties, explicit 'initial' means
+ // we can just use any initial value in the registration.
+ if !registration.inherits() {
+ return false;
+ }
+ },
+ CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) => {
+ // Explicit 'unset' means we can either just use any existing
+ // value in the inherited CustomPropertiesMap or the initial
+ // value in the registration.
+ return false;
+ },
+ _ => {},
+ }
+
+ let existing_value = self.custom_properties.get(registration, &name);
+ match (existing_value, value) {
+ (None, &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial)) => {
+ debug_assert!(registration.inherits(), "Should've been handled earlier");
+ // The initial value of a custom property without a
+ // guaranteed-invalid initial value is the same as it
+ // not existing in the map.
+ if registration.initial_value.is_none() {
+ return false;
+ }
+ },
+ (
+ Some(existing_value),
+ &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial),
+ ) => {
+ debug_assert!(registration.inherits(), "Should've been handled earlier");
+ // Don't bother overwriting an existing value with the initial value specified in
+ // the registration.
+ if Some(existing_value) == registration.initial_value.as_ref() {
+ return false;
+ }
+ },
+ (Some(_), &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit)) => {
+ debug_assert!(!registration.inherits(), "Should've been handled earlier");
+ // existing_value is the registered initial value.
+ // Don't bother adding it to self.custom_properties.non_inherited
+ // if the key is also absent from self.inherited.non_inherited.
+ if self
+ .computed_context
+ .inherited_custom_properties()
+ .non_inherited
+ .get(name)
+ .is_none()
+ {
+ return false;
+ }
+ },
+ (Some(existing_value), &CustomDeclarationValue::Value(ref value)) => {
+ // Don't bother overwriting an existing value with the same
+ // specified value.
+ if existing_value == value {
+ return false;
+ }
+ },
+ _ => {},
+ }
+
+ true
+ }
+
+ /// Computes the map of applicable custom properties, as well as
+ /// longhand properties that are now considered invalid-at-compute time.
+ /// The result is saved into the computed context.
+ ///
+ /// If there was any specified property or non-inherited custom property
+ /// with an initial value, we've created a new map and now we
+ /// need to remove any potential cycles (And marking non-custom
+ /// properties), and wrap it in an arc.
+ ///
+ /// Some registered custom properties may require font-related properties
+ /// be resolved to resolve. If these properties are not resolved at this time,
+ /// `defer` should be set to `Yes`, which will leave such custom properties,
+ /// and other properties referencing them, untouched. These properties are
+ /// returned separately, to be resolved by `build_deferred` to fully resolve
+ /// all custom properties after all necessary non-custom properties are resolved.
+ pub fn build(
+ mut self,
+ defer: DeferFontRelativeCustomPropertyResolution,
+ ) -> Option<ComputedCustomProperties> {
+ let mut deferred_custom_properties = None;
+ if self.may_have_cycles {
+ if defer == DeferFontRelativeCustomPropertyResolution::Yes {
+ deferred_custom_properties = Some(ComputedCustomProperties::default());
+ }
+ let mut invalid_non_custom_properties = LonghandIdSet::default();
+ substitute_all(
+ &mut self.custom_properties,
+ deferred_custom_properties.as_mut(),
+ &mut invalid_non_custom_properties,
+ &self.seen,
+ &self.references_from_non_custom_properties,
+ self.stylist,
+ self.computed_context,
+ );
+ self.computed_context.builder.invalid_non_custom_properties = invalid_non_custom_properties;
+ }
+
+ self.custom_properties.shrink_to_fit();
+
+ // Some pages apply a lot of redundant custom properties, see e.g.
+ // bug 1758974 comment 5. Try to detect the case where the values
+ // haven't really changed, and save some memory by reusing the inherited
+ // map in that case.
+ let initial_values = self.stylist.get_custom_property_initial_values();
+ self.computed_context.builder.custom_properties = ComputedCustomProperties {
+ inherited: if self
+ .computed_context
+ .inherited_custom_properties()
+ .inherited == self.custom_properties.inherited
+ {
+ self.computed_context
+ .inherited_custom_properties()
+ .inherited
+ .clone()
+ } else {
+ self.custom_properties.inherited
+ },
+ non_inherited: if initial_values.non_inherited == self.custom_properties.non_inherited {
+ initial_values.non_inherited.clone()
+ } else {
+ self.custom_properties.non_inherited
+ },
+ };
+
+ deferred_custom_properties
+ }
+
+ /// Fully resolve all deferred custom properties, assuming that the incoming context
+ /// has necessary properties resolved.
+ pub fn build_deferred(
+ deferred: ComputedCustomProperties,
+ stylist: &Stylist,
+ computed_context: &mut computed::Context,
+ ) {
+ if deferred.is_empty() {
+ return;
+ }
+ // Guaranteed to not have cycles at this point.
+ let substitute =
+ |deferred: &CustomPropertiesMap,
+ stylist: &Stylist,
+ context: &computed::Context,
+ custom_properties: &mut ComputedCustomProperties| {
+ // Since `CustomPropertiesMap` preserves insertion order, we shouldn't
+ // have to worry about resolving in a wrong order.
+ for (k, v) in deferred.iter() {
+ let Some(v) = v else { continue };
+ substitute_references_if_needed_and_apply(
+ k,
+ v,
+ custom_properties,
+ stylist,
+ context,
+ );
+ }
+ };
+ let mut custom_properties = std::mem::take(&mut computed_context.builder.custom_properties);
+ substitute(
+ &deferred.inherited,
+ stylist,
+ computed_context,
+ &mut custom_properties,
+ );
+ substitute(
+ &deferred.non_inherited,
+ stylist,
+ computed_context,
+ &mut custom_properties,
+ );
+ computed_context.builder.custom_properties = custom_properties;
+ }
+}
+
+/// Resolve all custom properties to either substituted, invalid, or unset
+/// (meaning we should use the inherited value).
+///
+/// It does cycle dependencies removal at the same time as substitution.
+fn substitute_all(
+ custom_properties_map: &mut ComputedCustomProperties,
+ mut deferred_properties_map: Option<&mut ComputedCustomProperties>,
+ invalid_non_custom_properties: &mut LonghandIdSet,
+ seen: &PrecomputedHashSet<&Name>,
+ references_from_non_custom_properties: &NonCustomReferenceMap<Vec<Name>>,
+ stylist: &Stylist,
+ computed_context: &computed::Context,
+) {
+ // The cycle dependencies removal in this function is a variant
+ // of Tarjan's algorithm. It is mostly based on the pseudo-code
+ // listed in
+ // https://en.wikipedia.org/w/index.php?
+ // title=Tarjan%27s_strongly_connected_components_algorithm&oldid=801728495
+
+ #[derive(Clone, Eq, PartialEq, Debug)]
+ enum VarType {
+ Custom(Name),
+ NonCustom(SingleNonCustomReference),
+ }
+
+ /// Struct recording necessary information for each variable.
+ #[derive(Debug)]
+ struct VarInfo {
+ /// The name of the variable. It will be taken to save addref
+ /// when the corresponding variable is popped from the stack.
+ /// This also serves as a mark for whether the variable is
+ /// currently in the stack below.
+ var: Option<VarType>,
+ /// If the variable is in a dependency cycle, lowlink represents
+ /// a smaller index which corresponds to a variable in the same
+ /// strong connected component, which is known to be accessible
+ /// from this variable. It is not necessarily the root, though.
+ lowlink: usize,
+ }
+ /// Context struct for traversing the variable graph, so that we can
+ /// avoid referencing all the fields multiple times.
+ struct Context<'a, 'b: 'a> {
+ /// Number of variables visited. This is used as the order index
+ /// when we visit a new unresolved variable.
+ count: usize,
+ /// The map from custom property name to its order index.
+ index_map: PrecomputedHashMap<Name, usize>,
+ /// Mapping from a non-custom dependency to its order index.
+ non_custom_index_map: NonCustomReferenceMap<usize>,
+ /// Information of each variable indexed by the order index.
+ var_info: SmallVec<[VarInfo; 5]>,
+ /// The stack of order index of visited variables. It contains
+ /// all unfinished strong connected components.
+ stack: SmallVec<[usize; 5]>,
+ /// References to non-custom properties in this strongly connected component.
+ non_custom_references: NonCustomReferences,
+ map: &'a mut ComputedCustomProperties,
+ /// The stylist is used to get registered properties, and to resolve the environment to
+ /// substitute `env()` variables.
+ stylist: &'a Stylist,
+ /// The computed context is used to get inherited custom
+ /// properties and compute registered custom properties.
+ computed_context: &'a computed::Context<'b>,
+ /// Longhand IDs that became invalid due to dependency cycle(s).
+ invalid_non_custom_properties: &'a mut LonghandIdSet,
+ /// Properties that cannot yet be substituted.
+ deferred_properties: Option<&'a mut ComputedCustomProperties>,
+ }
+
+ /// This function combines the traversal for cycle removal and value
+ /// substitution. It returns either a signal None if this variable
+ /// has been fully resolved (to either having no reference or being
+ /// marked invalid), or the order index for the given name.
+ ///
+ /// When it returns, the variable corresponds to the name would be
+ /// in one of the following states:
+ /// * It is still in context.stack, which means it is part of an
+ /// potentially incomplete dependency circle.
+ /// * It has been removed from the map. It can be either that the
+ /// substitution failed, or it is inside a dependency circle.
+ /// When this function removes a variable from the map because
+ /// of dependency circle, it would put all variables in the same
+ /// strong connected component to the set together.
+ /// * It doesn't have any reference, because either this variable
+ /// doesn't have reference at all in specified value, or it has
+ /// been completely resolved.
+ /// * There is no such variable at all.
+ fn traverse<'a, 'b>(
+ var: VarType,
+ non_custom_references: &NonCustomReferenceMap<Vec<Name>>,
+ context: &mut Context<'a, 'b>,
+ ) -> Option<usize> {
+ // Some shortcut checks.
+ let (value, should_substitute) = match var {
+ VarType::Custom(ref name) => {
+ let registration = context.stylist.get_custom_property_registration(name);
+ let value = context.map.get(registration, name)?;
+
+ let non_custom_references = value
+ .references
+ .get_non_custom_dependencies(context.computed_context.is_root_element());
+ let has_custom_property_reference = value.references.any_var;
+ // Nothing to resolve.
+ if !has_custom_property_reference && non_custom_references.is_empty() {
+ debug_assert!(!value.references.any_env, "Should've been handled earlier");
+ return None;
+ }
+
+ // Has this variable been visited?
+ match context.index_map.entry(name.clone()) {
+ Entry::Occupied(entry) => {
+ return Some(*entry.get());
+ },
+ Entry::Vacant(entry) => {
+ entry.insert(context.count);
+ },
+ }
+ context.non_custom_references |= value.as_ref().references.non_custom_references;
+
+ // Hold a strong reference to the value so that we don't
+ // need to keep reference to context.map.
+ (Some(value.clone()), has_custom_property_reference)
+ },
+ VarType::NonCustom(ref non_custom) => {
+ let entry = &mut context.non_custom_index_map[*non_custom];
+ if let Some(v) = entry {
+ return Some(*v);
+ }
+ *entry = Some(context.count);
+ (None, false)
+ },
+ };
+
+ // Add new entry to the information table.
+ let index = context.count;
+ context.count += 1;
+ debug_assert_eq!(index, context.var_info.len());
+ context.var_info.push(VarInfo {
+ var: Some(var.clone()),
+ lowlink: index,
+ });
+ context.stack.push(index);
+
+ let mut self_ref = false;
+ let mut lowlink = index;
+ let visit_link =
+ |var: VarType, context: &mut Context, lowlink: &mut usize, self_ref: &mut bool| {
+ let next_index = match traverse(var, non_custom_references, context) {
+ Some(index) => index,
+ // There is nothing to do if the next variable has been
+ // fully resolved at this point.
+ None => {
+ return;
+ },
+ };
+ let next_info = &context.var_info[next_index];
+ if next_index > index {
+ // The next variable has a larger index than us, so it
+ // must be inserted in the recursive call above. We want
+ // to get its lowlink.
+ *lowlink = cmp::min(*lowlink, next_info.lowlink);
+ } else if next_index == index {
+ *self_ref = true;
+ } else if next_info.var.is_some() {
+ // The next variable has a smaller order index and it is
+ // in the stack, so we are at the same component.
+ *lowlink = cmp::min(*lowlink, next_index);
+ }
+ };
+ if let Some(ref v) = value.as_ref() {
+ debug_assert!(
+ matches!(var, VarType::Custom(_)),
+ "Non-custom property has references?"
+ );
+
+ // Visit other custom properties...
+ // FIXME: Maybe avoid visiting the same var twice if not needed?
+ for next in &v.references.refs {
+ if !next.is_var {
+ continue;
+ }
+ visit_link(
+ VarType::Custom(next.name.clone()),
+ context,
+ &mut lowlink,
+ &mut self_ref,
+ );
+ }
+
+ // ... Then non-custom properties.
+ v.references.non_custom_references.for_each(|r| {
+ visit_link(VarType::NonCustom(r), context, &mut lowlink, &mut self_ref);
+ });
+ } else if let VarType::NonCustom(non_custom) = var {
+ let entry = &non_custom_references[non_custom];
+ if let Some(deps) = entry.as_ref() {
+ for d in deps {
+ // Visit any reference from this non-custom property to custom properties.
+ visit_link(
+ VarType::Custom(d.clone()),
+ context,
+ &mut lowlink,
+ &mut self_ref,
+ );
+ }
+ }
+ }
+
+ context.var_info[index].lowlink = lowlink;
+ if lowlink != index {
+ // This variable is in a loop, but it is not the root of
+ // this strong connected component. We simply return for
+ // now, and the root would remove it from the map.
+ //
+ // This cannot be removed from the map here, because
+ // otherwise the shortcut check at the beginning of this
+ // function would return the wrong value.
+ return Some(index);
+ }
+
+ // This is the root of a strong-connected component.
+ let mut in_loop = self_ref;
+ let name;
+
+ let handle_variable_in_loop = |name: &Name, context: &mut Context<'a, 'b>| {
+ if context
+ .non_custom_references
+ .intersects(NonCustomReferences::FONT_UNITS | NonCustomReferences::ROOT_FONT_UNITS)
+ {
+ context
+ .invalid_non_custom_properties
+ .insert(LonghandId::FontSize);
+ }
+ if context.non_custom_references.intersects(
+ NonCustomReferences::LH_UNITS |
+ NonCustomReferences::ROOT_LH_UNITS,
+ ) {
+ context
+ .invalid_non_custom_properties
+ .insert(LonghandId::LineHeight);
+ }
+ // This variable is in loop. Resolve to invalid.
+ handle_invalid_at_computed_value_time(
+ name,
+ context.map,
+ context.computed_context.inherited_custom_properties(),
+ context.stylist,
+ context.computed_context.is_root_element(),
+ );
+ };
+ loop {
+ let var_index = context
+ .stack
+ .pop()
+ .expect("The current variable should still be in stack");
+ let var_info = &mut context.var_info[var_index];
+ // We should never visit the variable again, so it's safe
+ // to take the name away, so that we don't do additional
+ // reference count.
+ let var_name = var_info
+ .var
+ .take()
+ .expect("Variable should not be poped from stack twice");
+ if var_index == index {
+ name = match var_name {
+ VarType::Custom(name) => name,
+ // At the root of this component, and it's a non-custom
+ // reference - we have nothing to substitute, so
+ // it's effectively resolved.
+ VarType::NonCustom(..) => return None,
+ };
+ break;
+ }
+ if let VarType::Custom(name) = var_name {
+ // Anything here is in a loop which can traverse to the
+ // variable we are handling, so it's invalid at
+ // computed-value time.
+ handle_variable_in_loop(&name, context);
+ }
+ in_loop = true;
+ }
+ // We've gotten to the root of this strongly connected component, so clear
+ // whether or not it involved non-custom references.
+ // It's fine to track it like this, because non-custom properties currently
+ // being tracked can only participate in any loop only once.
+ if in_loop {
+ handle_variable_in_loop(&name, context);
+ context.non_custom_references = NonCustomReferences::default();
+ return None;
+ }
+
+ if let Some(ref v) = value.as_ref() {
+ let registration = context.stylist.get_custom_property_registration(&name);
+ let registered_length_property =
+ registration.syntax.may_reference_font_relative_length();
+ let mut defer = false;
+ if !context.non_custom_references.is_empty() && registered_length_property {
+ if let Some(deferred) = &mut context.deferred_properties {
+ // This property directly depends on a non-custom property, defer resolving it.
+ deferred.insert(registration, &name, (*v).clone());
+ context.map.remove(registration, &name);
+ defer = true;
+ }
+ }
+ if should_substitute && !defer {
+ for reference in v.references.refs.iter() {
+ if !reference.is_var {
+ continue;
+ }
+ if let Some(deferred) = &mut context.deferred_properties {
+ let registration =
+ context.stylist.get_custom_property_registration(&reference.name);
+ if deferred.get(registration, &reference.name).is_some() {
+ // This property depends on a custom property that depends on a non-custom property, defer.
+ deferred.insert(registration, &name, Arc::clone(v));
+ context.map.remove(registration, &name);
+ defer = true;
+ break;
+ }
+ }
+ }
+ if !defer {
+ substitute_references_if_needed_and_apply(
+ &name,
+ v,
+ &mut context.map,
+ context.stylist,
+ context.computed_context,
+ );
+ }
+ }
+ }
+ context.non_custom_references = NonCustomReferences::default();
+
+ // All resolved, so return the signal value.
+ None
+ }
+
+ // Note that `seen` doesn't contain names inherited from our parent, but
+ // those can't have variable references (since we inherit the computed
+ // variables) so we don't want to spend cycles traversing them anyway.
+ for name in seen {
+ let mut context = Context {
+ count: 0,
+ index_map: PrecomputedHashMap::default(),
+ non_custom_index_map: NonCustomReferenceMap::default(),
+ stack: SmallVec::new(),
+ var_info: SmallVec::new(),
+ map: custom_properties_map,
+ non_custom_references: NonCustomReferences::default(),
+ stylist,
+ computed_context,
+ invalid_non_custom_properties,
+ deferred_properties: deferred_properties_map.as_deref_mut(),
+ };
+ traverse(
+ VarType::Custom((*name).clone()),
+ references_from_non_custom_properties,
+ &mut context,
+ );
+ }
+}
+
+// See https://drafts.csswg.org/css-variables-2/#invalid-at-computed-value-time
+fn handle_invalid_at_computed_value_time(
+ name: &Name,
+ custom_properties: &mut ComputedCustomProperties,
+ inherited: &ComputedCustomProperties,
+ stylist: &Stylist,
+ is_root_element: bool,
+) {
+ let registration = stylist.get_custom_property_registration(&name);
+ if !registration.syntax.is_universal() {
+ // For the root element, inherited maps are empty. We should just
+ // use the initial value if any, rather than removing the name.
+ if registration.inherits() && !is_root_element {
+ if let Some(value) = inherited.get(registration, name) {
+ custom_properties.insert(registration, name, Arc::clone(value));
+ return;
+ }
+ } else {
+ if let Some(ref initial_value) = registration.initial_value {
+ custom_properties.insert(registration, name, Arc::clone(initial_value));
+ return;
+ }
+ }
+ }
+ custom_properties.remove(registration, name);
+}
+
+/// Replace `var()` and `env()` functions in a pre-existing variable value.
+fn substitute_references_if_needed_and_apply(
+ name: &Name,
+ value: &Arc<VariableValue>,
+ custom_properties: &mut ComputedCustomProperties,
+ stylist: &Stylist,
+ computed_context: &computed::Context,
+) {
+ let registration = stylist.get_custom_property_registration(&name);
+ if !value.has_references() && registration.syntax.is_universal() {
+ // Trivial path: no references and no need to compute the value, just apply it directly.
+ custom_properties.insert(registration, name, Arc::clone(value));
+ return;
+ }
+
+ let inherited = computed_context.inherited_custom_properties();
+ let value = match substitute_internal(value, custom_properties, stylist, registration, computed_context) {
+ Ok(v) => v,
+ Err(..) => {
+ handle_invalid_at_computed_value_time(
+ name,
+ custom_properties,
+ inherited,
+ stylist,
+ computed_context.is_root_element(),
+ );
+ return;
+ },
+ }.into_value(&value.url_data);
+
+ // If variable fallback results in a wide keyword, deal with it now.
+ {
+ let mut input = ParserInput::new(&value.css);
+ let mut input = Parser::new(&mut input);
+
+ if let Ok(kw) = input.try_parse(CSSWideKeyword::parse) {
+ // TODO: It's unclear what this should do for revert / revert-layer, see
+ // https://github.com/w3c/csswg-drafts/issues/9131. For now treating as unset
+ // seems fine?
+ match (kw, registration.inherits(), computed_context.is_root_element()) {
+ (CSSWideKeyword::Initial, _, _) |
+ (CSSWideKeyword::Revert, false, _) |
+ (CSSWideKeyword::RevertLayer, false, _) |
+ (CSSWideKeyword::Unset, false, _) |
+ (CSSWideKeyword::Revert, true, true) |
+ (CSSWideKeyword::RevertLayer, true, true) |
+ (CSSWideKeyword::Unset, true, true) |
+ (CSSWideKeyword::Inherit, _, true) => {
+ custom_properties.remove(registration, name);
+ if let Some(ref initial_value) = registration.initial_value {
+ custom_properties.insert(registration, name, Arc::clone(initial_value));
+ }
+ },
+ (CSSWideKeyword::Revert, true, false) |
+ (CSSWideKeyword::RevertLayer, true, false) |
+ (CSSWideKeyword::Inherit, _, false) |
+ (CSSWideKeyword::Unset, true, false) => {
+ match inherited.get(registration, name) {
+ Some(value) => {
+ custom_properties.insert(registration, name, Arc::clone(value));
+ },
+ None => {
+ custom_properties.remove(registration, name);
+ },
+ };
+ },
+ }
+ return;
+ }
+ }
+
+ custom_properties.insert(registration, name, Arc::new(value));
+}
+
+#[derive(Default)]
+struct Substitution<'a> {
+ css: Cow<'a, str>,
+ first_token_type: TokenSerializationType,
+ last_token_type: TokenSerializationType,
+}
+
+impl<'a> Substitution<'a> {
+ fn new(
+ css: &'a str,
+ first_token_type: TokenSerializationType,
+ last_token_type: TokenSerializationType,
+ ) -> Self {
+ Self {
+ css: Cow::Borrowed(css),
+ first_token_type,
+ last_token_type,
+ }
+ }
+
+ fn from_value(v: VariableValue) -> Substitution<'static> {
+ debug_assert!(!v.has_references(), "Computed values shouldn't have references");
+ Substitution {
+ css: Cow::from(v.css),
+ first_token_type: v.first_token_type,
+ last_token_type: v.last_token_type,
+ }
+ }
+
+ fn into_value(self, url_data: &UrlExtraData) -> VariableValue {
+ VariableValue {
+ css: self.css.into_owned(),
+ first_token_type: self.first_token_type,
+ last_token_type: self.last_token_type,
+ url_data: url_data.clone(),
+ references: Default::default(),
+ }
+ }
+}
+
+fn compute_value(
+ css: &str,
+ url_data: &UrlExtraData,
+ registration: &PropertyRegistrationData,
+ computed_context: &computed::Context,
+) -> Result<Substitution<'static>, ()> {
+ debug_assert!(!registration.syntax.is_universal());
+
+ let mut input = ParserInput::new(&css);
+ let mut input = Parser::new(&mut input);
+
+ let value = SpecifiedRegisteredValue::compute(
+ &mut input,
+ registration,
+ url_data,
+ computed_context,
+ AllowComputationallyDependent::Yes,
+ )?;
+ Ok(Substitution::from_value(value))
+}
+
+fn do_substitute_chunk<'a>(
+ css: &'a str,
+ start: usize,
+ end: usize,
+ first_token_type: TokenSerializationType,
+ last_token_type: TokenSerializationType,
+ url_data: &UrlExtraData,
+ custom_properties: &'a ComputedCustomProperties,
+ registration: &PropertyRegistrationData,
+ stylist: &Stylist,
+ computed_context: &computed::Context,
+ references: &mut std::iter::Peekable<std::slice::Iter<VarOrEnvReference>>,
+) -> Result<Substitution<'a>, ()> {
+ if start == end {
+ // Empty string. Easy.
+ return Ok(Substitution::default());
+ }
+ // Easy case: no references involved.
+ if references
+ .peek()
+ .map_or(true, |reference| reference.end > end)
+ {
+ let result = &css[start..end];
+ if !registration.syntax.is_universal() {
+ return compute_value(result, url_data, registration, computed_context);
+ }
+ return Ok(Substitution::new(result, first_token_type, last_token_type));
+ }
+
+ let mut substituted = ComputedValue::empty(url_data);
+ let mut next_token_type = first_token_type;
+ let mut cur_pos = start;
+ while let Some(reference) = references.next_if(|reference| reference.end <= end) {
+ if reference.start != cur_pos {
+ substituted.push(
+ &css[cur_pos..reference.start],
+ next_token_type,
+ reference.prev_token_type,
+ )?;
+ }
+
+ let substitution = substitute_one_reference(
+ css,
+ url_data,
+ custom_properties,
+ reference,
+ stylist,
+ computed_context,
+ references,
+ )?;
+
+ // Optimize the property: var(--...) case to avoid allocating at all.
+ if reference.start == start && reference.end == end && registration.syntax.is_universal() {
+ return Ok(substitution);
+ }
+
+ substituted.push(
+ &substitution.css,
+ substitution.first_token_type,
+ substitution.last_token_type,
+ )?;
+ next_token_type = reference.next_token_type;
+ cur_pos = reference.end;
+ }
+ // Push the rest of the value if needed.
+ if cur_pos != end {
+ substituted.push(&css[cur_pos..end], next_token_type, last_token_type)?;
+ }
+ if !registration.syntax.is_universal() {
+ return compute_value(&substituted.css, url_data, registration, computed_context);
+ }
+ Ok(Substitution::from_value(substituted))
+}
+
+fn substitute_one_reference<'a>(
+ css: &'a str,
+ url_data: &UrlExtraData,
+ custom_properties: &'a ComputedCustomProperties,
+ reference: &VarOrEnvReference,
+ stylist: &Stylist,
+ computed_context: &computed::Context,
+ references: &mut std::iter::Peekable<std::slice::Iter<VarOrEnvReference>>,
+) -> Result<Substitution<'a>, ()> {
+ let registration;
+ if reference.is_var {
+ registration = stylist.get_custom_property_registration(&reference.name);
+ if let Some(v) = custom_properties.get(registration, &reference.name) {
+ debug_assert!(!v.has_references(), "Should be already computed");
+ if registration.syntax.is_universal() {
+ // Skip references that are inside the outer variable (in fallback for example).
+ while references
+ .next_if(|next_ref| next_ref.end <= reference.end)
+ .is_some()
+ {}
+ } else {
+ // We need to validate the fallback if any, since invalid fallback should
+ // invalidate the whole variable.
+ if let Some(ref fallback) = reference.fallback {
+ let _ = do_substitute_chunk(
+ css,
+ fallback.start.get(),
+ reference.end - 1, // Don't include the closing parenthesis.
+ fallback.first_token_type,
+ fallback.last_token_type,
+ url_data,
+ custom_properties,
+ registration,
+ stylist,
+ computed_context,
+ references,
+ )?;
+ }
+ }
+ return Ok(Substitution {
+ css: Cow::from(&v.css),
+ first_token_type: v.first_token_type,
+ last_token_type: v.last_token_type,
+ });
+ }
+ } else {
+ registration = PropertyRegistrationData::unregistered();
+ let device = stylist.device();
+ if let Some(v) = device.environment().get(&reference.name, device, url_data) {
+ while references
+ .next_if(|next_ref| next_ref.end <= reference.end)
+ .is_some()
+ {}
+ return Ok(Substitution::from_value(v));
+ }
+ }
+
+ let Some(ref fallback) = reference.fallback else { return Err(()) };
+
+ do_substitute_chunk(
+ css,
+ fallback.start.get(),
+ reference.end - 1, // Skip the closing parenthesis of the reference value.
+ fallback.first_token_type,
+ fallback.last_token_type,
+ url_data,
+ custom_properties,
+ registration,
+ stylist,
+ computed_context,
+ references,
+ )
+}
+
+/// Replace `var()` and `env()` functions. Return `Err(..)` for invalid at computed time.
+fn substitute_internal<'a>(
+ variable_value: &'a VariableValue,
+ custom_properties: &'a ComputedCustomProperties,
+ stylist: &Stylist,
+ registration: &PropertyRegistrationData,
+ computed_context: &computed::Context,
+) -> Result<Substitution<'a>, ()> {
+ let mut refs = variable_value.references.refs.iter().peekable();
+ do_substitute_chunk(
+ &variable_value.css,
+ /* start = */ 0,
+ /* end = */ variable_value.css.len(),
+ variable_value.first_token_type,
+ variable_value.last_token_type,
+ &variable_value.url_data,
+ custom_properties,
+ registration,
+ stylist,
+ computed_context,
+ &mut refs,
+ )
+}
+
+/// Replace var() and env() functions, returning the resulting CSS string.
+pub fn substitute<'a>(
+ variable_value: &'a VariableValue,
+ custom_properties: &'a ComputedCustomProperties,
+ stylist: &Stylist,
+ computed_context: &computed::Context,
+) -> Result<Cow<'a, str>, ()> {
+ debug_assert!(variable_value.has_references());
+ let v = substitute_internal(
+ variable_value,
+ custom_properties,
+ stylist,
+ PropertyRegistrationData::unregistered(),
+ computed_context,
+ )?;
+ Ok(v.css)
+}
diff --git a/servo/components/style/custom_properties_map.rs b/servo/components/style/custom_properties_map.rs
new file mode 100644
index 0000000000..04ca8e1b3d
--- /dev/null
+++ b/servo/components/style/custom_properties_map.rs
@@ -0,0 +1,237 @@
+/* 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/. */
+
+//! The structure that contains the custom properties of a given element.
+
+use crate::custom_properties::{Name, VariableValue};
+use crate::selector_map::PrecomputedHasher;
+use indexmap::IndexMap;
+use servo_arc::Arc;
+use std::hash::BuildHasherDefault;
+
+/// A map for a set of custom properties, which implements copy-on-write behavior on insertion with
+/// cheap copying.
+#[derive(Clone, Debug, PartialEq)]
+pub struct CustomPropertiesMap(Arc<Inner>);
+
+impl Default for CustomPropertiesMap {
+ fn default() -> Self {
+ Self(EMPTY.clone())
+ }
+}
+
+/// We use None in the value to represent a removed entry.
+type OwnMap = IndexMap<Name, Option<Arc<VariableValue>>, BuildHasherDefault<PrecomputedHasher>>;
+
+// IndexMap equality doesn't consider ordering, which we want to account for. Also, for the same
+// reason, IndexMap equality comparisons are slower than needed.
+//
+// See https://github.com/bluss/indexmap/issues/153.
+// TODO: use as_slice when updating to indexmap 2.0.
+fn maps_equal(l: &OwnMap, r: &OwnMap) -> bool {
+ if std::ptr::eq(l, r) {
+ return true;
+ }
+ if l.len() != r.len() {
+ return false;
+ }
+ l.iter()
+ .zip(r.iter())
+ .all(|((k1, v1), (k2, v2))| k1 == k2 && v1 == v2)
+}
+
+lazy_static! {
+ static ref EMPTY: Arc<Inner> = {
+ Arc::new_leaked(Inner {
+ own_properties: Default::default(),
+ parent: None,
+ len: 0,
+ ancestor_count: 0,
+ })
+ };
+}
+
+#[derive(Debug, Clone)]
+struct Inner {
+ own_properties: OwnMap,
+ parent: Option<Arc<Inner>>,
+ /// The number of custom properties we store. Note that this is different from the sum of our
+ /// own and our parent's length, since we might store duplicate entries.
+ len: usize,
+ /// The number of ancestors we have.
+ ancestor_count: u8,
+}
+
+/// A not-too-large, not too small ancestor limit, to prevent creating too-big chains.
+const ANCESTOR_COUNT_LIMIT: usize = 4;
+
+/// An iterator over the custom properties.
+pub struct Iter<'a> {
+ current: &'a Inner,
+ current_iter: indexmap::map::Iter<'a, Name, Option<Arc<VariableValue>>>,
+ descendants: smallvec::SmallVec<[&'a Inner; ANCESTOR_COUNT_LIMIT]>,
+}
+
+impl<'a> Iterator for Iter<'a> {
+ type Item = (&'a Name, &'a Option<Arc<VariableValue>>);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ let (name, value) = match self.current_iter.next() {
+ Some(v) => v,
+ None => {
+ let parent = self.current.parent.as_deref()?;
+ self.descendants.push(self.current);
+ self.current = parent;
+ self.current_iter = parent.own_properties.iter();
+ continue;
+ },
+ };
+ // If the property is overridden by a descendant we've already visited it.
+ for descendant in &self.descendants {
+ if descendant.own_properties.contains_key(name) {
+ continue;
+ }
+ }
+ return Some((name, value));
+ }
+ }
+}
+
+impl PartialEq for Inner {
+ fn eq(&self, other: &Self) -> bool {
+ if self.len != other.len {
+ return false;
+ }
+ if self.parent_ptr_eq(other) {
+ return maps_equal(&self.own_properties, &other.own_properties);
+ }
+ for (name, value) in self.iter() {
+ if other.get(name) != value.as_ref() {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+impl Inner {
+ fn parent_ptr_eq(&self, other: &Self) -> bool {
+ match (&self.parent, &other.parent) {
+ (Some(p1), Some(p2)) => Arc::ptr_eq(p1, p2),
+ (None, None) => true,
+ _ => false,
+ }
+ }
+
+ fn iter(&self) -> Iter {
+ Iter {
+ current: self,
+ current_iter: self.own_properties.iter(),
+ descendants: Default::default(),
+ }
+ }
+
+ fn is_empty(&self) -> bool {
+ self.len == 0
+ }
+
+ fn len(&self) -> usize {
+ self.len
+ }
+
+ fn get(&self, name: &Name) -> Option<&Arc<VariableValue>> {
+ if let Some(p) = self.own_properties.get(name) {
+ return p.as_ref();
+ }
+ self.parent.as_ref()?.get(name)
+ }
+
+ fn insert(&mut self, name: &Name, value: Option<Arc<VariableValue>>) {
+ let new = self.own_properties.insert(name.clone(), value).is_none();
+ if new && self.parent.as_ref().map_or(true, |p| p.get(name).is_none()) {
+ self.len += 1;
+ }
+ }
+
+ /// Whether we should expand the chain, or just copy-on-write.
+ fn should_expand_chain(&self) -> bool {
+ const SMALL_THRESHOLD: usize = 8;
+ if self.own_properties.len() <= SMALL_THRESHOLD {
+ return false; // Just copy, to avoid very long chains.
+ }
+ self.ancestor_count < ANCESTOR_COUNT_LIMIT as u8
+ }
+}
+
+impl CustomPropertiesMap {
+ /// Returns whether the map has no properties in it.
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ /// Returns the amount of different properties in the map.
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
+ /// Returns the property name and value at a given index.
+ pub fn get_index(&self, index: usize) -> Option<(&Name, &Option<Arc<VariableValue>>)> {
+ if index >= self.len() {
+ return None;
+ }
+ // FIXME: This is O(n) which is a bit unfortunate.
+ self.0.iter().nth(index)
+ }
+
+ /// Returns a given property value by name.
+ pub fn get(&self, name: &Name) -> Option<&Arc<VariableValue>> {
+ self.0.get(name)
+ }
+
+ fn do_insert(&mut self, name: &Name, value: Option<Arc<VariableValue>>) {
+ if let Some(inner) = Arc::get_mut(&mut self.0) {
+ return inner.insert(name, value);
+ }
+ if self.get(name) == value.as_ref() {
+ return;
+ }
+ if !self.0.should_expand_chain() {
+ return Arc::make_mut(&mut self.0).insert(name, value);
+ }
+ let len = self.0.len;
+ let ancestor_count = self.0.ancestor_count + 1;
+ let mut new_inner = Inner {
+ own_properties: Default::default(),
+ // FIXME: Would be nice to avoid this clone.
+ parent: Some(self.0.clone()),
+ len,
+ ancestor_count,
+ };
+ new_inner.insert(name, value);
+ self.0 = Arc::new(new_inner);
+ }
+
+ /// Inserts an element in the map.
+ pub fn insert(&mut self, name: &Name, value: Arc<VariableValue>) {
+ self.do_insert(name, Some(value))
+ }
+
+ /// Removes an element from the map.
+ pub fn remove(&mut self, name: &Name) {
+ self.do_insert(name, None)
+ }
+
+ /// Shrinks the map as much as possible.
+ pub fn shrink_to_fit(&mut self) {
+ if let Some(inner) = Arc::get_mut(&mut self.0) {
+ inner.own_properties.shrink_to_fit()
+ }
+ }
+
+ /// Return iterator to go through all properties.
+ pub fn iter(&self) -> Iter {
+ self.0.iter()
+ }
+}
diff --git a/servo/components/style/data.rs b/servo/components/style/data.rs
new file mode 100644
index 0000000000..ceddc5bd20
--- /dev/null
+++ b/servo/components/style/data.rs
@@ -0,0 +1,545 @@
+/* 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/. */
+
+//! Per-node data used in style calculation.
+
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::context::{SharedStyleContext, StackLimitChecker};
+use crate::dom::TElement;
+use crate::invalidation::element::invalidator::InvalidationResult;
+use crate::invalidation::element::restyle_hints::RestyleHint;
+use crate::properties::ComputedValues;
+use crate::selector_parser::{PseudoElement, RestyleDamage, EAGER_PSEUDO_COUNT};
+use crate::style_resolver::{PrimaryStyle, ResolvedElementStyles, ResolvedStyle};
+#[cfg(feature = "gecko")]
+use malloc_size_of::MallocSizeOfOps;
+use selectors::matching::SelectorCaches;
+use servo_arc::Arc;
+use std::fmt;
+use std::mem;
+use std::ops::{Deref, DerefMut};
+
+bitflags! {
+ /// Various flags stored on ElementData.
+ #[derive(Debug, Default)]
+ pub struct ElementDataFlags: u8 {
+ /// Whether the styles changed for this restyle.
+ const WAS_RESTYLED = 1 << 0;
+ /// Whether the last traversal of this element did not do
+ /// any style computation. This is not true during the initial
+ /// styling pass, nor is it true when we restyle (in which case
+ /// WAS_RESTYLED is set).
+ ///
+ /// This bit always corresponds to the last time the element was
+ /// traversed, so each traversal simply updates it with the appropriate
+ /// value.
+ const TRAVERSED_WITHOUT_STYLING = 1 << 1;
+
+ /// Whether the primary style of this element data was reused from
+ /// another element via a rule node comparison. This allows us to
+ /// differentiate between elements that shared styles because they met
+ /// all the criteria of the style sharing cache, compared to elements
+ /// that reused style structs via rule node identity.
+ ///
+ /// The former gives us stronger transitive guarantees that allows us to
+ /// apply the style sharing cache to cousins.
+ const PRIMARY_STYLE_REUSED_VIA_RULE_NODE = 1 << 2;
+ }
+}
+
+/// A lazily-allocated list of styles for eagerly-cascaded pseudo-elements.
+///
+/// We use an Arc so that sharing these styles via the style sharing cache does
+/// not require duplicate allocations. We leverage the copy-on-write semantics of
+/// Arc::make_mut(), which is free (i.e. does not require atomic RMU operations)
+/// in servo_arc.
+#[derive(Clone, Debug, Default)]
+pub struct EagerPseudoStyles(Option<Arc<EagerPseudoArray>>);
+
+#[derive(Default)]
+struct EagerPseudoArray(EagerPseudoArrayInner);
+type EagerPseudoArrayInner = [Option<Arc<ComputedValues>>; EAGER_PSEUDO_COUNT];
+
+impl Deref for EagerPseudoArray {
+ type Target = EagerPseudoArrayInner;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for EagerPseudoArray {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+// Manually implement `Clone` here because the derived impl of `Clone` for
+// array types assumes the value inside is `Copy`.
+impl Clone for EagerPseudoArray {
+ fn clone(&self) -> Self {
+ let mut clone = Self::default();
+ for i in 0..EAGER_PSEUDO_COUNT {
+ clone[i] = self.0[i].clone();
+ }
+ clone
+ }
+}
+
+// Override Debug to print which pseudos we have, and substitute the rule node
+// for the much-more-verbose ComputedValues stringification.
+impl fmt::Debug for EagerPseudoArray {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "EagerPseudoArray {{ ")?;
+ for i in 0..EAGER_PSEUDO_COUNT {
+ if let Some(ref values) = self[i] {
+ write!(
+ f,
+ "{:?}: {:?}, ",
+ PseudoElement::from_eager_index(i),
+ &values.rules
+ )?;
+ }
+ }
+ write!(f, "}}")
+ }
+}
+
+// Can't use [None; EAGER_PSEUDO_COUNT] here because it complains
+// about Copy not being implemented for our Arc type.
+#[cfg(feature = "gecko")]
+const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None, None];
+#[cfg(feature = "servo")]
+const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None];
+
+impl EagerPseudoStyles {
+ /// Returns whether there are any pseudo styles.
+ pub fn is_empty(&self) -> bool {
+ self.0.is_none()
+ }
+
+ /// Grabs a reference to the list of styles, if they exist.
+ pub fn as_optional_array(&self) -> Option<&EagerPseudoArrayInner> {
+ match self.0 {
+ None => None,
+ Some(ref x) => Some(&x.0),
+ }
+ }
+
+ /// Grabs a reference to the list of styles or a list of None if
+ /// there are no styles to be had.
+ pub fn as_array(&self) -> &EagerPseudoArrayInner {
+ self.as_optional_array().unwrap_or(EMPTY_PSEUDO_ARRAY)
+ }
+
+ /// Returns a reference to the style for a given eager pseudo, if it exists.
+ pub fn get(&self, pseudo: &PseudoElement) -> Option<&Arc<ComputedValues>> {
+ debug_assert!(pseudo.is_eager());
+ self.0
+ .as_ref()
+ .and_then(|p| p[pseudo.eager_index()].as_ref())
+ }
+
+ /// Sets the style for the eager pseudo.
+ pub fn set(&mut self, pseudo: &PseudoElement, value: Arc<ComputedValues>) {
+ if self.0.is_none() {
+ self.0 = Some(Arc::new(Default::default()));
+ }
+ let arr = Arc::make_mut(self.0.as_mut().unwrap());
+ arr[pseudo.eager_index()] = Some(value);
+ }
+}
+
+/// The styles associated with a node, including the styles for any
+/// pseudo-elements.
+#[derive(Clone, Default)]
+pub struct ElementStyles {
+ /// The element's style.
+ pub primary: Option<Arc<ComputedValues>>,
+ /// A list of the styles for the element's eagerly-cascaded pseudo-elements.
+ pub pseudos: EagerPseudoStyles,
+}
+
+// There's one of these per rendered elements so it better be small.
+size_of_test!(ElementStyles, 16);
+
+/// Information on how this element uses viewport units.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ViewportUnitUsage {
+ /// No viewport units are used.
+ None = 0,
+ /// There are viewport units used from regular style rules (which means we
+ /// should re-cascade).
+ FromDeclaration,
+ /// There are viewport units used from container queries (which means we
+ /// need to re-selector-match).
+ FromQuery,
+}
+
+impl ElementStyles {
+ /// Returns the primary style.
+ pub fn get_primary(&self) -> Option<&Arc<ComputedValues>> {
+ self.primary.as_ref()
+ }
+
+ /// Returns the primary style. Panic if no style available.
+ pub fn primary(&self) -> &Arc<ComputedValues> {
+ self.primary.as_ref().unwrap()
+ }
+
+ /// Whether this element `display` value is `none`.
+ pub fn is_display_none(&self) -> bool {
+ self.primary().get_box().clone_display().is_none()
+ }
+
+ /// Whether this element uses viewport units.
+ pub fn viewport_unit_usage(&self) -> ViewportUnitUsage {
+ fn usage_from_flags(flags: ComputedValueFlags) -> ViewportUnitUsage {
+ if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES) {
+ return ViewportUnitUsage::FromQuery;
+ }
+ if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS) {
+ return ViewportUnitUsage::FromDeclaration;
+ }
+ ViewportUnitUsage::None
+ }
+
+ let mut usage = usage_from_flags(self.primary().flags);
+ for pseudo_style in self.pseudos.as_array() {
+ if let Some(ref pseudo_style) = pseudo_style {
+ usage = std::cmp::max(usage, usage_from_flags(pseudo_style.flags));
+ }
+ }
+
+ usage
+ }
+
+ #[cfg(feature = "gecko")]
+ fn size_of_excluding_cvs(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ // As the method name suggests, we don't measures the ComputedValues
+ // here, because they are measured on the C++ side.
+
+ // XXX: measure the EagerPseudoArray itself, but not the ComputedValues
+ // within it.
+
+ 0
+ }
+}
+
+// We manually implement Debug for ElementStyles so that we can avoid the
+// verbose stringification of every property in the ComputedValues. We
+// substitute the rule node instead.
+impl fmt::Debug for ElementStyles {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "ElementStyles {{ primary: {:?}, pseudos: {:?} }}",
+ self.primary.as_ref().map(|x| &x.rules),
+ self.pseudos
+ )
+ }
+}
+
+/// Style system data associated with an Element.
+///
+/// In Gecko, this hangs directly off the Element. Servo, this is embedded
+/// inside of layout data, which itself hangs directly off the Element. In
+/// both cases, it is wrapped inside an AtomicRefCell to ensure thread safety.
+#[derive(Debug, Default)]
+pub struct ElementData {
+ /// The styles for the element and its pseudo-elements.
+ pub styles: ElementStyles,
+
+ /// The restyle damage, indicating what kind of layout changes are required
+ /// afte restyling.
+ pub damage: RestyleDamage,
+
+ /// The restyle hint, which indicates whether selectors need to be rematched
+ /// for this element, its children, and its descendants.
+ pub hint: RestyleHint,
+
+ /// Flags.
+ pub flags: ElementDataFlags,
+}
+
+// There's one of these per rendered elements so it better be small.
+size_of_test!(ElementData, 24);
+
+/// The kind of restyle that a single element should do.
+#[derive(Debug)]
+pub enum RestyleKind {
+ /// We need to run selector matching plus re-cascade, that is, a full
+ /// restyle.
+ MatchAndCascade,
+ /// We need to recascade with some replacement rule, such as the style
+ /// attribute, or animation rules.
+ CascadeWithReplacements(RestyleHint),
+ /// We only need to recascade, for example, because only inherited
+ /// properties in the parent changed.
+ CascadeOnly,
+}
+
+impl ElementData {
+ /// Invalidates style for this element, its descendants, and later siblings,
+ /// based on the snapshot of the element that we took when attributes or
+ /// state changed.
+ pub fn invalidate_style_if_needed<'a, E: TElement>(
+ &mut self,
+ element: E,
+ shared_context: &SharedStyleContext,
+ stack_limit_checker: Option<&StackLimitChecker>,
+ selector_caches: &'a mut SelectorCaches,
+ ) -> InvalidationResult {
+ // In animation-only restyle we shouldn't touch snapshot at all.
+ if shared_context.traversal_flags.for_animation_only() {
+ return InvalidationResult::empty();
+ }
+
+ use crate::invalidation::element::invalidator::TreeStyleInvalidator;
+ use crate::invalidation::element::state_and_attributes::StateAndAttrInvalidationProcessor;
+
+ debug!(
+ "invalidate_style_if_needed: {:?}, flags: {:?}, has_snapshot: {}, \
+ handled_snapshot: {}, pseudo: {:?}",
+ element,
+ shared_context.traversal_flags,
+ element.has_snapshot(),
+ element.handled_snapshot(),
+ element.implemented_pseudo_element()
+ );
+
+ if !element.has_snapshot() || element.handled_snapshot() {
+ return InvalidationResult::empty();
+ }
+
+ let mut processor =
+ StateAndAttrInvalidationProcessor::new(shared_context, element, self, selector_caches);
+
+ let invalidator = TreeStyleInvalidator::new(element, stack_limit_checker, &mut processor);
+
+ let result = invalidator.invalidate();
+
+ unsafe { element.set_handled_snapshot() }
+ debug_assert!(element.handled_snapshot());
+
+ result
+ }
+
+ /// Returns true if this element has styles.
+ #[inline]
+ pub fn has_styles(&self) -> bool {
+ self.styles.primary.is_some()
+ }
+
+ /// Returns this element's styles as resolved styles to use for sharing.
+ pub fn share_styles(&self) -> ResolvedElementStyles {
+ ResolvedElementStyles {
+ primary: self.share_primary_style(),
+ pseudos: self.styles.pseudos.clone(),
+ }
+ }
+
+ /// Returns this element's primary style as a resolved style to use for sharing.
+ pub fn share_primary_style(&self) -> PrimaryStyle {
+ let reused_via_rule_node = self
+ .flags
+ .contains(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE);
+
+ PrimaryStyle {
+ style: ResolvedStyle(self.styles.primary().clone()),
+ reused_via_rule_node,
+ }
+ }
+
+ /// Sets a new set of styles, returning the old ones.
+ pub fn set_styles(&mut self, new_styles: ResolvedElementStyles) -> ElementStyles {
+ if new_styles.primary.reused_via_rule_node {
+ self.flags
+ .insert(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE);
+ } else {
+ self.flags
+ .remove(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE);
+ }
+ mem::replace(&mut self.styles, new_styles.into())
+ }
+
+ /// Returns the kind of restyling that we're going to need to do on this
+ /// element, based of the stored restyle hint.
+ pub fn restyle_kind(&self, shared_context: &SharedStyleContext) -> Option<RestyleKind> {
+ if shared_context.traversal_flags.for_animation_only() {
+ return self.restyle_kind_for_animation(shared_context);
+ }
+
+ let style = match self.styles.primary {
+ Some(ref s) => s,
+ None => return Some(RestyleKind::MatchAndCascade),
+ };
+
+ let hint = self.hint;
+ if hint.is_empty() {
+ return None;
+ }
+
+ let needs_to_match_self = hint.intersects(RestyleHint::RESTYLE_SELF) ||
+ (hint.intersects(RestyleHint::RESTYLE_SELF_IF_PSEUDO) && style.is_pseudo_style());
+ if needs_to_match_self {
+ return Some(RestyleKind::MatchAndCascade);
+ }
+
+ if hint.has_replacements() {
+ debug_assert!(
+ !hint.has_animation_hint(),
+ "Animation only restyle hint should have already processed"
+ );
+ return Some(RestyleKind::CascadeWithReplacements(
+ hint & RestyleHint::replacements(),
+ ));
+ }
+
+ let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) ||
+ (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) &&
+ style
+ .flags
+ .contains(ComputedValueFlags::INHERITS_RESET_STYLE));
+ if needs_to_recascade_self {
+ return Some(RestyleKind::CascadeOnly);
+ }
+
+ None
+ }
+
+ /// Returns the kind of restyling for animation-only restyle.
+ fn restyle_kind_for_animation(
+ &self,
+ shared_context: &SharedStyleContext,
+ ) -> Option<RestyleKind> {
+ debug_assert!(shared_context.traversal_flags.for_animation_only());
+ debug_assert!(
+ self.has_styles(),
+ "animation traversal doesn't care about unstyled elements"
+ );
+
+ // FIXME: We should ideally restyle here, but it is a hack to work around our weird
+ // animation-only traversal stuff: If we're display: none and the rules we could
+ // match could change, we consider our style up-to-date. This is because re-cascading with
+ // and old style doesn't guarantee returning the correct animation style (that's
+ // bug 1393323). So if our display changed, and it changed from display: none, we would
+ // incorrectly forget about it and wouldn't be able to correctly style our descendants
+ // later.
+ // XXX Figure out if this still makes sense.
+ let hint = self.hint;
+ if self.styles.is_display_none() && hint.intersects(RestyleHint::RESTYLE_SELF) {
+ return None;
+ }
+
+ let style = self.styles.primary();
+ // Return either CascadeWithReplacements or CascadeOnly in case of
+ // animation-only restyle. I.e. animation-only restyle never does
+ // selector matching.
+ if hint.has_animation_hint() {
+ return Some(RestyleKind::CascadeWithReplacements(
+ hint & RestyleHint::for_animations(),
+ ));
+ }
+
+ let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) ||
+ (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) &&
+ style
+ .flags
+ .contains(ComputedValueFlags::INHERITS_RESET_STYLE));
+ if needs_to_recascade_self {
+ return Some(RestyleKind::CascadeOnly);
+ }
+ return None;
+ }
+
+ /// Drops any restyle state from the element.
+ ///
+ /// FIXME(bholley): The only caller of this should probably just assert that
+ /// the hint is empty and call clear_flags_and_damage().
+ #[inline]
+ pub fn clear_restyle_state(&mut self) {
+ self.hint = RestyleHint::empty();
+ self.clear_restyle_flags_and_damage();
+ }
+
+ /// Drops restyle flags and damage from the element.
+ #[inline]
+ pub fn clear_restyle_flags_and_damage(&mut self) {
+ self.damage = RestyleDamage::empty();
+ self.flags.remove(ElementDataFlags::WAS_RESTYLED);
+ }
+
+ /// Mark this element as restyled, which is useful to know whether we need
+ /// to do a post-traversal.
+ pub fn set_restyled(&mut self) {
+ self.flags.insert(ElementDataFlags::WAS_RESTYLED);
+ self.flags
+ .remove(ElementDataFlags::TRAVERSED_WITHOUT_STYLING);
+ }
+
+ /// Returns true if this element was restyled.
+ #[inline]
+ pub fn is_restyle(&self) -> bool {
+ self.flags.contains(ElementDataFlags::WAS_RESTYLED)
+ }
+
+ /// Mark that we traversed this element without computing any style for it.
+ pub fn set_traversed_without_styling(&mut self) {
+ self.flags
+ .insert(ElementDataFlags::TRAVERSED_WITHOUT_STYLING);
+ }
+
+ /// Returns whether this element has been part of a restyle.
+ #[inline]
+ pub fn contains_restyle_data(&self) -> bool {
+ self.is_restyle() || !self.hint.is_empty() || !self.damage.is_empty()
+ }
+
+ /// Returns whether it is safe to perform cousin sharing based on the ComputedValues
+ /// identity of the primary style in this ElementData. There are a few subtle things
+ /// to check.
+ ///
+ /// First, if a parent element was already styled and we traversed past it without
+ /// restyling it, that may be because our clever invalidation logic was able to prove
+ /// that the styles of that element would remain unchanged despite changes to the id
+ /// or class attributes. However, style sharing relies on the strong guarantee that all
+ /// the classes and ids up the respective parent chains are identical. As such, if we
+ /// skipped styling for one (or both) of the parents on this traversal, we can't share
+ /// styles across cousins. Note that this is a somewhat conservative check. We could
+ /// tighten it by having the invalidation logic explicitly flag elements for which it
+ /// ellided styling.
+ ///
+ /// Second, we want to only consider elements whose ComputedValues match due to a hit
+ /// in the style sharing cache, rather than due to the rule-node-based reuse that
+ /// happens later in the styling pipeline. The former gives us the stronger guarantees
+ /// we need for style sharing, the latter does not.
+ pub fn safe_for_cousin_sharing(&self) -> bool {
+ if self.flags.intersects(
+ ElementDataFlags::TRAVERSED_WITHOUT_STYLING |
+ ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE,
+ ) {
+ return false;
+ }
+ if !self
+ .styles
+ .primary()
+ .get_box()
+ .clone_container_type()
+ .is_normal()
+ {
+ return false;
+ }
+ true
+ }
+
+ /// Measures memory usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of_excluding_cvs(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let n = self.styles.size_of_excluding_cvs(ops);
+
+ // We may measure more fields in the future if DMD says it's worth it.
+
+ n
+ }
+}
diff --git a/servo/components/style/dom.rs b/servo/components/style/dom.rs
new file mode 100644
index 0000000000..554d79fdb3
--- /dev/null
+++ b/servo/components/style/dom.rs
@@ -0,0 +1,951 @@
+/* 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/. */
+
+//! Types and traits used to access the DOM from style calculation.
+
+#![allow(unsafe_code)]
+#![deny(missing_docs)]
+
+use crate::applicable_declarations::ApplicableDeclarationBlock;
+use crate::context::SharedStyleContext;
+#[cfg(feature = "gecko")]
+use crate::context::{PostAnimationTasks, UpdateAnimationsTasks};
+use crate::data::ElementData;
+use crate::media_queries::Device;
+use crate::properties::{AnimationDeclarations, ComputedValues, PropertyDeclarationBlock};
+use crate::selector_parser::{AttrValue, CustomState, Lang, PseudoElement, SelectorImpl};
+use crate::shared_lock::{Locked, SharedRwLock};
+use crate::stylist::CascadeData;
+use crate::values::computed::Display;
+use crate::values::AtomIdent;
+use crate::WeakAtom;
+use atomic_refcell::{AtomicRef, AtomicRefMut};
+use dom::ElementState;
+use selectors::matching::{ElementSelectorFlags, QuirksMode, VisitedHandlingMode};
+use selectors::sink::Push;
+use selectors::Element as SelectorsElement;
+use servo_arc::{Arc, ArcBorrow};
+use std::fmt;
+use std::fmt::Debug;
+use std::hash::Hash;
+use std::ops::Deref;
+
+pub use style_traits::dom::OpaqueNode;
+
+/// Simple trait to provide basic information about the type of an element.
+///
+/// We avoid exposing the full type id, since computing it in the general case
+/// would be difficult for Gecko nodes.
+pub trait NodeInfo {
+ /// Whether this node is an element.
+ fn is_element(&self) -> bool;
+ /// Whether this node is a text node.
+ fn is_text_node(&self) -> bool;
+}
+
+/// A node iterator that only returns node that don't need layout.
+pub struct LayoutIterator<T>(pub T);
+
+impl<T, N> Iterator for LayoutIterator<T>
+where
+ T: Iterator<Item = N>,
+ N: NodeInfo,
+{
+ type Item = N;
+
+ fn next(&mut self) -> Option<N> {
+ loop {
+ let n = self.0.next()?;
+ // Filter out nodes that layout should ignore.
+ if n.is_text_node() || n.is_element() {
+ return Some(n);
+ }
+ }
+ }
+}
+
+/// An iterator over the DOM children of a node.
+pub struct DomChildren<N>(Option<N>);
+impl<N> Iterator for DomChildren<N>
+where
+ N: TNode,
+{
+ type Item = N;
+
+ fn next(&mut self) -> Option<N> {
+ let n = self.0.take()?;
+ self.0 = n.next_sibling();
+ Some(n)
+ }
+}
+
+/// An iterator over the DOM descendants of a node in pre-order.
+pub struct DomDescendants<N> {
+ previous: Option<N>,
+ scope: N,
+}
+
+impl<N> Iterator for DomDescendants<N>
+where
+ N: TNode,
+{
+ type Item = N;
+
+ #[inline]
+ fn next(&mut self) -> Option<N> {
+ let prev = self.previous.take()?;
+ self.previous = prev.next_in_preorder(self.scope);
+ self.previous
+ }
+}
+
+/// The `TDocument` trait, to represent a document node.
+pub trait TDocument: Sized + Copy + Clone {
+ /// The concrete `TNode` type.
+ type ConcreteNode: TNode<ConcreteDocument = Self>;
+
+ /// Get this document as a `TNode`.
+ fn as_node(&self) -> Self::ConcreteNode;
+
+ /// Returns whether this document is an HTML document.
+ fn is_html_document(&self) -> bool;
+
+ /// Returns the quirks mode of this document.
+ fn quirks_mode(&self) -> QuirksMode;
+
+ /// Get a list of elements with a given ID in this document, sorted by
+ /// tree position.
+ ///
+ /// Can return an error to signal that this list is not available, or also
+ /// return an empty slice.
+ fn elements_with_id<'a>(
+ &self,
+ _id: &AtomIdent,
+ ) -> Result<&'a [<Self::ConcreteNode as TNode>::ConcreteElement], ()>
+ where
+ Self: 'a,
+ {
+ Err(())
+ }
+
+ /// This document's shared lock.
+ fn shared_lock(&self) -> &SharedRwLock;
+}
+
+/// The `TNode` trait. This is the main generic trait over which the style
+/// system can be implemented.
+pub trait TNode: Sized + Copy + Clone + Debug + NodeInfo + PartialEq {
+ /// The concrete `TElement` type.
+ type ConcreteElement: TElement<ConcreteNode = Self>;
+
+ /// The concrete `TDocument` type.
+ type ConcreteDocument: TDocument<ConcreteNode = Self>;
+
+ /// The concrete `TShadowRoot` type.
+ type ConcreteShadowRoot: TShadowRoot<ConcreteNode = Self>;
+
+ /// Get this node's parent node.
+ fn parent_node(&self) -> Option<Self>;
+
+ /// Get this node's first child.
+ fn first_child(&self) -> Option<Self>;
+
+ /// Get this node's last child.
+ fn last_child(&self) -> Option<Self>;
+
+ /// Get this node's previous sibling.
+ fn prev_sibling(&self) -> Option<Self>;
+
+ /// Get this node's next sibling.
+ fn next_sibling(&self) -> Option<Self>;
+
+ /// Get the owner document of this node.
+ fn owner_doc(&self) -> Self::ConcreteDocument;
+
+ /// Iterate over the DOM children of a node.
+ #[inline(always)]
+ fn dom_children(&self) -> DomChildren<Self> {
+ DomChildren(self.first_child())
+ }
+
+ /// Returns whether the node is attached to a document.
+ fn is_in_document(&self) -> bool;
+
+ /// Iterate over the DOM children of a node, in preorder.
+ #[inline(always)]
+ fn dom_descendants(&self) -> DomDescendants<Self> {
+ DomDescendants {
+ previous: Some(*self),
+ scope: *self,
+ }
+ }
+
+ /// Returns the next node after this one, in a pre-order tree-traversal of
+ /// the subtree rooted at scoped_to.
+ #[inline]
+ fn next_in_preorder(&self, scoped_to: Self) -> Option<Self> {
+ if let Some(c) = self.first_child() {
+ return Some(c);
+ }
+
+ let mut current = *self;
+ loop {
+ if current == scoped_to {
+ return None;
+ }
+
+ if let Some(s) = current.next_sibling() {
+ return Some(s);
+ }
+
+ debug_assert!(
+ current.parent_node().is_some(),
+ "Not a descendant of the scope?"
+ );
+ current = current.parent_node()?;
+ }
+ }
+
+ /// Get this node's parent element from the perspective of a restyle
+ /// traversal.
+ fn traversal_parent(&self) -> Option<Self::ConcreteElement>;
+
+ /// Get this node's parent element if present.
+ fn parent_element(&self) -> Option<Self::ConcreteElement> {
+ self.parent_node().and_then(|n| n.as_element())
+ }
+
+ /// Get this node's parent element, or shadow host if it's a shadow root.
+ fn parent_element_or_host(&self) -> Option<Self::ConcreteElement> {
+ let parent = self.parent_node()?;
+ if let Some(e) = parent.as_element() {
+ return Some(e);
+ }
+ if let Some(root) = parent.as_shadow_root() {
+ return Some(root.host());
+ }
+ None
+ }
+
+ /// Converts self into an `OpaqueNode`.
+ fn opaque(&self) -> OpaqueNode;
+
+ /// A debug id, only useful, mm... for debugging.
+ fn debug_id(self) -> usize;
+
+ /// Get this node as an element, if it's one.
+ fn as_element(&self) -> Option<Self::ConcreteElement>;
+
+ /// Get this node as a document, if it's one.
+ fn as_document(&self) -> Option<Self::ConcreteDocument>;
+
+ /// Get this node as a ShadowRoot, if it's one.
+ fn as_shadow_root(&self) -> Option<Self::ConcreteShadowRoot>;
+}
+
+/// Wrapper to output the subtree rather than the single node when formatting
+/// for Debug.
+pub struct ShowSubtree<N: TNode>(pub N);
+impl<N: TNode> Debug for ShowSubtree<N> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ writeln!(f, "DOM Subtree:")?;
+ fmt_subtree(f, &|f, n| write!(f, "{:?}", n), self.0, 1)
+ }
+}
+
+/// Wrapper to output the subtree along with the ElementData when formatting
+/// for Debug.
+pub struct ShowSubtreeData<N: TNode>(pub N);
+impl<N: TNode> Debug for ShowSubtreeData<N> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ writeln!(f, "DOM Subtree:")?;
+ fmt_subtree(f, &|f, n| fmt_with_data(f, n), self.0, 1)
+ }
+}
+
+/// Wrapper to output the subtree along with the ElementData and primary
+/// ComputedValues when formatting for Debug. This is extremely verbose.
+#[cfg(feature = "servo")]
+pub struct ShowSubtreeDataAndPrimaryValues<N: TNode>(pub N);
+#[cfg(feature = "servo")]
+impl<N: TNode> Debug for ShowSubtreeDataAndPrimaryValues<N> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ writeln!(f, "DOM Subtree:")?;
+ fmt_subtree(f, &|f, n| fmt_with_data_and_primary_values(f, n), self.0, 1)
+ }
+}
+
+fn fmt_with_data<N: TNode>(f: &mut fmt::Formatter, n: N) -> fmt::Result {
+ if let Some(el) = n.as_element() {
+ write!(
+ f,
+ "{:?} dd={} aodd={} data={:?}",
+ el,
+ el.has_dirty_descendants(),
+ el.has_animation_only_dirty_descendants(),
+ el.borrow_data(),
+ )
+ } else {
+ write!(f, "{:?}", n)
+ }
+}
+
+#[cfg(feature = "servo")]
+fn fmt_with_data_and_primary_values<N: TNode>(f: &mut fmt::Formatter, n: N) -> fmt::Result {
+ if let Some(el) = n.as_element() {
+ let dd = el.has_dirty_descendants();
+ let aodd = el.has_animation_only_dirty_descendants();
+ let data = el.borrow_data();
+ let values = data.as_ref().and_then(|d| d.styles.get_primary());
+ write!(
+ f,
+ "{:?} dd={} aodd={} data={:?} values={:?}",
+ el, dd, aodd, &data, values
+ )
+ } else {
+ write!(f, "{:?}", n)
+ }
+}
+
+fn fmt_subtree<F, N: TNode>(f: &mut fmt::Formatter, stringify: &F, n: N, indent: u32) -> fmt::Result
+where
+ F: Fn(&mut fmt::Formatter, N) -> fmt::Result,
+{
+ for _ in 0..indent {
+ write!(f, " ")?;
+ }
+ stringify(f, n)?;
+ if let Some(e) = n.as_element() {
+ for kid in e.traversal_children() {
+ writeln!(f, "")?;
+ fmt_subtree(f, stringify, kid, indent + 1)?;
+ }
+ }
+
+ Ok(())
+}
+
+/// The ShadowRoot trait.
+pub trait TShadowRoot: Sized + Copy + Clone + Debug + PartialEq {
+ /// The concrete node type.
+ type ConcreteNode: TNode<ConcreteShadowRoot = Self>;
+
+ /// Get this ShadowRoot as a node.
+ fn as_node(&self) -> Self::ConcreteNode;
+
+ /// Get the shadow host that hosts this ShadowRoot.
+ fn host(&self) -> <Self::ConcreteNode as TNode>::ConcreteElement;
+
+ /// Get the style data for this ShadowRoot.
+ fn style_data<'a>(&self) -> Option<&'a CascadeData>
+ where
+ Self: 'a;
+
+ /// Get the list of shadow parts for this shadow root.
+ fn parts<'a>(&self) -> &[<Self::ConcreteNode as TNode>::ConcreteElement]
+ where
+ Self: 'a,
+ {
+ &[]
+ }
+
+ /// Get a list of elements with a given ID in this shadow root, sorted by
+ /// tree position.
+ ///
+ /// Can return an error to signal that this list is not available, or also
+ /// return an empty slice.
+ fn elements_with_id<'a>(
+ &self,
+ _id: &AtomIdent,
+ ) -> Result<&'a [<Self::ConcreteNode as TNode>::ConcreteElement], ()>
+ where
+ Self: 'a,
+ {
+ Err(())
+ }
+}
+
+/// The element trait, the main abstraction the style crate acts over.
+pub trait TElement:
+ Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + SelectorsElement<Impl = SelectorImpl>
+{
+ /// The concrete node type.
+ type ConcreteNode: TNode<ConcreteElement = Self>;
+
+ /// A concrete children iterator type in order to iterate over the `Node`s.
+ ///
+ /// TODO(emilio): We should eventually replace this with the `impl Trait`
+ /// syntax.
+ type TraversalChildrenIterator: Iterator<Item = Self::ConcreteNode>;
+
+ /// Get this element as a node.
+ fn as_node(&self) -> Self::ConcreteNode;
+
+ /// A debug-only check that the device's owner doc matches the actual doc
+ /// we're the root of.
+ ///
+ /// Otherwise we may set document-level state incorrectly, like the root
+ /// font-size used for rem units.
+ fn owner_doc_matches_for_testing(&self, _: &Device) -> bool {
+ true
+ }
+
+ /// Whether this element should match user and content rules.
+ ///
+ /// We use this for Native Anonymous Content in Gecko.
+ fn matches_user_and_content_rules(&self) -> bool {
+ true
+ }
+
+ /// Returns the depth of this element in the DOM.
+ fn depth(&self) -> usize {
+ let mut depth = 0;
+ let mut curr = *self;
+ while let Some(parent) = curr.traversal_parent() {
+ depth += 1;
+ curr = parent;
+ }
+
+ depth
+ }
+
+ /// Get this node's parent element from the perspective of a restyle
+ /// traversal.
+ fn traversal_parent(&self) -> Option<Self> {
+ self.as_node().traversal_parent()
+ }
+
+ /// Get this node's children from the perspective of a restyle traversal.
+ fn traversal_children(&self) -> LayoutIterator<Self::TraversalChildrenIterator>;
+
+ /// Returns the parent element we should inherit from.
+ ///
+ /// This is pretty much always the parent element itself, except in the case
+ /// of Gecko's Native Anonymous Content, which uses the traversal parent
+ /// (i.e. the flattened tree parent) and which also may need to find the
+ /// closest non-NAC ancestor.
+ fn inheritance_parent(&self) -> Option<Self> {
+ self.parent_element()
+ }
+
+ /// The ::before pseudo-element of this element, if it exists.
+ fn before_pseudo_element(&self) -> Option<Self> {
+ None
+ }
+
+ /// The ::after pseudo-element of this element, if it exists.
+ fn after_pseudo_element(&self) -> Option<Self> {
+ None
+ }
+
+ /// The ::marker pseudo-element of this element, if it exists.
+ fn marker_pseudo_element(&self) -> Option<Self> {
+ None
+ }
+
+ /// Execute `f` for each anonymous content child (apart from ::before and
+ /// ::after) whose originating element is `self`.
+ fn each_anonymous_content_child<F>(&self, _f: F)
+ where
+ F: FnMut(Self),
+ {
+ }
+
+ /// Return whether this element is an element in the HTML namespace.
+ fn is_html_element(&self) -> bool;
+
+ /// Return whether this element is an element in the MathML namespace.
+ fn is_mathml_element(&self) -> bool;
+
+ /// Return whether this element is an element in the SVG namespace.
+ fn is_svg_element(&self) -> bool;
+
+ /// Return whether this element is an element in the XUL namespace.
+ fn is_xul_element(&self) -> bool {
+ false
+ }
+
+ /// Return the list of slotted nodes of this node.
+ fn slotted_nodes(&self) -> &[Self::ConcreteNode] {
+ &[]
+ }
+
+ /// Get this element's style attribute.
+ fn style_attribute(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>;
+
+ /// Unset the style attribute's dirty bit.
+ /// Servo doesn't need to manage ditry bit for style attribute.
+ fn unset_dirty_style_attribute(&self) {}
+
+ /// Get this element's SMIL override declarations.
+ fn smil_override(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>> {
+ None
+ }
+
+ /// Get the combined animation and transition rules.
+ ///
+ /// FIXME(emilio): Is this really useful?
+ fn animation_declarations(&self, context: &SharedStyleContext) -> AnimationDeclarations {
+ if !self.may_have_animations() {
+ return Default::default();
+ }
+
+ AnimationDeclarations {
+ animations: self.animation_rule(context),
+ transitions: self.transition_rule(context),
+ }
+ }
+
+ /// Get this element's animation rule.
+ fn animation_rule(
+ &self,
+ _: &SharedStyleContext,
+ ) -> Option<Arc<Locked<PropertyDeclarationBlock>>>;
+
+ /// Get this element's transition rule.
+ fn transition_rule(
+ &self,
+ context: &SharedStyleContext,
+ ) -> Option<Arc<Locked<PropertyDeclarationBlock>>>;
+
+ /// Get this element's state, for non-tree-structural pseudos.
+ fn state(&self) -> ElementState;
+
+ /// Returns whether this element's CustomStateSet contains a given state.
+ fn has_custom_state(&self, _state: &CustomState) -> bool {
+ false
+ }
+
+ /// Returns whether this element has a `part` attribute.
+ fn has_part_attr(&self) -> bool;
+
+ /// Returns whether this element exports any part from its shadow tree.
+ fn exports_any_part(&self) -> bool;
+
+ /// The ID for this element.
+ fn id(&self) -> Option<&WeakAtom>;
+
+ /// Internal iterator for the classes of this element.
+ fn each_class<F>(&self, callback: F)
+ where
+ F: FnMut(&AtomIdent);
+
+ /// Internal iterator for the part names of this element.
+ fn each_part<F>(&self, _callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ }
+
+ /// Internal iterator for the attribute names of this element.
+ fn each_attr_name<F>(&self, callback: F)
+ where
+ F: FnMut(&AtomIdent);
+
+ /// Internal iterator for the part names that this element exports for a
+ /// given part name.
+ fn each_exported_part<F>(&self, _name: &AtomIdent, _callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ }
+
+ /// Whether a given element may generate a pseudo-element.
+ ///
+ /// This is useful to avoid computing, for example, pseudo styles for
+ /// `::-first-line` or `::-first-letter`, when we know it won't affect us.
+ ///
+ /// TODO(emilio, bz): actually implement the logic for it.
+ fn may_generate_pseudo(&self, pseudo: &PseudoElement, _primary_style: &ComputedValues) -> bool {
+ // ::before/::after are always supported for now, though we could try to
+ // optimize out leaf elements.
+
+ // ::first-letter and ::first-line are only supported for block-inside
+ // things, and only in Gecko, not Servo. Unfortunately, Gecko has
+ // block-inside things that might have any computed display value due to
+ // things like fieldsets, legends, etc. Need to figure out how this
+ // should work.
+ debug_assert!(
+ pseudo.is_eager(),
+ "Someone called may_generate_pseudo with a non-eager pseudo."
+ );
+ true
+ }
+
+ /// Returns true if this element may have a descendant needing style processing.
+ ///
+ /// Note that we cannot guarantee the existence of such an element, because
+ /// it may have been removed from the DOM between marking it for restyle and
+ /// the actual restyle traversal.
+ fn has_dirty_descendants(&self) -> bool;
+
+ /// Returns whether state or attributes that may change style have changed
+ /// on the element, and thus whether the element has been snapshotted to do
+ /// restyle hint computation.
+ fn has_snapshot(&self) -> bool;
+
+ /// Returns whether the current snapshot if present has been handled.
+ fn handled_snapshot(&self) -> bool;
+
+ /// Flags this element as having handled already its snapshot.
+ unsafe fn set_handled_snapshot(&self);
+
+ /// Returns whether the element's styles are up-to-date after traversal
+ /// (i.e. in post traversal).
+ fn has_current_styles(&self, data: &ElementData) -> bool {
+ if self.has_snapshot() && !self.handled_snapshot() {
+ return false;
+ }
+
+ data.has_styles() &&
+ // TODO(hiro): When an animating element moved into subtree of
+ // contenteditable element, there remains animation restyle hints in
+ // post traversal. It's generally harmless since the hints will be
+ // processed in a next styling but ideally it should be processed soon.
+ //
+ // Without this, we get failures in:
+ // layout/style/crashtests/1383319.html
+ // layout/style/crashtests/1383001.html
+ //
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1389675 tracks fixing
+ // this.
+ !data.hint.has_non_animation_invalidations()
+ }
+
+ /// Flag that this element has a descendant for style processing.
+ ///
+ /// Only safe to call with exclusive access to the element.
+ unsafe fn set_dirty_descendants(&self);
+
+ /// Flag that this element has no descendant for style processing.
+ ///
+ /// Only safe to call with exclusive access to the element.
+ unsafe fn unset_dirty_descendants(&self);
+
+ /// Similar to the dirty_descendants but for representing a descendant of
+ /// the element needs to be updated in animation-only traversal.
+ fn has_animation_only_dirty_descendants(&self) -> bool {
+ false
+ }
+
+ /// Flag that this element has a descendant for animation-only restyle
+ /// processing.
+ ///
+ /// Only safe to call with exclusive access to the element.
+ unsafe fn set_animation_only_dirty_descendants(&self) {}
+
+ /// Flag that this element has no descendant for animation-only restyle processing.
+ ///
+ /// Only safe to call with exclusive access to the element.
+ unsafe fn unset_animation_only_dirty_descendants(&self) {}
+
+ /// Clear all bits related describing the dirtiness of descendants.
+ ///
+ /// In Gecko, this corresponds to the regular dirty descendants bit, the
+ /// animation-only dirty descendants bit, and the lazy frame construction
+ /// descendants bit.
+ unsafe fn clear_descendant_bits(&self) {
+ self.unset_dirty_descendants();
+ }
+
+ /// Returns true if this element is a visited link.
+ ///
+ /// Servo doesn't support visited styles yet.
+ fn is_visited_link(&self) -> bool {
+ false
+ }
+
+ /// Returns the pseudo-element implemented by this element, if any.
+ ///
+ /// Gecko traverses pseudo-elements during the style traversal, and we need
+ /// to know this so we can properly grab the pseudo-element style from the
+ /// parent element.
+ ///
+ /// Note that we still need to compute the pseudo-elements before-hand,
+ /// given otherwise we don't know if we need to create an element or not.
+ ///
+ /// Servo doesn't have to deal with this.
+ fn implemented_pseudo_element(&self) -> Option<PseudoElement> {
+ None
+ }
+
+ /// Atomically stores the number of children of this node that we will
+ /// need to process during bottom-up traversal.
+ fn store_children_to_process(&self, n: isize);
+
+ /// Atomically notes that a child has been processed during bottom-up
+ /// traversal. Returns the number of children left to process.
+ fn did_process_child(&self) -> isize;
+
+ /// Gets a reference to the ElementData container, or creates one.
+ ///
+ /// Unsafe because it can race to allocate and leak if not used with
+ /// exclusive access to the element.
+ unsafe fn ensure_data(&self) -> AtomicRefMut<ElementData>;
+
+ /// Clears the element data reference, if any.
+ ///
+ /// Unsafe following the same reasoning as ensure_data.
+ unsafe fn clear_data(&self);
+
+ /// Whether there is an ElementData container.
+ fn has_data(&self) -> bool;
+
+ /// Immutably borrows the ElementData.
+ fn borrow_data(&self) -> Option<AtomicRef<ElementData>>;
+
+ /// Mutably borrows the ElementData.
+ fn mutate_data(&self) -> Option<AtomicRefMut<ElementData>>;
+
+ /// Whether we should skip any root- or item-based display property
+ /// blockification on this element. (This function exists so that Gecko
+ /// native anonymous content can opt out of this style fixup.)
+ fn skip_item_display_fixup(&self) -> bool;
+
+ /// In Gecko, element has a flag that represents the element may have
+ /// any type of animations or not to bail out animation stuff early.
+ /// Whereas Servo doesn't have such flag.
+ fn may_have_animations(&self) -> bool;
+
+ /// Creates a task to update various animation state on a given (pseudo-)element.
+ #[cfg(feature = "gecko")]
+ fn update_animations(
+ &self,
+ before_change_style: Option<Arc<ComputedValues>>,
+ tasks: UpdateAnimationsTasks,
+ );
+
+ /// Creates a task to process post animation on a given element.
+ #[cfg(feature = "gecko")]
+ fn process_post_animation(&self, tasks: PostAnimationTasks);
+
+ /// Returns true if the element has relevant animations. Relevant
+ /// animations are those animations that are affecting the element's style
+ /// or are scheduled to do so in the future.
+ fn has_animations(&self, context: &SharedStyleContext) -> bool;
+
+ /// Returns true if the element has a CSS animation. The `context` and `pseudo_element`
+ /// arguments are only used by Servo, since it stores animations globally and pseudo-elements
+ /// are not in the DOM.
+ fn has_css_animations(
+ &self,
+ context: &SharedStyleContext,
+ pseudo_element: Option<PseudoElement>,
+ ) -> bool;
+
+ /// Returns true if the element has a CSS transition (including running transitions and
+ /// completed transitions). The `context` and `pseudo_element` arguments are only used
+ /// by Servo, since it stores animations globally and pseudo-elements are not in the DOM.
+ fn has_css_transitions(
+ &self,
+ context: &SharedStyleContext,
+ pseudo_element: Option<PseudoElement>,
+ ) -> bool;
+
+ /// Returns true if the element has animation restyle hints.
+ fn has_animation_restyle_hints(&self) -> bool {
+ let data = match self.borrow_data() {
+ Some(d) => d,
+ None => return false,
+ };
+ return data.hint.has_animation_hint();
+ }
+
+ /// The shadow root this element is a host of.
+ fn shadow_root(&self) -> Option<<Self::ConcreteNode as TNode>::ConcreteShadowRoot>;
+
+ /// The shadow root which roots the subtree this element is contained in.
+ fn containing_shadow(&self) -> Option<<Self::ConcreteNode as TNode>::ConcreteShadowRoot>;
+
+ /// Return the element which we can use to look up rules in the selector
+ /// maps.
+ ///
+ /// This is always the element itself, except in the case where we are an
+ /// element-backed pseudo-element, in which case we return the originating
+ /// element.
+ fn rule_hash_target(&self) -> Self {
+ if self.is_pseudo_element() {
+ self.pseudo_element_originating_element()
+ .expect("Trying to collect rules for a detached pseudo-element")
+ } else {
+ *self
+ }
+ }
+
+ /// Executes the callback for each applicable style rule data which isn't
+ /// the main document's data (which stores UA / author rules).
+ ///
+ /// The element passed to the callback is the containing shadow host for the
+ /// data if it comes from Shadow DOM.
+ ///
+ /// Returns whether normal document author rules should apply.
+ ///
+ /// TODO(emilio): We could separate the invalidation data for elements
+ /// matching in other scopes to avoid over-invalidation.
+ fn each_applicable_non_document_style_rule_data<'a, F>(&self, mut f: F) -> bool
+ where
+ Self: 'a,
+ F: FnMut(&'a CascadeData, Self),
+ {
+ use crate::rule_collector::containing_shadow_ignoring_svg_use;
+
+ let target = self.rule_hash_target();
+ let matches_user_and_content_rules = target.matches_user_and_content_rules();
+ let mut doc_rules_apply = matches_user_and_content_rules;
+
+ // Use the same rules to look for the containing host as we do for rule
+ // collection.
+ if let Some(shadow) = containing_shadow_ignoring_svg_use(target) {
+ doc_rules_apply = false;
+ if let Some(data) = shadow.style_data() {
+ f(data, shadow.host());
+ }
+ }
+
+ if let Some(shadow) = target.shadow_root() {
+ if let Some(data) = shadow.style_data() {
+ f(data, shadow.host());
+ }
+ }
+
+ let mut current = target.assigned_slot();
+ while let Some(slot) = current {
+ // Slots can only have assigned nodes when in a shadow tree.
+ let shadow = slot.containing_shadow().unwrap();
+ if let Some(data) = shadow.style_data() {
+ if data.any_slotted_rule() {
+ f(data, shadow.host());
+ }
+ }
+ current = slot.assigned_slot();
+ }
+
+ if target.has_part_attr() {
+ if let Some(mut inner_shadow) = target.containing_shadow() {
+ loop {
+ let inner_shadow_host = inner_shadow.host();
+ match inner_shadow_host.containing_shadow() {
+ Some(shadow) => {
+ if let Some(data) = shadow.style_data() {
+ if data.any_part_rule() {
+ f(data, shadow.host())
+ }
+ }
+ // TODO: Could be more granular.
+ if !inner_shadow_host.exports_any_part() {
+ break;
+ }
+ inner_shadow = shadow;
+ },
+ None => {
+ // TODO(emilio): Should probably distinguish with
+ // MatchesDocumentRules::{No,Yes,IfPart} or something so that we could
+ // skip some work.
+ doc_rules_apply = matches_user_and_content_rules;
+ break;
+ },
+ }
+ }
+ }
+ }
+
+ doc_rules_apply
+ }
+
+ /// Returns true if one of the transitions needs to be updated on this element. We check all
+ /// the transition properties to make sure that updating transitions is necessary.
+ /// This method should only be called if might_needs_transitions_update returns true when
+ /// passed the same parameters.
+ #[cfg(feature = "gecko")]
+ fn needs_transitions_update(
+ &self,
+ before_change_style: &ComputedValues,
+ after_change_style: &ComputedValues,
+ ) -> bool;
+
+ /// Returns the value of the `xml:lang=""` attribute (or, if appropriate,
+ /// the `lang=""` attribute) on this element.
+ fn lang_attr(&self) -> Option<AttrValue>;
+
+ /// Returns whether this element's language matches the language tag
+ /// `value`. If `override_lang` is not `None`, it specifies the value
+ /// of the `xml:lang=""` or `lang=""` attribute to use in place of
+ /// looking at the element and its ancestors. (This argument is used
+ /// to implement matching of `:lang()` against snapshots.)
+ fn match_element_lang(&self, override_lang: Option<Option<AttrValue>>, value: &Lang) -> bool;
+
+ /// Returns whether this element is the main body element of the HTML
+ /// document it is on.
+ fn is_html_document_body_element(&self) -> bool;
+
+ /// Generate the proper applicable declarations due to presentational hints,
+ /// and insert them into `hints`.
+ fn synthesize_presentational_hints_for_legacy_attributes<V>(
+ &self,
+ visited_handling: VisitedHandlingMode,
+ hints: &mut V,
+ ) where
+ V: Push<ApplicableDeclarationBlock>;
+
+ /// Returns element's local name.
+ fn local_name(&self) -> &<SelectorImpl as selectors::parser::SelectorImpl>::BorrowedLocalName;
+
+ /// Returns element's namespace.
+ fn namespace(&self)
+ -> &<SelectorImpl as selectors::parser::SelectorImpl>::BorrowedNamespaceUrl;
+
+ /// Returns the size of the element to be used in container size queries.
+ /// This will usually be the size of the content area of the primary box,
+ /// but can be None if there is no box or if some axis lacks size containment.
+ fn query_container_size(
+ &self,
+ display: &Display,
+ ) -> euclid::default::Size2D<Option<app_units::Au>>;
+
+ /// Returns true if the element has all of specified selector flags.
+ fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool;
+
+ /// Returns the search direction for relative selector invalidation, if it is on the search path.
+ fn relative_selector_search_direction(&self) -> Option<ElementSelectorFlags>;
+}
+
+/// TNode and TElement aren't Send because we want to be careful and explicit
+/// about our parallel traversal. However, there are certain situations
+/// (including but not limited to the traversal) where we need to send DOM
+/// objects to other threads.
+///
+/// That's the reason why `SendNode` exists.
+#[derive(Clone, Debug, PartialEq)]
+pub struct SendNode<N: TNode>(N);
+unsafe impl<N: TNode> Send for SendNode<N> {}
+impl<N: TNode> SendNode<N> {
+ /// Unsafely construct a SendNode.
+ pub unsafe fn new(node: N) -> Self {
+ SendNode(node)
+ }
+}
+impl<N: TNode> Deref for SendNode<N> {
+ type Target = N;
+ fn deref(&self) -> &N {
+ &self.0
+ }
+}
+
+/// Same reason as for the existence of SendNode, SendElement does the proper
+/// things for a given `TElement`.
+#[derive(Debug, Eq, Hash, PartialEq)]
+pub struct SendElement<E: TElement>(E);
+unsafe impl<E: TElement> Send for SendElement<E> {}
+impl<E: TElement> SendElement<E> {
+ /// Unsafely construct a SendElement.
+ pub unsafe fn new(el: E) -> Self {
+ SendElement(el)
+ }
+}
+impl<E: TElement> Deref for SendElement<E> {
+ type Target = E;
+ fn deref(&self) -> &E {
+ &self.0
+ }
+}
diff --git a/servo/components/style/dom_apis.rs b/servo/components/style/dom_apis.rs
new file mode 100644
index 0000000000..cdc106e1ad
--- /dev/null
+++ b/servo/components/style/dom_apis.rs
@@ -0,0 +1,814 @@
+/* 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/. */
+
+//! Generic implementations of some DOM APIs so they can be shared between Servo
+//! and Gecko.
+
+use crate::context::QuirksMode;
+use crate::dom::{TDocument, TElement, TNode, TShadowRoot};
+use crate::invalidation::element::invalidation_map::Dependency;
+use crate::invalidation::element::invalidator::{
+ DescendantInvalidationLists, Invalidation, SiblingTraversalMap,
+};
+use crate::invalidation::element::invalidator::{InvalidationProcessor, InvalidationVector};
+use crate::selector_parser::SelectorImpl;
+use crate::values::AtomIdent;
+use selectors::attr::CaseSensitivity;
+use selectors::attr::{AttrSelectorOperation, NamespaceConstraint};
+use selectors::matching::{
+ self, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags,
+ SelectorCaches,
+};
+use selectors::parser::{Combinator, Component, LocalName};
+use selectors::{Element, SelectorList};
+use smallvec::SmallVec;
+
+/// <https://dom.spec.whatwg.org/#dom-element-matches>
+pub fn element_matches<E>(
+ element: &E,
+ selector_list: &SelectorList<E::Impl>,
+ quirks_mode: QuirksMode,
+) -> bool
+where
+ E: Element,
+{
+ let mut selector_caches = SelectorCaches::default();
+
+ let mut context = MatchingContext::new(
+ MatchingMode::Normal,
+ None,
+ &mut selector_caches,
+ quirks_mode,
+ NeedsSelectorFlags::No,
+ MatchingForInvalidation::No,
+ );
+ context.scope_element = Some(element.opaque());
+ context.current_host = element.containing_shadow_host().map(|e| e.opaque());
+ matching::matches_selector_list(selector_list, element, &mut context)
+}
+
+/// <https://dom.spec.whatwg.org/#dom-element-closest>
+pub fn element_closest<E>(
+ element: E,
+ selector_list: &SelectorList<E::Impl>,
+ quirks_mode: QuirksMode,
+) -> Option<E>
+where
+ E: Element,
+{
+ let mut selector_caches = SelectorCaches::default();
+
+ let mut context = MatchingContext::new(
+ MatchingMode::Normal,
+ None,
+ &mut selector_caches,
+ quirks_mode,
+ NeedsSelectorFlags::No,
+ MatchingForInvalidation::No,
+ );
+ context.scope_element = Some(element.opaque());
+ context.current_host = element.containing_shadow_host().map(|e| e.opaque());
+
+ let mut current = Some(element);
+ while let Some(element) = current.take() {
+ if matching::matches_selector_list(selector_list, &element, &mut context) {
+ return Some(element);
+ }
+ current = element.parent_element();
+ }
+
+ return None;
+}
+
+/// A selector query abstraction, in order to be generic over QuerySelector and
+/// QuerySelectorAll.
+pub trait SelectorQuery<E: TElement> {
+ /// The output of the query.
+ type Output;
+
+ /// Whether the query should stop after the first element has been matched.
+ fn should_stop_after_first_match() -> bool;
+
+ /// Append an element matching after the first query.
+ fn append_element(output: &mut Self::Output, element: E);
+
+ /// Returns true if the output is empty.
+ fn is_empty(output: &Self::Output) -> bool;
+}
+
+/// The result of a querySelectorAll call.
+pub type QuerySelectorAllResult<E> = SmallVec<[E; 128]>;
+
+/// A query for all the elements in a subtree.
+pub struct QueryAll;
+
+impl<E: TElement> SelectorQuery<E> for QueryAll {
+ type Output = QuerySelectorAllResult<E>;
+
+ fn should_stop_after_first_match() -> bool {
+ false
+ }
+
+ fn append_element(output: &mut Self::Output, element: E) {
+ output.push(element);
+ }
+
+ fn is_empty(output: &Self::Output) -> bool {
+ output.is_empty()
+ }
+}
+
+/// A query for the first in-tree match of all the elements in a subtree.
+pub struct QueryFirst;
+
+impl<E: TElement> SelectorQuery<E> for QueryFirst {
+ type Output = Option<E>;
+
+ fn should_stop_after_first_match() -> bool {
+ true
+ }
+
+ fn append_element(output: &mut Self::Output, element: E) {
+ if output.is_none() {
+ *output = Some(element)
+ }
+ }
+
+ fn is_empty(output: &Self::Output) -> bool {
+ output.is_none()
+ }
+}
+
+struct QuerySelectorProcessor<'a, 'b, E, Q>
+where
+ E: TElement + 'a,
+ Q: SelectorQuery<E>,
+ Q::Output: 'a,
+{
+ results: &'a mut Q::Output,
+ matching_context: MatchingContext<'b, E::Impl>,
+ traversal_map: SiblingTraversalMap<E>,
+ dependencies: &'a [Dependency],
+}
+
+impl<'a, 'b, E, Q> InvalidationProcessor<'a, 'b, E> for QuerySelectorProcessor<'a, 'b, E, Q>
+where
+ E: TElement + 'a,
+ Q: SelectorQuery<E>,
+ Q::Output: 'a,
+{
+ fn light_tree_only(&self) -> bool {
+ true
+ }
+
+ fn check_outer_dependency(&mut self, _: &Dependency, _: E) -> bool {
+ debug_assert!(
+ false,
+ "How? We should only have parent-less dependencies here!"
+ );
+ true
+ }
+
+ fn collect_invalidations(
+ &mut self,
+ element: E,
+ self_invalidations: &mut InvalidationVector<'a>,
+ descendant_invalidations: &mut DescendantInvalidationLists<'a>,
+ _sibling_invalidations: &mut InvalidationVector<'a>,
+ ) -> bool {
+ // TODO(emilio): If the element is not a root element, and
+ // selector_list has any descendant combinator, we need to do extra work
+ // in order to handle properly things like:
+ //
+ // <div id="a">
+ // <div id="b">
+ // <div id="c"></div>
+ // </div>
+ // </div>
+ //
+ // b.querySelector('#a div'); // Should return "c".
+ //
+ // For now, assert it's a root element.
+ debug_assert!(element.parent_element().is_none());
+
+ let target_vector = if self.matching_context.scope_element.is_some() {
+ &mut descendant_invalidations.dom_descendants
+ } else {
+ self_invalidations
+ };
+
+ for dependency in self.dependencies.iter() {
+ target_vector.push(Invalidation::new(
+ dependency,
+ self.matching_context.current_host.clone(),
+ ))
+ }
+
+ false
+ }
+
+ fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl> {
+ &mut self.matching_context
+ }
+
+ fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> {
+ &self.traversal_map
+ }
+
+ fn should_process_descendants(&mut self, _: E) -> bool {
+ if Q::should_stop_after_first_match() {
+ return Q::is_empty(&self.results);
+ }
+
+ true
+ }
+
+ fn invalidated_self(&mut self, e: E) {
+ Q::append_element(self.results, e);
+ }
+
+ fn invalidated_sibling(&mut self, e: E, _of: E) {
+ Q::append_element(self.results, e);
+ }
+
+ fn recursion_limit_exceeded(&mut self, _e: E) {}
+ fn invalidated_descendants(&mut self, _e: E, _child: E) {}
+}
+
+fn collect_all_elements<E, Q, F>(root: E::ConcreteNode, results: &mut Q::Output, mut filter: F)
+where
+ E: TElement,
+ Q: SelectorQuery<E>,
+ F: FnMut(E) -> bool,
+{
+ for node in root.dom_descendants() {
+ let element = match node.as_element() {
+ Some(e) => e,
+ None => continue,
+ };
+
+ if !filter(element) {
+ continue;
+ }
+
+ Q::append_element(results, element);
+ if Q::should_stop_after_first_match() {
+ return;
+ }
+ }
+}
+
+/// Returns whether a given element connected to `root` is descendant of `root`.
+///
+/// NOTE(emilio): if root == element, this returns false.
+fn connected_element_is_descendant_of<E>(element: E, root: E::ConcreteNode) -> bool
+where
+ E: TElement,
+{
+ // Optimize for when the root is a document or a shadow root and the element
+ // is connected to that root.
+ if root.as_document().is_some() {
+ debug_assert!(element.as_node().is_in_document(), "Not connected?");
+ debug_assert_eq!(
+ root,
+ root.owner_doc().as_node(),
+ "Where did this element come from?",
+ );
+ return true;
+ }
+
+ if root.as_shadow_root().is_some() {
+ debug_assert_eq!(
+ element.containing_shadow().unwrap().as_node(),
+ root,
+ "Not connected?"
+ );
+ return true;
+ }
+
+ let mut current = element.as_node().parent_node();
+ while let Some(n) = current.take() {
+ if n == root {
+ return true;
+ }
+
+ current = n.parent_node();
+ }
+ false
+}
+
+/// Fast path for iterating over every element with a given id in the document
+/// or shadow root that `root` is connected to.
+fn fast_connected_elements_with_id<'a, N>(
+ root: N,
+ id: &AtomIdent,
+ case_sensitivity: CaseSensitivity,
+) -> Result<&'a [N::ConcreteElement], ()>
+where
+ N: TNode + 'a,
+{
+ if case_sensitivity != CaseSensitivity::CaseSensitive {
+ return Err(());
+ }
+
+ if root.is_in_document() {
+ return root.owner_doc().elements_with_id(id);
+ }
+
+ if let Some(shadow) = root.as_shadow_root() {
+ return shadow.elements_with_id(id);
+ }
+
+ if let Some(shadow) = root.as_element().and_then(|e| e.containing_shadow()) {
+ return shadow.elements_with_id(id);
+ }
+
+ Err(())
+}
+
+/// Collects elements with a given id under `root`, that pass `filter`.
+fn collect_elements_with_id<E, Q, F>(
+ root: E::ConcreteNode,
+ id: &AtomIdent,
+ results: &mut Q::Output,
+ class_and_id_case_sensitivity: CaseSensitivity,
+ mut filter: F,
+) where
+ E: TElement,
+ Q: SelectorQuery<E>,
+ F: FnMut(E) -> bool,
+{
+ let elements = match fast_connected_elements_with_id(root, id, class_and_id_case_sensitivity) {
+ Ok(elements) => elements,
+ Err(()) => {
+ collect_all_elements::<E, Q, _>(root, results, |e| {
+ e.has_id(id, class_and_id_case_sensitivity) && filter(e)
+ });
+
+ return;
+ },
+ };
+
+ for element in elements {
+ // If the element is not an actual descendant of the root, even though
+ // it's connected, we don't really care about it.
+ if !connected_element_is_descendant_of(*element, root) {
+ continue;
+ }
+
+ if !filter(*element) {
+ continue;
+ }
+
+ Q::append_element(results, *element);
+ if Q::should_stop_after_first_match() {
+ break;
+ }
+ }
+}
+
+fn has_attr<E>(element: E, local_name: &AtomIdent) -> bool
+where
+ E: TElement,
+{
+ let mut found = false;
+ element.each_attr_name(|name| found |= name == local_name);
+ found
+}
+
+#[inline(always)]
+fn local_name_matches<E>(element: E, local_name: &LocalName<E::Impl>) -> bool
+where
+ E: TElement,
+{
+ let LocalName {
+ ref name,
+ ref lower_name,
+ } = *local_name;
+
+ let chosen_name = if name == lower_name || element.is_html_element_in_html_document() {
+ lower_name
+ } else {
+ name
+ };
+
+ element.local_name() == &**chosen_name
+}
+
+fn get_attr_name(component: &Component<SelectorImpl>) -> Option<&AtomIdent> {
+ let (name, name_lower) = match component {
+ Component::AttributeInNoNamespace { ref local_name, .. } => return Some(local_name),
+ Component::AttributeInNoNamespaceExists {
+ ref local_name,
+ ref local_name_lower,
+ ..
+ } => (local_name, local_name_lower),
+ Component::AttributeOther(ref attr) => (&attr.local_name, &attr.local_name_lower),
+ _ => return None,
+ };
+ if name != name_lower {
+ return None; // TODO: Maybe optimize this?
+ }
+ Some(name)
+}
+
+fn get_id(component: &Component<SelectorImpl>) -> Option<&AtomIdent> {
+ use selectors::attr::AttrSelectorOperator;
+ Some(match component {
+ Component::ID(ref id) => id,
+ Component::AttributeInNoNamespace {
+ ref operator,
+ ref local_name,
+ ref value,
+ ..
+ } => {
+ if *local_name != local_name!("id") {
+ return None;
+ }
+ if *operator != AttrSelectorOperator::Equal {
+ return None;
+ }
+ AtomIdent::cast(&value.0)
+ },
+ _ => return None,
+ })
+}
+
+/// Fast paths for querySelector with a single simple selector.
+fn query_selector_single_query<E, Q>(
+ root: E::ConcreteNode,
+ component: &Component<E::Impl>,
+ results: &mut Q::Output,
+ class_and_id_case_sensitivity: CaseSensitivity,
+) -> Result<(), ()>
+where
+ E: TElement,
+ Q: SelectorQuery<E>,
+{
+ match *component {
+ Component::ExplicitUniversalType => {
+ collect_all_elements::<E, Q, _>(root, results, |_| true)
+ },
+ Component::Class(ref class) => collect_all_elements::<E, Q, _>(root, results, |element| {
+ element.has_class(class, class_and_id_case_sensitivity)
+ }),
+ Component::LocalName(ref local_name) => {
+ collect_all_elements::<E, Q, _>(root, results, |element| {
+ local_name_matches(element, local_name)
+ })
+ },
+ Component::AttributeInNoNamespaceExists {
+ ref local_name,
+ ref local_name_lower,
+ } => collect_all_elements::<E, Q, _>(root, results, |element| {
+ element.has_attr_in_no_namespace(matching::select_name(
+ &element,
+ local_name,
+ local_name_lower,
+ ))
+ }),
+ Component::AttributeInNoNamespace {
+ ref local_name,
+ ref value,
+ operator,
+ case_sensitivity,
+ } => {
+ let empty_namespace = selectors::parser::namespace_empty_string::<E::Impl>();
+ let namespace_constraint = NamespaceConstraint::Specific(&empty_namespace);
+ collect_all_elements::<E, Q, _>(root, results, |element| {
+ element.attr_matches(
+ &namespace_constraint,
+ local_name,
+ &AttrSelectorOperation::WithValue {
+ operator,
+ case_sensitivity: matching::to_unconditional_case_sensitivity(
+ case_sensitivity,
+ &element,
+ ),
+ value,
+ },
+ )
+ })
+ },
+ ref other => {
+ let id = match get_id(other) {
+ Some(id) => id,
+ // TODO(emilio): More fast paths?
+ None => return Err(()),
+ };
+ collect_elements_with_id::<E, Q, _>(
+ root,
+ id,
+ results,
+ class_and_id_case_sensitivity,
+ |_| true,
+ );
+ },
+ }
+
+ Ok(())
+}
+
+enum SimpleFilter<'a> {
+ Class(&'a AtomIdent),
+ Attr(&'a AtomIdent),
+ LocalName(&'a LocalName<SelectorImpl>),
+}
+
+/// Fast paths for a given selector query.
+///
+/// When there's only one component, we go directly to
+/// `query_selector_single_query`, otherwise, we try to optimize by looking just
+/// at the subtrees rooted at ids in the selector, and otherwise we try to look
+/// up by class name or local name in the rightmost compound.
+///
+/// FIXME(emilio, nbp): This may very well be a good candidate for code to be
+/// replaced by HolyJit :)
+fn query_selector_fast<E, Q>(
+ root: E::ConcreteNode,
+ selector_list: &SelectorList<E::Impl>,
+ results: &mut Q::Output,
+ matching_context: &mut MatchingContext<E::Impl>,
+) -> Result<(), ()>
+where
+ E: TElement,
+ Q: SelectorQuery<E>,
+{
+ // We need to return elements in document order, and reordering them
+ // afterwards is kinda silly.
+ if selector_list.len() > 1 {
+ return Err(());
+ }
+
+ let selector = &selector_list.slice()[0];
+ let class_and_id_case_sensitivity = matching_context.classes_and_ids_case_sensitivity();
+ // Let's just care about the easy cases for now.
+ if selector.len() == 1 {
+ if query_selector_single_query::<E, Q>(
+ root,
+ selector.iter().next().unwrap(),
+ results,
+ class_and_id_case_sensitivity,
+ )
+ .is_ok()
+ {
+ return Ok(());
+ }
+ }
+
+ let mut iter = selector.iter();
+ let mut combinator: Option<Combinator> = None;
+
+ // We want to optimize some cases where there's no id involved whatsoever,
+ // like `.foo .bar`, but we don't want to make `#foo .bar` slower because of
+ // that.
+ let mut simple_filter = None;
+
+ 'selector_loop: loop {
+ debug_assert!(combinator.map_or(true, |c| !c.is_sibling()));
+
+ 'component_loop: for component in &mut iter {
+ match *component {
+ Component::Class(ref class) => {
+ if combinator.is_none() {
+ simple_filter = Some(SimpleFilter::Class(class));
+ }
+ },
+ Component::LocalName(ref local_name) => {
+ if combinator.is_none() {
+ // Prefer to look at class rather than local-name if
+ // both are present.
+ if let Some(SimpleFilter::Class(..)) = simple_filter {
+ continue;
+ }
+ simple_filter = Some(SimpleFilter::LocalName(local_name));
+ }
+ },
+ ref other => {
+ if let Some(id) = get_id(other) {
+ if combinator.is_none() {
+ // In the rightmost compound, just find descendants of root that match
+ // the selector list with that id.
+ collect_elements_with_id::<E, Q, _>(
+ root,
+ id,
+ results,
+ class_and_id_case_sensitivity,
+ |e| {
+ matching::matches_selector_list(
+ selector_list,
+ &e,
+ matching_context,
+ )
+ },
+ );
+ return Ok(());
+ }
+
+ let elements = fast_connected_elements_with_id(
+ root,
+ id,
+ class_and_id_case_sensitivity,
+ )?;
+ if elements.is_empty() {
+ return Ok(());
+ }
+
+ // Results need to be in document order. Let's not bother
+ // reordering or deduplicating nodes, which we would need to
+ // do if one element with the given id were a descendant of
+ // another element with that given id.
+ if !Q::should_stop_after_first_match() && elements.len() > 1 {
+ continue;
+ }
+
+ for element in elements {
+ // If the element is not a descendant of the root, then
+ // it may have descendants that match our selector that
+ // _are_ descendants of the root, and other descendants
+ // that match our selector that are _not_.
+ //
+ // So we can't just walk over the element's descendants
+ // and match the selector against all of them, nor can
+ // we skip looking at this element's descendants.
+ //
+ // Give up on trying to optimize based on this id and
+ // keep walking our selector.
+ if !connected_element_is_descendant_of(*element, root) {
+ continue 'component_loop;
+ }
+
+ query_selector_slow::<E, Q>(
+ element.as_node(),
+ selector_list,
+ results,
+ matching_context,
+ );
+
+ if Q::should_stop_after_first_match() && !Q::is_empty(&results) {
+ break;
+ }
+ }
+
+ return Ok(());
+ }
+ if combinator.is_none() && simple_filter.is_none() {
+ if let Some(attr_name) = get_attr_name(other) {
+ simple_filter = Some(SimpleFilter::Attr(attr_name));
+ }
+ }
+ },
+ }
+ }
+
+ loop {
+ let next_combinator = match iter.next_sequence() {
+ None => break 'selector_loop,
+ Some(c) => c,
+ };
+
+ // We don't want to scan stuff affected by sibling combinators,
+ // given we scan the subtree of elements with a given id (and we
+ // don't want to care about scanning the siblings' subtrees).
+ if next_combinator.is_sibling() {
+ // Advance to the next combinator.
+ for _ in &mut iter {}
+ continue;
+ }
+
+ combinator = Some(next_combinator);
+ break;
+ }
+ }
+
+ // We got here without finding any ID or such that we could handle. Try to
+ // use one of the simple filters.
+ let simple_filter = match simple_filter {
+ Some(f) => f,
+ None => return Err(()),
+ };
+
+ match simple_filter {
+ SimpleFilter::Class(ref class) => {
+ collect_all_elements::<E, Q, _>(root, results, |element| {
+ element.has_class(class, class_and_id_case_sensitivity) &&
+ matching::matches_selector_list(selector_list, &element, matching_context)
+ });
+ },
+ SimpleFilter::LocalName(ref local_name) => {
+ collect_all_elements::<E, Q, _>(root, results, |element| {
+ local_name_matches(element, local_name) &&
+ matching::matches_selector_list(selector_list, &element, matching_context)
+ });
+ },
+ SimpleFilter::Attr(ref local_name) => {
+ collect_all_elements::<E, Q, _>(root, results, |element| {
+ has_attr(element, local_name) &&
+ matching::matches_selector_list(selector_list, &element, matching_context)
+ });
+ },
+ }
+
+ Ok(())
+}
+
+// Slow path for a given selector query.
+fn query_selector_slow<E, Q>(
+ root: E::ConcreteNode,
+ selector_list: &SelectorList<E::Impl>,
+ results: &mut Q::Output,
+ matching_context: &mut MatchingContext<E::Impl>,
+) where
+ E: TElement,
+ Q: SelectorQuery<E>,
+{
+ collect_all_elements::<E, Q, _>(root, results, |element| {
+ matching::matches_selector_list(selector_list, &element, matching_context)
+ });
+}
+
+/// Whether the invalidation machinery should be used for this query.
+#[derive(PartialEq)]
+pub enum MayUseInvalidation {
+ /// We may use it if we deem it useful.
+ Yes,
+ /// Don't use it.
+ No,
+}
+
+/// <https://dom.spec.whatwg.org/#dom-parentnode-queryselector>
+pub fn query_selector<E, Q>(
+ root: E::ConcreteNode,
+ selector_list: &SelectorList<E::Impl>,
+ results: &mut Q::Output,
+ may_use_invalidation: MayUseInvalidation,
+) where
+ E: TElement,
+ Q: SelectorQuery<E>,
+{
+ use crate::invalidation::element::invalidator::TreeStyleInvalidator;
+
+ let mut selector_caches = SelectorCaches::default();
+ let quirks_mode = root.owner_doc().quirks_mode();
+
+ let mut matching_context = MatchingContext::new(
+ MatchingMode::Normal,
+ None,
+ &mut selector_caches,
+ quirks_mode,
+ NeedsSelectorFlags::No,
+ MatchingForInvalidation::No,
+ );
+ let root_element = root.as_element();
+ matching_context.scope_element = root_element.map(|e| e.opaque());
+ matching_context.current_host = match root_element {
+ Some(root) => root.containing_shadow_host().map(|host| host.opaque()),
+ None => root.as_shadow_root().map(|root| root.host().opaque()),
+ };
+
+ let fast_result =
+ query_selector_fast::<E, Q>(root, selector_list, results, &mut matching_context);
+
+ if fast_result.is_ok() {
+ return;
+ }
+
+ // Slow path: Use the invalidation machinery if we're a root, and tree
+ // traversal otherwise.
+ //
+ // See the comment in collect_invalidations to see why only if we're a root.
+ //
+ // The invalidation mechanism is only useful in presence of combinators.
+ //
+ // We could do that check properly here, though checking the length of the
+ // selectors is a good heuristic.
+ //
+ // A selector with a combinator needs to have a length of at least 3: A
+ // simple selector, a combinator, and another simple selector.
+ let invalidation_may_be_useful = may_use_invalidation == MayUseInvalidation::Yes &&
+ selector_list.slice().iter().any(|s| s.len() > 2);
+
+ if root_element.is_some() || !invalidation_may_be_useful {
+ query_selector_slow::<E, Q>(root, selector_list, results, &mut matching_context);
+ } else {
+ let dependencies = selector_list
+ .slice()
+ .iter()
+ .map(|selector| Dependency::for_full_selector_invalidation(selector.clone()))
+ .collect::<SmallVec<[_; 5]>>();
+ let mut processor = QuerySelectorProcessor::<E, Q> {
+ results,
+ matching_context,
+ traversal_map: SiblingTraversalMap::default(),
+ dependencies: &dependencies,
+ };
+
+ for node in root.dom_children() {
+ if let Some(e) = node.as_element() {
+ TreeStyleInvalidator::new(e, /* stack_limit_checker = */ None, &mut processor)
+ .invalidate();
+ }
+ }
+ }
+}
diff --git a/servo/components/style/driver.rs b/servo/components/style/driver.rs
new file mode 100644
index 0000000000..95447ce08e
--- /dev/null
+++ b/servo/components/style/driver.rs
@@ -0,0 +1,164 @@
+/* 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/. */
+
+//! Implements traversal over the DOM tree. The traversal starts in sequential
+//! mode, and optionally parallelizes as it discovers work.
+
+#![deny(missing_docs)]
+
+use crate::context::{PerThreadTraversalStatistics, StyleContext};
+use crate::context::{ThreadLocalStyleContext, TraversalStatistics};
+use crate::dom::{SendNode, TElement, TNode};
+use crate::parallel;
+use crate::scoped_tls::ScopedTLS;
+use crate::traversal::{DomTraversal, PerLevelTraversalData, PreTraverseToken};
+use rayon;
+use std::collections::VecDeque;
+use time;
+
+#[cfg(feature = "servo")]
+fn should_report_statistics() -> bool {
+ false
+}
+
+#[cfg(feature = "gecko")]
+fn should_report_statistics() -> bool {
+ unsafe { crate::gecko_bindings::structs::ServoTraversalStatistics_sActive }
+}
+
+#[cfg(feature = "servo")]
+fn report_statistics(_stats: &PerThreadTraversalStatistics) {
+ unreachable!("Servo never report stats");
+}
+
+#[cfg(feature = "gecko")]
+fn report_statistics(stats: &PerThreadTraversalStatistics) {
+ // This should only be called in the main thread, or it may be racy
+ // to update the statistics in a global variable.
+ debug_assert!(unsafe { crate::gecko_bindings::bindings::Gecko_IsMainThread() });
+ let gecko_stats =
+ unsafe { &mut crate::gecko_bindings::structs::ServoTraversalStatistics_sSingleton };
+ gecko_stats.mElementsTraversed += stats.elements_traversed;
+ gecko_stats.mElementsStyled += stats.elements_styled;
+ gecko_stats.mElementsMatched += stats.elements_matched;
+ gecko_stats.mStylesShared += stats.styles_shared;
+ gecko_stats.mStylesReused += stats.styles_reused;
+}
+
+fn with_pool_in_place_scope<'scope, R>(
+ work_unit_max: usize,
+ pool: Option<&rayon::ThreadPool>,
+ closure: impl FnOnce(Option<&rayon::ScopeFifo<'scope>>) -> R,
+) -> R {
+ if work_unit_max == 0 || pool.is_none() {
+ closure(None)
+ } else {
+ pool.unwrap()
+ .in_place_scope_fifo(|scope| closure(Some(scope)))
+ }
+}
+
+/// See documentation of the pref for performance characteristics.
+fn work_unit_max() -> usize {
+ static_prefs::pref!("layout.css.stylo-work-unit-size") as usize
+}
+
+/// Do a DOM traversal for top-down and (optionally) bottom-up processing, generic over `D`.
+///
+/// We use an adaptive traversal strategy. We start out with simple sequential processing, until we
+/// arrive at a wide enough level in the DOM that the parallel traversal would parallelize it.
+/// If a thread pool is provided, we then transfer control over to the parallel traversal.
+///
+/// Returns true if the traversal was parallel, and also returns the statistics object containing
+/// information on nodes traversed (on nightly only). Not all of its fields will be initialized
+/// since we don't call finish().
+pub fn traverse_dom<E, D>(
+ traversal: &D,
+ token: PreTraverseToken<E>,
+ pool: Option<&rayon::ThreadPool>,
+) -> E
+where
+ E: TElement,
+ D: DomTraversal<E>,
+{
+ let root = token
+ .traversal_root()
+ .expect("Should've ensured we needed to traverse");
+
+ let report_stats = should_report_statistics();
+ let dump_stats = traversal.shared_context().options.dump_style_statistics;
+ let start_time = if dump_stats {
+ Some(time::precise_time_s())
+ } else {
+ None
+ };
+
+ // Declare the main-thread context, as well as the worker-thread contexts,
+ // which we may or may not instantiate. It's important to declare the worker-
+ // thread contexts first, so that they get dropped second. This matters because:
+ // * ThreadLocalContexts borrow AtomicRefCells in TLS.
+ // * Dropping a ThreadLocalContext can run SequentialTasks.
+ // * Sequential tasks may call into functions like
+ // Servo_StyleSet_GetBaseComputedValuesForElement, which instantiate a
+ // ThreadLocalStyleContext on the main thread. If the main thread
+ // ThreadLocalStyleContext has not released its TLS borrow by that point,
+ // we'll panic on double-borrow.
+ let mut scoped_tls = ScopedTLS::<ThreadLocalStyleContext<E>>::new(pool);
+ // Process the nodes breadth-first. This helps keep similar traversal characteristics for the
+ // style sharing cache.
+ let work_unit_max = work_unit_max();
+ with_pool_in_place_scope(work_unit_max, pool, |maybe_scope| {
+ let mut tlc = scoped_tls.ensure(parallel::create_thread_local_context);
+ let mut context = StyleContext {
+ shared: traversal.shared_context(),
+ thread_local: &mut tlc,
+ };
+
+ debug_assert_eq!(
+ scoped_tls.current_thread_index(),
+ 0,
+ "Main thread should be the first thread"
+ );
+
+ let mut discovered = VecDeque::with_capacity(work_unit_max * 2);
+ discovered.push_back(unsafe { SendNode::new(root.as_node()) });
+ parallel::style_trees(
+ &mut context,
+ discovered,
+ root.as_node().opaque(),
+ work_unit_max,
+ PerLevelTraversalData {
+ current_dom_depth: root.depth(),
+ },
+ maybe_scope,
+ traversal,
+ &scoped_tls,
+ );
+ });
+
+ // Collect statistics from thread-locals if requested.
+ if dump_stats || report_stats {
+ let mut aggregate = PerThreadTraversalStatistics::default();
+ for slot in scoped_tls.slots() {
+ if let Some(cx) = slot.get_mut() {
+ aggregate += cx.statistics.clone();
+ }
+ }
+
+ if report_stats {
+ report_statistics(&aggregate);
+ }
+ // dump statistics to stdout if requested
+ if dump_stats {
+ let parallel = pool.is_some();
+ let stats =
+ TraversalStatistics::new(aggregate, traversal, parallel, start_time.unwrap());
+ if stats.is_large {
+ println!("{}", stats);
+ }
+ }
+ }
+
+ root
+}
diff --git a/servo/components/style/encoding_support.rs b/servo/components/style/encoding_support.rs
new file mode 100644
index 0000000000..c144ad0b3b
--- /dev/null
+++ b/servo/components/style/encoding_support.rs
@@ -0,0 +1,105 @@
+/* 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/. */
+
+//! Parsing stylesheets from bytes (not `&str`).
+
+use crate::context::QuirksMode;
+use crate::error_reporting::ParseErrorReporter;
+use crate::media_queries::MediaList;
+use crate::shared_lock::SharedRwLock;
+use crate::stylesheets::{AllowImportRules, Origin, Stylesheet, StylesheetLoader, UrlExtraData};
+use cssparser::{stylesheet_encoding, EncodingSupport};
+use servo_arc::Arc;
+use std::borrow::Cow;
+use std::str;
+
+struct EncodingRs;
+
+impl EncodingSupport for EncodingRs {
+ type Encoding = &'static encoding_rs::Encoding;
+
+ fn utf8() -> Self::Encoding {
+ encoding_rs::UTF_8
+ }
+
+ fn is_utf16_be_or_le(encoding: &Self::Encoding) -> bool {
+ *encoding == encoding_rs::UTF_16LE || *encoding == encoding_rs::UTF_16BE
+ }
+
+ fn from_label(ascii_label: &[u8]) -> Option<Self::Encoding> {
+ encoding_rs::Encoding::for_label(ascii_label)
+ }
+}
+
+fn decode_stylesheet_bytes<'a>(
+ css: &'a [u8],
+ protocol_encoding_label: Option<&str>,
+ environment_encoding: Option<&'static encoding_rs::Encoding>,
+) -> Cow<'a, str> {
+ let fallback_encoding = stylesheet_encoding::<EncodingRs>(
+ css,
+ protocol_encoding_label.map(str::as_bytes),
+ environment_encoding,
+ );
+ let (result, _used_encoding, _) = fallback_encoding.decode(&css);
+ // FIXME record used encoding for environment encoding of @import
+ result
+}
+
+impl Stylesheet {
+ /// Parse a stylesheet from a set of bytes, potentially received over the
+ /// network.
+ ///
+ /// Takes care of decoding the network bytes and forwards the resulting
+ /// string to `Stylesheet::from_str`.
+ pub fn from_bytes(
+ bytes: &[u8],
+ url_data: UrlExtraData,
+ protocol_encoding_label: Option<&str>,
+ environment_encoding: Option<&'static encoding_rs::Encoding>,
+ origin: Origin,
+ media: MediaList,
+ shared_lock: SharedRwLock,
+ stylesheet_loader: Option<&dyn StylesheetLoader>,
+ error_reporter: Option<&dyn ParseErrorReporter>,
+ quirks_mode: QuirksMode,
+ ) -> Stylesheet {
+ let string = decode_stylesheet_bytes(bytes, protocol_encoding_label, environment_encoding);
+ Stylesheet::from_str(
+ &string,
+ url_data,
+ origin,
+ Arc::new(shared_lock.wrap(media)),
+ shared_lock,
+ stylesheet_loader,
+ error_reporter,
+ quirks_mode,
+ 0,
+ AllowImportRules::Yes,
+ )
+ }
+
+ /// Updates an empty stylesheet with a set of bytes that reached over the
+ /// network.
+ pub fn update_from_bytes(
+ existing: &Stylesheet,
+ bytes: &[u8],
+ protocol_encoding_label: Option<&str>,
+ environment_encoding: Option<&'static encoding_rs::Encoding>,
+ url_data: UrlExtraData,
+ stylesheet_loader: Option<&dyn StylesheetLoader>,
+ error_reporter: Option<&dyn ParseErrorReporter>,
+ ) {
+ let string = decode_stylesheet_bytes(bytes, protocol_encoding_label, environment_encoding);
+ Self::update_from_str(
+ existing,
+ &string,
+ url_data,
+ stylesheet_loader,
+ error_reporter,
+ 0,
+ AllowImportRules::Yes,
+ )
+ }
+}
diff --git a/servo/components/style/error_reporting.rs b/servo/components/style/error_reporting.rs
new file mode 100644
index 0000000000..6db4c18e27
--- /dev/null
+++ b/servo/components/style/error_reporting.rs
@@ -0,0 +1,454 @@
+/* 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/. */
+
+//! Types used to report parsing errors.
+
+#![deny(missing_docs)]
+
+use crate::selector_parser::SelectorImpl;
+use crate::stylesheets::UrlExtraData;
+use cssparser::{BasicParseErrorKind, ParseErrorKind, SourceLocation, Token};
+use selectors::parser::{Component, RelativeSelector, Selector};
+use selectors::visitor::{SelectorListKind, SelectorVisitor};
+use selectors::SelectorList;
+use std::fmt;
+use style_traits::ParseError;
+
+/// Errors that can be encountered while parsing CSS.
+#[derive(Debug)]
+pub enum ContextualParseError<'a> {
+ /// A property declaration was not recognized.
+ UnsupportedPropertyDeclaration(&'a str, ParseError<'a>, &'a [SelectorList<SelectorImpl>]),
+ /// A property descriptor was not recognized.
+ UnsupportedPropertyDescriptor(&'a str, ParseError<'a>),
+ /// A font face descriptor was not recognized.
+ UnsupportedFontFaceDescriptor(&'a str, ParseError<'a>),
+ /// A font feature values descriptor was not recognized.
+ UnsupportedFontFeatureValuesDescriptor(&'a str, ParseError<'a>),
+ /// A font palette values descriptor was not recognized.
+ UnsupportedFontPaletteValuesDescriptor(&'a str, ParseError<'a>),
+ /// A keyframe rule was not valid.
+ InvalidKeyframeRule(&'a str, ParseError<'a>),
+ /// A font feature values rule was not valid.
+ InvalidFontFeatureValuesRule(&'a str, ParseError<'a>),
+ /// A keyframe property declaration was not recognized.
+ UnsupportedKeyframePropertyDeclaration(&'a str, ParseError<'a>),
+ /// A rule was invalid for some reason.
+ InvalidRule(&'a str, ParseError<'a>),
+ /// A rule was not recognized.
+ UnsupportedRule(&'a str, ParseError<'a>),
+ /// A viewport descriptor declaration was not recognized.
+ UnsupportedViewportDescriptorDeclaration(&'a str, ParseError<'a>),
+ /// A counter style descriptor declaration was not recognized.
+ UnsupportedCounterStyleDescriptorDeclaration(&'a str, ParseError<'a>),
+ /// A counter style rule had no symbols.
+ InvalidCounterStyleWithoutSymbols(String),
+ /// A counter style rule had less than two symbols.
+ InvalidCounterStyleNotEnoughSymbols(String),
+ /// A counter style rule did not have additive-symbols.
+ InvalidCounterStyleWithoutAdditiveSymbols,
+ /// A counter style rule had extends with symbols.
+ InvalidCounterStyleExtendsWithSymbols,
+ /// A counter style rule had extends with additive-symbols.
+ InvalidCounterStyleExtendsWithAdditiveSymbols,
+ /// A media rule was invalid for some reason.
+ InvalidMediaRule(&'a str, ParseError<'a>),
+ /// A value was not recognized.
+ UnsupportedValue(&'a str, ParseError<'a>),
+ /// A never-matching `:host` selector was found.
+ NeverMatchingHostSelector(String),
+}
+
+impl<'a> fmt::Display for ContextualParseError<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fn token_to_str(t: &Token, f: &mut fmt::Formatter) -> fmt::Result {
+ match *t {
+ Token::Ident(ref i) => write!(f, "identifier {}", i),
+ Token::AtKeyword(ref kw) => write!(f, "keyword @{}", kw),
+ Token::Hash(ref h) => write!(f, "hash #{}", h),
+ Token::IDHash(ref h) => write!(f, "id selector #{}", h),
+ Token::QuotedString(ref s) => write!(f, "quoted string \"{}\"", s),
+ Token::UnquotedUrl(ref u) => write!(f, "url {}", u),
+ Token::Delim(ref d) => write!(f, "delimiter {}", d),
+ Token::Number {
+ int_value: Some(i), ..
+ } => write!(f, "number {}", i),
+ Token::Number { value, .. } => write!(f, "number {}", value),
+ Token::Percentage {
+ int_value: Some(i), ..
+ } => write!(f, "percentage {}", i),
+ Token::Percentage { unit_value, .. } => {
+ write!(f, "percentage {}", unit_value * 100.)
+ },
+ Token::Dimension {
+ value, ref unit, ..
+ } => write!(f, "dimension {}{}", value, unit),
+ Token::WhiteSpace(_) => write!(f, "whitespace"),
+ Token::Comment(_) => write!(f, "comment"),
+ Token::Colon => write!(f, "colon (:)"),
+ Token::Semicolon => write!(f, "semicolon (;)"),
+ Token::Comma => write!(f, "comma (,)"),
+ Token::IncludeMatch => write!(f, "include match (~=)"),
+ Token::DashMatch => write!(f, "dash match (|=)"),
+ Token::PrefixMatch => write!(f, "prefix match (^=)"),
+ Token::SuffixMatch => write!(f, "suffix match ($=)"),
+ Token::SubstringMatch => write!(f, "substring match (*=)"),
+ Token::CDO => write!(f, "CDO (<!--)"),
+ Token::CDC => write!(f, "CDC (-->)"),
+ Token::Function(ref name) => write!(f, "function {}", name),
+ Token::ParenthesisBlock => write!(f, "parenthesis ("),
+ Token::SquareBracketBlock => write!(f, "square bracket ["),
+ Token::CurlyBracketBlock => write!(f, "curly bracket {{"),
+ Token::BadUrl(ref _u) => write!(f, "bad url parse error"),
+ Token::BadString(ref _s) => write!(f, "bad string parse error"),
+ Token::CloseParenthesis => write!(f, "unmatched close parenthesis"),
+ Token::CloseSquareBracket => write!(f, "unmatched close square bracket"),
+ Token::CloseCurlyBracket => write!(f, "unmatched close curly bracket"),
+ }
+ }
+
+ fn parse_error_to_str(err: &ParseError, f: &mut fmt::Formatter) -> fmt::Result {
+ match err.kind {
+ ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(ref t)) => {
+ write!(f, "found unexpected ")?;
+ token_to_str(t, f)
+ },
+ ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => {
+ write!(f, "unexpected end of input")
+ },
+ ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(ref i)) => {
+ write!(f, "@ rule invalid: {}", i)
+ },
+ ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid) => {
+ write!(f, "@ rule invalid")
+ },
+ ParseErrorKind::Basic(BasicParseErrorKind::QualifiedRuleInvalid) => {
+ write!(f, "qualified rule invalid")
+ },
+ ParseErrorKind::Custom(ref err) => write!(f, "{:?}", err),
+ }
+ }
+
+ match *self {
+ ContextualParseError::UnsupportedPropertyDeclaration(decl, ref err, _selectors) => {
+ write!(f, "Unsupported property declaration: '{}', ", decl)?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedPropertyDescriptor(decl, ref err) => {
+ write!(
+ f,
+ "Unsupported @property descriptor declaration: '{}', ",
+ decl
+ )?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedFontFaceDescriptor(decl, ref err) => {
+ write!(
+ f,
+ "Unsupported @font-face descriptor declaration: '{}', ",
+ decl
+ )?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedFontFeatureValuesDescriptor(decl, ref err) => {
+ write!(
+ f,
+ "Unsupported @font-feature-values descriptor declaration: '{}', ",
+ decl
+ )?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedFontPaletteValuesDescriptor(decl, ref err) => {
+ write!(
+ f,
+ "Unsupported @font-palette-values descriptor declaration: '{}', ",
+ decl
+ )?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::InvalidKeyframeRule(rule, ref err) => {
+ write!(f, "Invalid keyframe rule: '{}', ", rule)?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::InvalidFontFeatureValuesRule(rule, ref err) => {
+ write!(f, "Invalid font feature value rule: '{}', ", rule)?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedKeyframePropertyDeclaration(decl, ref err) => {
+ write!(f, "Unsupported keyframe property declaration: '{}', ", decl)?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::InvalidRule(rule, ref err) => {
+ write!(f, "Invalid rule: '{}', ", rule)?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedRule(rule, ref err) => {
+ write!(f, "Unsupported rule: '{}', ", rule)?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedViewportDescriptorDeclaration(decl, ref err) => {
+ write!(
+ f,
+ "Unsupported @viewport descriptor declaration: '{}', ",
+ decl
+ )?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(decl, ref err) => {
+ write!(
+ f,
+ "Unsupported @counter-style descriptor declaration: '{}', ",
+ decl
+ )?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::InvalidCounterStyleWithoutSymbols(ref system) => write!(
+ f,
+ "Invalid @counter-style rule: 'system: {}' without 'symbols'",
+ system
+ ),
+ ContextualParseError::InvalidCounterStyleNotEnoughSymbols(ref system) => write!(
+ f,
+ "Invalid @counter-style rule: 'system: {}' less than two 'symbols'",
+ system
+ ),
+ ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols => write!(
+ f,
+ "Invalid @counter-style rule: 'system: additive' without 'additive-symbols'"
+ ),
+ ContextualParseError::InvalidCounterStyleExtendsWithSymbols => write!(
+ f,
+ "Invalid @counter-style rule: 'system: extends …' with 'symbols'"
+ ),
+ ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols => write!(
+ f,
+ "Invalid @counter-style rule: 'system: extends …' with 'additive-symbols'"
+ ),
+ ContextualParseError::InvalidMediaRule(media_rule, ref err) => {
+ write!(f, "Invalid media rule: {}, ", media_rule)?;
+ parse_error_to_str(err, f)
+ },
+ ContextualParseError::UnsupportedValue(_value, ref err) => parse_error_to_str(err, f),
+ ContextualParseError::NeverMatchingHostSelector(ref selector) => {
+ write!(f, ":host selector is not featureless: {}", selector)
+ },
+ }
+ }
+}
+
+/// A generic trait for an error reporter.
+pub trait ParseErrorReporter {
+ /// Called when the style engine detects an error.
+ ///
+ /// Returns the current input being parsed, the source location it was
+ /// reported from, and a message.
+ fn report_error(
+ &self,
+ url: &UrlExtraData,
+ location: SourceLocation,
+ error: ContextualParseError,
+ );
+}
+
+/// An error reporter that uses [the `log` crate](https://github.com/rust-lang-nursery/log)
+/// at `info` level.
+///
+/// This logging is silent by default, and can be enabled with a `RUST_LOG=style=info`
+/// environment variable.
+/// (See [`env_logger`](https://rust-lang-nursery.github.io/log/env_logger/).)
+#[cfg(feature = "servo")]
+pub struct RustLogReporter;
+
+#[cfg(feature = "servo")]
+impl ParseErrorReporter for RustLogReporter {
+ fn report_error(
+ &self,
+ url: &UrlExtraData,
+ location: SourceLocation,
+ error: ContextualParseError,
+ ) {
+ if log_enabled!(log::Level::Info) {
+ info!(
+ "Url:\t{}\n{}:{} {}",
+ url.as_str(),
+ location.line,
+ location.column,
+ error
+ )
+ }
+ }
+}
+
+/// Any warning a selector may generate.
+/// TODO(dshin): Bug 1860634 - Merge with never matching host selector warning, which is part of the rule parser.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub enum SelectorWarningKind {
+ /// Relative Selector with not enough constraint, either outside or inside the selector. e.g. `*:has(.a)`, `.a:has(*)`.
+ /// May cause expensive invalidations for every element inserted and/or removed.
+ UnconstraintedRelativeSelector,
+}
+
+impl SelectorWarningKind {
+ /// Get all warnings for this selector.
+ pub fn from_selector(selector: &Selector<SelectorImpl>) -> Vec<Self> {
+ let mut result = vec![];
+ if UnconstrainedRelativeSelectorVisitor::has_warning(selector, 0, false) {
+ result.push(SelectorWarningKind::UnconstraintedRelativeSelector);
+ }
+ result
+ }
+}
+
+/// Per-compound state for finding unconstrained relative selectors.
+struct PerCompoundState {
+ /// Is there a relative selector in this compound?
+ relative_selector_found: bool,
+ /// Is this compound constrained in any way?
+ constrained: bool,
+ /// Nested below, or inside relative selector?
+ in_relative_selector: bool,
+}
+
+impl PerCompoundState {
+ fn new(in_relative_selector: bool) -> Self {
+ Self {
+ relative_selector_found: false,
+ constrained: false,
+ in_relative_selector,
+ }
+ }
+}
+
+/// Visitor to check if there's any unconstrained relative selector.
+struct UnconstrainedRelativeSelectorVisitor {
+ compound_state: PerCompoundState,
+}
+
+impl UnconstrainedRelativeSelectorVisitor {
+ fn new(in_relative_selector: bool) -> Self {
+ Self {
+ compound_state: PerCompoundState::new(in_relative_selector),
+ }
+ }
+
+ fn has_warning(
+ selector: &Selector<SelectorImpl>,
+ offset: usize,
+ in_relative_selector: bool,
+ ) -> bool {
+ let relative_selector = matches!(
+ selector.iter_raw_parse_order_from(0).next().unwrap(),
+ Component::RelativeSelectorAnchor
+ );
+ debug_assert!(
+ !relative_selector || offset == 0,
+ "Checking relative selector from non-rightmost?"
+ );
+ let mut visitor = Self::new(in_relative_selector);
+ let mut iter = if relative_selector {
+ selector.iter_skip_relative_selector_anchor()
+ } else {
+ selector.iter_from(offset)
+ };
+ loop {
+ visitor.compound_state = PerCompoundState::new(in_relative_selector);
+
+ for s in &mut iter {
+ s.visit(&mut visitor);
+ }
+
+ if (visitor.compound_state.relative_selector_found ||
+ visitor.compound_state.in_relative_selector) &&
+ !visitor.compound_state.constrained
+ {
+ return true;
+ }
+
+ if iter.next_sequence().is_none() {
+ break;
+ }
+ }
+ false
+ }
+}
+
+impl SelectorVisitor for UnconstrainedRelativeSelectorVisitor {
+ type Impl = SelectorImpl;
+
+ fn visit_simple_selector(&mut self, c: &Component<Self::Impl>) -> bool {
+ match c {
+ // Deferred to visit_selector_list
+ Component::Is(..) |
+ Component::Where(..) |
+ Component::Negation(..) |
+ Component::Has(..) => (),
+ Component::ExplicitUniversalType => (),
+ _ => self.compound_state.constrained |= true,
+ };
+ true
+ }
+
+ fn visit_selector_list(
+ &mut self,
+ _list_kind: SelectorListKind,
+ list: &[Selector<Self::Impl>],
+ ) -> bool {
+ let mut all_constrained = true;
+ for s in list {
+ let mut offset = 0;
+ // First, check the rightmost compound for constraint at this level.
+ if !self.compound_state.in_relative_selector {
+ let mut nested = Self::new(false);
+ let mut iter = s.iter();
+ loop {
+ for c in &mut iter {
+ c.visit(&mut nested);
+ offset += 1;
+ }
+
+ let c = iter.next_sequence();
+ offset += 1;
+ if c.map_or(true, |c| !c.is_pseudo_element()) {
+ break;
+ }
+ }
+ // Every single selector in the list must be constrained.
+ all_constrained &= nested.compound_state.constrained;
+ }
+
+ if offset >= s.len() {
+ continue;
+ }
+
+ // Then, recurse in to check at the deeper level.
+ if Self::has_warning(s, offset, self.compound_state.in_relative_selector) {
+ self.compound_state.constrained = false;
+ if !self.compound_state.in_relative_selector {
+ self.compound_state.relative_selector_found = true;
+ }
+ return false;
+ }
+ }
+ self.compound_state.constrained |= all_constrained;
+ true
+ }
+
+ fn visit_relative_selector_list(&mut self, list: &[RelativeSelector<Self::Impl>]) -> bool {
+ debug_assert!(
+ !self.compound_state.in_relative_selector,
+ "Nested relative selector"
+ );
+ self.compound_state.relative_selector_found = true;
+
+ for rs in list {
+ // If the inside is unconstrained, we are unconstrained no matter what.
+ if Self::has_warning(&rs.selector, 0, true) {
+ self.compound_state.constrained = false;
+ return false;
+ }
+ }
+ true
+ }
+}
diff --git a/servo/components/style/font_face.rs b/servo/components/style/font_face.rs
new file mode 100644
index 0000000000..5f1efdd266
--- /dev/null
+++ b/servo/components/style/font_face.rs
@@ -0,0 +1,807 @@
+/* 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/. */
+
+//! The [`@font-face`][ff] at-rule.
+//!
+//! [ff]: https://drafts.csswg.org/css-fonts/#at-font-face-rule
+
+use crate::error_reporting::ContextualParseError;
+use crate::parser::{Parse, ParserContext};
+#[cfg(feature = "gecko")]
+use crate::properties::longhands::font_language_override;
+use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::values::computed::font::{FamilyName, FontStretch};
+use crate::values::generics::font::FontStyle as GenericFontStyle;
+use crate::values::specified::font::SpecifiedFontStyle;
+use crate::values::specified::font::{
+ AbsoluteFontWeight, FontStretch as SpecifiedFontStretch, MetricsOverride,
+};
+#[cfg(feature = "gecko")]
+use crate::values::specified::font::{FontFeatureSettings, FontVariationSettings};
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::{Angle, NonNegativePercentage};
+#[cfg(feature = "gecko")]
+use cssparser::UnicodeRange;
+use cssparser::{
+ AtRuleParser, CowRcStr, DeclarationParser, Parser, QualifiedRuleParser, RuleBodyItemParser,
+ RuleBodyParser, SourceLocation,
+};
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError};
+use style_traits::{StyleParseErrorKind, ToCss};
+
+/// A source for a font-face rule.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)]
+pub enum Source {
+ /// A `url()` source.
+ Url(UrlSource),
+ /// A `local()` source.
+ #[css(function)]
+ Local(FamilyName),
+}
+
+/// A list of sources for the font-face src descriptor.
+#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)]
+#[css(comma)]
+pub struct SourceList(#[css(iterable)] pub Vec<Source>);
+
+// We can't just use OneOrMoreSeparated to derive Parse for the Source list,
+// because we want to filter out components that parsed as None, then fail if no
+// valid components remain. So we provide our own implementation here.
+impl Parse for SourceList {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Parse the comma-separated list, then let filter_map discard any None items.
+ let list = input
+ .parse_comma_separated(|input| {
+ let s = input.parse_entirely(|input| Source::parse(context, input));
+ while input.next().is_ok() {}
+ Ok(s.ok())
+ })?
+ .into_iter()
+ .filter_map(|s| s)
+ .collect::<Vec<Source>>();
+ if list.is_empty() {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(SourceList(list))
+ }
+ }
+}
+
+/// Keywords for the font-face src descriptor's format() function.
+/// ('None' and 'Unknown' are for internal use in gfx, not exposed to CSS.)
+#[derive(Clone, Copy, Debug, Eq, Parse, PartialEq, ToCss, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum FontFaceSourceFormatKeyword {
+ #[css(skip)]
+ None,
+ Collection,
+ EmbeddedOpentype,
+ Opentype,
+ Svg,
+ Truetype,
+ Woff,
+ Woff2,
+ #[css(skip)]
+ Unknown,
+}
+
+/// Flags for the @font-face tech() function, indicating font technologies
+/// required by the resource.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)]
+#[repr(C)]
+pub struct FontFaceSourceTechFlags(u16);
+bitflags! {
+ impl FontFaceSourceTechFlags: u16 {
+ /// Font requires OpenType feature support.
+ const FEATURES_OPENTYPE = 1 << 0;
+ /// Font requires Apple Advanced Typography support.
+ const FEATURES_AAT = 1 << 1;
+ /// Font requires Graphite shaping support.
+ const FEATURES_GRAPHITE = 1 << 2;
+ /// Font requires COLRv0 rendering support (simple list of colored layers).
+ const COLOR_COLRV0 = 1 << 3;
+ /// Font requires COLRv1 rendering support (graph of paint operations).
+ const COLOR_COLRV1 = 1 << 4;
+ /// Font requires SVG glyph rendering support.
+ const COLOR_SVG = 1 << 5;
+ /// Font has bitmap glyphs in 'sbix' format.
+ const COLOR_SBIX = 1 << 6;
+ /// Font has bitmap glyphs in 'CBDT' format.
+ const COLOR_CBDT = 1 << 7;
+ /// Font requires OpenType Variations support.
+ const VARIATIONS = 1 << 8;
+ /// Font requires CPAL palette selection support.
+ const PALETTES = 1 << 9;
+ /// Font requires support for incremental downloading.
+ const INCREMENTAL = 1 << 10;
+ }
+}
+
+impl FontFaceSourceTechFlags {
+ /// Parse a single font-technology keyword and return its flag.
+ pub fn parse_one<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "features-opentype" => Self::FEATURES_OPENTYPE,
+ "features-aat" => Self::FEATURES_AAT,
+ "features-graphite" => Self::FEATURES_GRAPHITE,
+ "color-colrv0" => Self::COLOR_COLRV0,
+ "color-colrv1" => Self::COLOR_COLRV1,
+ "color-svg" => Self::COLOR_SVG,
+ "color-sbix" => Self::COLOR_SBIX,
+ "color-cbdt" => Self::COLOR_CBDT,
+ "variations" => Self::VARIATIONS,
+ "palettes" => Self::PALETTES,
+ "incremental" => Self::INCREMENTAL,
+ })
+ }
+}
+
+impl Parse for FontFaceSourceTechFlags {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ // We don't actually care about the return value of parse_comma_separated,
+ // because we insert the flags into result as we go.
+ let mut result = Self::empty();
+ input.parse_comma_separated(|input| {
+ let flag = Self::parse_one(input)?;
+ result.insert(flag);
+ Ok(())
+ })?;
+ if !result.is_empty() {
+ Ok(result)
+ } else {
+ Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+#[allow(unused_assignments)]
+impl ToCss for FontFaceSourceTechFlags {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let mut first = true;
+
+ macro_rules! write_if_flag {
+ ($s:expr => $f:ident) => {
+ if self.contains(Self::$f) {
+ if first {
+ first = false;
+ } else {
+ dest.write_str(", ")?;
+ }
+ dest.write_str($s)?;
+ }
+ };
+ }
+
+ write_if_flag!("features-opentype" => FEATURES_OPENTYPE);
+ write_if_flag!("features-aat" => FEATURES_AAT);
+ write_if_flag!("features-graphite" => FEATURES_GRAPHITE);
+ write_if_flag!("color-colrv0" => COLOR_COLRV0);
+ write_if_flag!("color-colrv1" => COLOR_COLRV1);
+ write_if_flag!("color-svg" => COLOR_SVG);
+ write_if_flag!("color-sbix" => COLOR_SBIX);
+ write_if_flag!("color-cbdt" => COLOR_CBDT);
+ write_if_flag!("variations" => VARIATIONS);
+ write_if_flag!("palettes" => PALETTES);
+ write_if_flag!("incremental" => INCREMENTAL);
+
+ Ok(())
+ }
+}
+
+/// A POD representation for Gecko. All pointers here are non-owned and as such
+/// can't outlive the rule they came from, but we can't enforce that via C++.
+///
+/// All the strings are of course utf8.
+#[cfg(feature = "gecko")]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum FontFaceSourceListComponent {
+ Url(*const crate::gecko::url::CssUrl),
+ Local(*mut crate::gecko_bindings::structs::nsAtom),
+ FormatHintKeyword(FontFaceSourceFormatKeyword),
+ FormatHintString {
+ length: usize,
+ utf8_bytes: *const u8,
+ },
+ TechFlags(FontFaceSourceTechFlags),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum FontFaceSourceFormat {
+ Keyword(FontFaceSourceFormatKeyword),
+ String(String),
+}
+
+/// A `UrlSource` represents a font-face source that has been specified with a
+/// `url()` function.
+///
+/// <https://drafts.csswg.org/css-fonts/#src-desc>
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(Clone, Debug, Eq, PartialEq, ToShmem)]
+pub struct UrlSource {
+ /// The specified url.
+ pub url: SpecifiedUrl,
+ /// The format hint specified with the `format()` function, if present.
+ pub format_hint: Option<FontFaceSourceFormat>,
+ /// The font technology flags specified with the `tech()` function, if any.
+ pub tech_flags: FontFaceSourceTechFlags,
+}
+
+impl ToCss for UrlSource {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.url.to_css(dest)?;
+ if let Some(hint) = &self.format_hint {
+ dest.write_str(" format(")?;
+ hint.to_css(dest)?;
+ dest.write_char(')')?;
+ }
+ if !self.tech_flags.is_empty() {
+ dest.write_str(" tech(")?;
+ self.tech_flags.to_css(dest)?;
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+}
+
+/// A font-display value for a @font-face rule.
+/// The font-display descriptor determines how a font face is displayed based
+/// on whether and when it is downloaded and ready to use.
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss, ToShmem,
+)]
+#[repr(u8)]
+pub enum FontDisplay {
+ Auto,
+ Block,
+ Swap,
+ Fallback,
+ Optional,
+}
+
+macro_rules! impl_range {
+ ($range:ident, $component:ident) => {
+ impl Parse for $range {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let first = $component::parse(context, input)?;
+ let second = input
+ .try_parse(|input| $component::parse(context, input))
+ .unwrap_or_else(|_| first.clone());
+ Ok($range(first, second))
+ }
+ }
+ impl ToCss for $range {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.0.to_css(dest)?;
+ if self.0 != self.1 {
+ dest.write_char(' ')?;
+ self.1.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+ };
+}
+
+/// The font-weight descriptor:
+///
+/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-weight
+#[derive(Clone, Debug, PartialEq, ToShmem)]
+pub struct FontWeightRange(pub AbsoluteFontWeight, pub AbsoluteFontWeight);
+impl_range!(FontWeightRange, AbsoluteFontWeight);
+
+/// The computed representation of the above so Gecko can read them easily.
+///
+/// This one is needed because cbindgen doesn't know how to generate
+/// specified::Number.
+#[repr(C)]
+#[allow(missing_docs)]
+pub struct ComputedFontWeightRange(f32, f32);
+
+#[inline]
+fn sort_range<T: PartialOrd>(a: T, b: T) -> (T, T) {
+ if a > b {
+ (b, a)
+ } else {
+ (a, b)
+ }
+}
+
+impl FontWeightRange {
+ /// Returns a computed font-stretch range.
+ pub fn compute(&self) -> ComputedFontWeightRange {
+ let (min, max) = sort_range(self.0.compute().value(), self.1.compute().value());
+ ComputedFontWeightRange(min, max)
+ }
+}
+
+/// The font-stretch descriptor:
+///
+/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-stretch
+#[derive(Clone, Debug, PartialEq, ToShmem)]
+pub struct FontStretchRange(pub SpecifiedFontStretch, pub SpecifiedFontStretch);
+impl_range!(FontStretchRange, SpecifiedFontStretch);
+
+/// The computed representation of the above, so that Gecko can read them
+/// easily.
+#[repr(C)]
+#[allow(missing_docs)]
+pub struct ComputedFontStretchRange(FontStretch, FontStretch);
+
+impl FontStretchRange {
+ /// Returns a computed font-stretch range.
+ pub fn compute(&self) -> ComputedFontStretchRange {
+ fn compute_stretch(s: &SpecifiedFontStretch) -> FontStretch {
+ match *s {
+ SpecifiedFontStretch::Keyword(ref kw) => kw.compute(),
+ SpecifiedFontStretch::Stretch(ref p) => FontStretch::from_percentage(p.0.get()),
+ SpecifiedFontStretch::System(..) => unreachable!(),
+ }
+ }
+
+ let (min, max) = sort_range(compute_stretch(&self.0), compute_stretch(&self.1));
+ ComputedFontStretchRange(min, max)
+ }
+}
+
+/// The font-style descriptor:
+///
+/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-style
+#[derive(Clone, Debug, PartialEq, ToShmem)]
+#[allow(missing_docs)]
+pub enum FontStyle {
+ Normal,
+ Italic,
+ Oblique(Angle, Angle),
+}
+
+/// The computed representation of the above, with angles in degrees, so that
+/// Gecko can read them easily.
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum ComputedFontStyleDescriptor {
+ Normal,
+ Italic,
+ Oblique(f32, f32),
+}
+
+impl Parse for FontStyle {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let style = SpecifiedFontStyle::parse(context, input)?;
+ Ok(match style {
+ GenericFontStyle::Normal => FontStyle::Normal,
+ GenericFontStyle::Italic => FontStyle::Italic,
+ GenericFontStyle::Oblique(angle) => {
+ let second_angle = input
+ .try_parse(|input| SpecifiedFontStyle::parse_angle(context, input))
+ .unwrap_or_else(|_| angle.clone());
+
+ FontStyle::Oblique(angle, second_angle)
+ },
+ })
+ }
+}
+
+impl ToCss for FontStyle {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ FontStyle::Normal => dest.write_str("normal"),
+ FontStyle::Italic => dest.write_str("italic"),
+ FontStyle::Oblique(ref first, ref second) => {
+ dest.write_str("oblique")?;
+ if *first != SpecifiedFontStyle::default_angle() || first != second {
+ dest.write_char(' ')?;
+ first.to_css(dest)?;
+ }
+ if first != second {
+ dest.write_char(' ')?;
+ second.to_css(dest)?;
+ }
+ Ok(())
+ },
+ }
+ }
+}
+
+impl FontStyle {
+ /// Returns a computed font-style descriptor.
+ pub fn compute(&self) -> ComputedFontStyleDescriptor {
+ match *self {
+ FontStyle::Normal => ComputedFontStyleDescriptor::Normal,
+ FontStyle::Italic => ComputedFontStyleDescriptor::Italic,
+ FontStyle::Oblique(ref first, ref second) => {
+ let (min, max) = sort_range(
+ SpecifiedFontStyle::compute_angle_degrees(first),
+ SpecifiedFontStyle::compute_angle_degrees(second),
+ );
+ ComputedFontStyleDescriptor::Oblique(min, max)
+ },
+ }
+ }
+}
+
+/// Parse the block inside a `@font-face` rule.
+///
+/// Note that the prelude parsing code lives in the `stylesheets` module.
+pub fn parse_font_face_block(
+ context: &ParserContext,
+ input: &mut Parser,
+ location: SourceLocation,
+) -> FontFaceRuleData {
+ let mut rule = FontFaceRuleData::empty(location);
+ {
+ let mut parser = FontFaceRuleParser {
+ context,
+ rule: &mut rule,
+ };
+ let mut iter = RuleBodyParser::new(input, &mut parser);
+ while let Some(declaration) = iter.next() {
+ if let Err((error, slice)) = declaration {
+ let location = error.location;
+ let error = ContextualParseError::UnsupportedFontFaceDescriptor(slice, error);
+ context.log_css_error(location, error)
+ }
+ }
+ }
+ rule
+}
+
+/// A @font-face rule that is known to have font-family and src declarations.
+#[cfg(feature = "servo")]
+pub struct FontFace<'a>(&'a FontFaceRuleData);
+
+/// A list of effective sources that we send over through IPC to the font cache.
+#[cfg(feature = "servo")]
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+pub struct EffectiveSources(SourceList);
+
+#[cfg(feature = "servo")]
+impl<'a> FontFace<'a> {
+ /// Returns the list of effective sources for that font-face, that is the
+ /// sources which don't list any format hint, or the ones which list at
+ /// least "truetype" or "opentype".
+ pub fn effective_sources(&self) -> EffectiveSources {
+ EffectiveSources(
+ self.sources()
+ .iter()
+ .rev()
+ .filter(|source| {
+ if let Source::Url(ref url_source) = **source {
+ // We support only opentype fonts and truetype is an alias for
+ // that format. Sources without format hints need to be
+ // downloaded in case we support them.
+ url_source.format_hint.as_ref().map_or(true, |hint| {
+ hint == "truetype" || hint == "opentype" || hint == "woff"
+ })
+ } else {
+ true
+ }
+ })
+ .cloned()
+ .collect(),
+ )
+ }
+}
+
+#[cfg(feature = "servo")]
+impl Iterator for EffectiveSources {
+ type Item = Source;
+ fn next(&mut self) -> Option<Source> {
+ self.0.pop()
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (self.0.len(), Some(self.0.len()))
+ }
+}
+
+struct FontFaceRuleParser<'a, 'b: 'a> {
+ context: &'a ParserContext<'b>,
+ rule: &'a mut FontFaceRuleData,
+}
+
+/// Default methods reject all at rules.
+impl<'a, 'b, 'i> AtRuleParser<'i> for FontFaceRuleParser<'a, 'b> {
+ type Prelude = ();
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> QualifiedRuleParser<'i> for FontFaceRuleParser<'a, 'b> {
+ type Prelude = ();
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>>
+ for FontFaceRuleParser<'a, 'b>
+{
+ fn parse_qualified(&self) -> bool {
+ false
+ }
+ fn parse_declarations(&self) -> bool {
+ true
+ }
+}
+
+impl Parse for Source {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Source, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_function_matching("local"))
+ .is_ok()
+ {
+ return input
+ .parse_nested_block(|input| FamilyName::parse(context, input))
+ .map(Source::Local);
+ }
+
+ let url = SpecifiedUrl::parse(context, input)?;
+
+ // Parsing optional format()
+ let format_hint = if input
+ .try_parse(|input| input.expect_function_matching("format"))
+ .is_ok()
+ {
+ input.parse_nested_block(|input| {
+ if let Ok(kw) = input.try_parse(FontFaceSourceFormatKeyword::parse) {
+ Ok(Some(FontFaceSourceFormat::Keyword(kw)))
+ } else {
+ let s = input.expect_string()?.as_ref().to_owned();
+ Ok(Some(FontFaceSourceFormat::String(s)))
+ }
+ })?
+ } else {
+ None
+ };
+
+ // Parse optional tech()
+ let tech_flags = if static_prefs::pref!("layout.css.font-tech.enabled") &&
+ input
+ .try_parse(|input| input.expect_function_matching("tech"))
+ .is_ok()
+ {
+ input.parse_nested_block(|input| FontFaceSourceTechFlags::parse(context, input))?
+ } else {
+ FontFaceSourceTechFlags::empty()
+ };
+
+ Ok(Source::Url(UrlSource {
+ url,
+ format_hint,
+ tech_flags,
+ }))
+ }
+}
+
+macro_rules! is_descriptor_enabled {
+ ("font-variation-settings") => {
+ static_prefs::pref!("layout.css.font-variations.enabled")
+ };
+ ("size-adjust") => {
+ static_prefs::pref!("layout.css.size-adjust.enabled")
+ };
+ ($name:tt) => {
+ true
+ };
+}
+
+macro_rules! font_face_descriptors_common {
+ (
+ $( #[$doc: meta] $name: tt $ident: ident / $gecko_ident: ident: $ty: ty, )*
+ ) => {
+ /// Data inside a `@font-face` rule.
+ ///
+ /// <https://drafts.csswg.org/css-fonts/#font-face-rule>
+ #[derive(Clone, Debug, PartialEq, ToShmem)]
+ pub struct FontFaceRuleData {
+ $(
+ #[$doc]
+ pub $ident: Option<$ty>,
+ )*
+ /// Line and column of the @font-face rule source code.
+ pub source_location: SourceLocation,
+ }
+
+ impl FontFaceRuleData {
+ /// Create an empty font-face rule
+ pub fn empty(location: SourceLocation) -> Self {
+ FontFaceRuleData {
+ $(
+ $ident: None,
+ )*
+ source_location: location,
+ }
+ }
+
+ /// Serialization of declarations in the FontFaceRule
+ pub fn decl_to_css(&self, dest: &mut CssStringWriter) -> fmt::Result {
+ $(
+ if let Some(ref value) = self.$ident {
+ dest.write_str(concat!($name, ": "))?;
+ value.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str("; ")?;
+ }
+ )*
+ Ok(())
+ }
+ }
+
+ impl<'a, 'b, 'i> DeclarationParser<'i> for FontFaceRuleParser<'a, 'b> {
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_value<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ match_ignore_ascii_case! { &*name,
+ $(
+ $name if is_descriptor_enabled!($name) => {
+ // DeclarationParser also calls parse_entirely
+ // so we’d normally not need to,
+ // but in this case we do because we set the value as a side effect
+ // rather than returning it.
+ let value = input.parse_entirely(|i| Parse::parse(self.context, i))?;
+ self.rule.$ident = Some(value)
+ },
+ )*
+ _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl ToCssWithGuard for FontFaceRuleData {
+ // Serialization of FontFaceRule is not specced.
+ fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@font-face { ")?;
+ self.decl_to_css(dest)?;
+ dest.write_char('}')
+ }
+}
+
+macro_rules! font_face_descriptors {
+ (
+ mandatory descriptors = [
+ $( #[$m_doc: meta] $m_name: tt $m_ident: ident / $m_gecko_ident: ident: $m_ty: ty, )*
+ ]
+ optional descriptors = [
+ $( #[$o_doc: meta] $o_name: tt $o_ident: ident / $o_gecko_ident: ident: $o_ty: ty, )*
+ ]
+ ) => {
+ font_face_descriptors_common! {
+ $( #[$m_doc] $m_name $m_ident / $m_gecko_ident: $m_ty, )*
+ $( #[$o_doc] $o_name $o_ident / $o_gecko_ident: $o_ty, )*
+ }
+
+ impl FontFaceRuleData {
+ /// Per https://github.com/w3c/csswg-drafts/issues/1133 an @font-face rule
+ /// is valid as far as the CSS parser is concerned even if it doesn’t have
+ /// a font-family or src declaration.
+ ///
+ /// However both are required for the rule to represent an actual font face.
+ #[cfg(feature = "servo")]
+ pub fn font_face(&self) -> Option<FontFace> {
+ if $( self.$m_ident.is_some() )&&* {
+ Some(FontFace(self))
+ } else {
+ None
+ }
+ }
+ }
+
+ #[cfg(feature = "servo")]
+ impl<'a> FontFace<'a> {
+ $(
+ #[$m_doc]
+ pub fn $m_ident(&self) -> &$m_ty {
+ self.0 .$m_ident.as_ref().unwrap()
+ }
+ )*
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+font_face_descriptors! {
+ mandatory descriptors = [
+ /// The name of this font face
+ "font-family" family / mFamily: FamilyName,
+
+ /// The alternative sources for this font face.
+ "src" sources / mSrc: SourceList,
+ ]
+ optional descriptors = [
+ /// The style of this font face.
+ "font-style" style / mStyle: FontStyle,
+
+ /// The weight of this font face.
+ "font-weight" weight / mWeight: FontWeightRange,
+
+ /// The stretch of this font face.
+ "font-stretch" stretch / mStretch: FontStretchRange,
+
+ /// The display of this font face.
+ "font-display" display / mDisplay: FontDisplay,
+
+ /// The ranges of code points outside of which this font face should not be used.
+ "unicode-range" unicode_range / mUnicodeRange: Vec<UnicodeRange>,
+
+ /// The feature settings of this font face.
+ "font-feature-settings" feature_settings / mFontFeatureSettings: FontFeatureSettings,
+
+ /// The variation settings of this font face.
+ "font-variation-settings" variation_settings / mFontVariationSettings: FontVariationSettings,
+
+ /// The language override of this font face.
+ "font-language-override" language_override / mFontLanguageOverride: font_language_override::SpecifiedValue,
+
+ /// The ascent override for this font face.
+ "ascent-override" ascent_override / mAscentOverride: MetricsOverride,
+
+ /// The descent override for this font face.
+ "descent-override" descent_override / mDescentOverride: MetricsOverride,
+
+ /// The line-gap override for this font face.
+ "line-gap-override" line_gap_override / mLineGapOverride: MetricsOverride,
+
+ /// The size adjustment for this font face.
+ "size-adjust" size_adjust / mSizeAdjust: NonNegativePercentage,
+ ]
+}
+
+#[cfg(feature = "servo")]
+font_face_descriptors! {
+ mandatory descriptors = [
+ /// The name of this font face
+ "font-family" family / mFamily: FamilyName,
+
+ /// The alternative sources for this font face.
+ "src" sources / mSrc: SourceList,
+ ]
+ optional descriptors = [
+ ]
+}
diff --git a/servo/components/style/font_metrics.rs b/servo/components/style/font_metrics.rs
new file mode 100644
index 0000000000..391d3653ee
--- /dev/null
+++ b/servo/components/style/font_metrics.rs
@@ -0,0 +1,58 @@
+/* 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/. */
+
+//! Access to font metrics from the style system.
+
+#![deny(missing_docs)]
+
+use crate::values::computed::Length;
+
+/// Represents the font metrics that style needs from a font to compute the
+/// value of certain CSS units like `ex`.
+#[derive(Clone, Debug, PartialEq)]
+pub struct FontMetrics {
+ /// The x-height of the font.
+ pub x_height: Option<Length>,
+ /// The zero advance. This is usually writing mode dependent
+ pub zero_advance_measure: Option<Length>,
+ /// The cap-height of the font.
+ pub cap_height: Option<Length>,
+ /// The ideographic-width of the font.
+ pub ic_width: Option<Length>,
+ /// The ascent of the font (a value is always available for this).
+ pub ascent: Length,
+ /// Script scale down factor for math-depth 1.
+ /// https://w3c.github.io/mathml-core/#dfn-scriptpercentscaledown
+ pub script_percent_scale_down: Option<f32>,
+ /// Script scale down factor for math-depth 2.
+ /// https://w3c.github.io/mathml-core/#dfn-scriptscriptpercentscaledown
+ pub script_script_percent_scale_down: Option<f32>,
+}
+
+impl Default for FontMetrics {
+ fn default() -> Self {
+ FontMetrics {
+ x_height: None,
+ zero_advance_measure: None,
+ cap_height: None,
+ ic_width: None,
+ ascent: Length::new(0.0),
+ script_percent_scale_down: None,
+ script_script_percent_scale_down: None,
+ }
+ }
+}
+
+/// Type of font metrics to retrieve.
+#[derive(Clone, Debug, PartialEq)]
+pub enum FontMetricsOrientation {
+ /// Get metrics for horizontal or vertical according to the Context's
+ /// writing mode, using horizontal metrics for vertical/mixed
+ MatchContextPreferHorizontal,
+ /// Get metrics for horizontal or vertical according to the Context's
+ /// writing mode, using vertical metrics for vertical/mixed
+ MatchContextPreferVertical,
+ /// Force getting horizontal metrics.
+ Horizontal,
+}
diff --git a/servo/components/style/gecko/arc_types.rs b/servo/components/style/gecko/arc_types.rs
new file mode 100644
index 0000000000..24bf22d69a
--- /dev/null
+++ b/servo/components/style/gecko/arc_types.rs
@@ -0,0 +1,171 @@
+/* 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/. */
+
+//! This file lists all arc FFI types and defines corresponding addref and release functions. This
+//! list loosely corresponds to ServoLockedArcTypeList.h file in Gecko.
+
+#![allow(non_snake_case, missing_docs)]
+
+use crate::gecko::url::CssUrlData;
+use crate::media_queries::MediaList;
+use crate::properties::animated_properties::AnimationValue;
+use crate::properties::{ComputedValues, PropertyDeclarationBlock};
+use crate::shared_lock::Locked;
+use crate::stylesheets::keyframes_rule::Keyframe;
+use crate::stylesheets::{
+ ContainerRule, CounterStyleRule, CssRules, DocumentRule, FontFaceRule, FontFeatureValuesRule,
+ FontPaletteValuesRule, ImportRule, KeyframesRule, LayerBlockRule, LayerStatementRule,
+ MediaRule, NamespaceRule, PageRule, PropertyRule, StyleRule, StylesheetContents, SupportsRule,
+};
+use servo_arc::Arc;
+
+macro_rules! impl_simple_arc_ffi {
+ ($ty:ty, $addref:ident, $release:ident) => {
+ #[no_mangle]
+ pub unsafe extern "C" fn $addref(obj: *const $ty) {
+ std::mem::forget(Arc::from_raw_addrefed(obj));
+ }
+
+ #[no_mangle]
+ pub unsafe extern "C" fn $release(obj: *const $ty) {
+ let _ = Arc::from_raw(obj);
+ }
+ };
+}
+
+macro_rules! impl_locked_arc_ffi {
+ ($servo_type:ty, $alias:ident, $addref:ident, $release:ident) => {
+ /// A simple alias for a locked type.
+ pub type $alias = Locked<$servo_type>;
+ impl_simple_arc_ffi!($alias, $addref, $release);
+ };
+}
+
+impl_locked_arc_ffi!(
+ CssRules,
+ LockedCssRules,
+ Servo_CssRules_AddRef,
+ Servo_CssRules_Release
+);
+impl_locked_arc_ffi!(
+ PropertyDeclarationBlock,
+ LockedDeclarationBlock,
+ Servo_DeclarationBlock_AddRef,
+ Servo_DeclarationBlock_Release
+);
+impl_locked_arc_ffi!(
+ StyleRule,
+ LockedStyleRule,
+ Servo_StyleRule_AddRef,
+ Servo_StyleRule_Release
+);
+impl_locked_arc_ffi!(
+ ImportRule,
+ LockedImportRule,
+ Servo_ImportRule_AddRef,
+ Servo_ImportRule_Release
+);
+impl_locked_arc_ffi!(
+ Keyframe,
+ LockedKeyframe,
+ Servo_Keyframe_AddRef,
+ Servo_Keyframe_Release
+);
+impl_locked_arc_ffi!(
+ KeyframesRule,
+ LockedKeyframesRule,
+ Servo_KeyframesRule_AddRef,
+ Servo_KeyframesRule_Release
+);
+impl_simple_arc_ffi!(
+ LayerBlockRule,
+ Servo_LayerBlockRule_AddRef,
+ Servo_LayerBlockRule_Release
+);
+impl_simple_arc_ffi!(
+ LayerStatementRule,
+ Servo_LayerStatementRule_AddRef,
+ Servo_LayerStatementRule_Release
+);
+impl_locked_arc_ffi!(
+ MediaList,
+ LockedMediaList,
+ Servo_MediaList_AddRef,
+ Servo_MediaList_Release
+);
+impl_simple_arc_ffi!(MediaRule, Servo_MediaRule_AddRef, Servo_MediaRule_Release);
+impl_simple_arc_ffi!(
+ NamespaceRule,
+ Servo_NamespaceRule_AddRef,
+ Servo_NamespaceRule_Release
+);
+impl_locked_arc_ffi!(
+ PageRule,
+ LockedPageRule,
+ Servo_PageRule_AddRef,
+ Servo_PageRule_Release
+);
+impl_simple_arc_ffi!(
+ PropertyRule,
+ Servo_PropertyRule_AddRef,
+ Servo_PropertyRule_Release
+);
+impl_simple_arc_ffi!(
+ SupportsRule,
+ Servo_SupportsRule_AddRef,
+ Servo_SupportsRule_Release
+);
+impl_simple_arc_ffi!(
+ ContainerRule,
+ Servo_ContainerRule_AddRef,
+ Servo_ContainerRule_Release
+);
+impl_simple_arc_ffi!(
+ DocumentRule,
+ Servo_DocumentRule_AddRef,
+ Servo_DocumentRule_Release
+);
+impl_simple_arc_ffi!(
+ FontFeatureValuesRule,
+ Servo_FontFeatureValuesRule_AddRef,
+ Servo_FontFeatureValuesRule_Release
+);
+impl_simple_arc_ffi!(
+ FontPaletteValuesRule,
+ Servo_FontPaletteValuesRule_AddRef,
+ Servo_FontPaletteValuesRule_Release
+);
+impl_locked_arc_ffi!(
+ FontFaceRule,
+ LockedFontFaceRule,
+ Servo_FontFaceRule_AddRef,
+ Servo_FontFaceRule_Release
+);
+impl_locked_arc_ffi!(
+ CounterStyleRule,
+ LockedCounterStyleRule,
+ Servo_CounterStyleRule_AddRef,
+ Servo_CounterStyleRule_Release
+);
+
+impl_simple_arc_ffi!(
+ StylesheetContents,
+ Servo_StyleSheetContents_AddRef,
+ Servo_StyleSheetContents_Release
+);
+impl_simple_arc_ffi!(
+ CssUrlData,
+ Servo_CssUrlData_AddRef,
+ Servo_CssUrlData_Release
+);
+impl_simple_arc_ffi!(
+ ComputedValues,
+ Servo_ComputedStyle_AddRef,
+ Servo_ComputedStyle_Release
+);
+impl_simple_arc_ffi!(
+ AnimationValue,
+ Servo_AnimationValue_AddRef,
+ Servo_AnimationValue_Release
+);
diff --git a/servo/components/style/gecko/conversions.rs b/servo/components/style/gecko/conversions.rs
new file mode 100644
index 0000000000..ea3700a323
--- /dev/null
+++ b/servo/components/style/gecko/conversions.rs
@@ -0,0 +1,59 @@
+/* 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/. */
+
+//! This module contains conversion helpers between Servo and Gecko types
+//! Ideally, it would be in geckolib itself, but coherence
+//! forces us to keep the traits and implementations here
+//!
+//! FIXME(emilio): This file should generally just die.
+
+#![allow(unsafe_code)]
+
+use crate::gecko_bindings::structs::{nsresult, Matrix4x4Components};
+use crate::stylesheets::RulesMutateError;
+use crate::values::computed::transform::Matrix3D;
+
+impl From<RulesMutateError> for nsresult {
+ fn from(other: RulesMutateError) -> Self {
+ match other {
+ RulesMutateError::Syntax => nsresult::NS_ERROR_DOM_SYNTAX_ERR,
+ RulesMutateError::IndexSize => nsresult::NS_ERROR_DOM_INDEX_SIZE_ERR,
+ RulesMutateError::HierarchyRequest => nsresult::NS_ERROR_DOM_HIERARCHY_REQUEST_ERR,
+ RulesMutateError::InvalidState => nsresult::NS_ERROR_DOM_INVALID_STATE_ERR,
+ }
+ }
+}
+
+impl<'a> From<&'a Matrix4x4Components> for Matrix3D {
+ fn from(m: &'a Matrix4x4Components) -> Matrix3D {
+ Matrix3D {
+ m11: m[0],
+ m12: m[1],
+ m13: m[2],
+ m14: m[3],
+ m21: m[4],
+ m22: m[5],
+ m23: m[6],
+ m24: m[7],
+ m31: m[8],
+ m32: m[9],
+ m33: m[10],
+ m34: m[11],
+ m41: m[12],
+ m42: m[13],
+ m43: m[14],
+ m44: m[15],
+ }
+ }
+}
+
+impl From<Matrix3D> for Matrix4x4Components {
+ fn from(matrix: Matrix3D) -> Self {
+ [
+ matrix.m11, matrix.m12, matrix.m13, matrix.m14, matrix.m21, matrix.m22, matrix.m23,
+ matrix.m24, matrix.m31, matrix.m32, matrix.m33, matrix.m34, matrix.m41, matrix.m42,
+ matrix.m43, matrix.m44,
+ ]
+ }
+}
diff --git a/servo/components/style/gecko/data.rs b/servo/components/style/gecko/data.rs
new file mode 100644
index 0000000000..c4a5554c5e
--- /dev/null
+++ b/servo/components/style/gecko/data.rs
@@ -0,0 +1,198 @@
+/* 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/. */
+
+//! Data needed to style a Gecko document.
+
+use crate::dom::TElement;
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs::{
+ self, ServoStyleSetSizes, StyleSheet as DomStyleSheet, StyleSheetInfo,
+};
+use crate::invalidation::media_queries::{MediaListKey, ToMediaListKey};
+use crate::media_queries::{Device, MediaList};
+use crate::properties::ComputedValues;
+use crate::selector_parser::SnapshotMap;
+use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards};
+use crate::stylesheets::{StylesheetContents, StylesheetInDocument};
+use crate::stylist::Stylist;
+use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
+use malloc_size_of::MallocSizeOfOps;
+use servo_arc::Arc;
+use std::fmt;
+
+/// Little wrapper to a Gecko style sheet.
+#[derive(Eq, PartialEq)]
+pub struct GeckoStyleSheet(*const DomStyleSheet);
+
+// NOTE(emilio): These are kind of a lie. We allow to make these Send + Sync so that other data
+// structures can also be Send and Sync, but Gecko's stylesheets are main-thread-reference-counted.
+//
+// We assert that we reference-count in the right thread (in the Addref/Release implementations).
+// Sending these to a different thread can't really happen (it could theoretically really happen if
+// we allowed @import rules inside a nested style rule, but that can't happen per spec and would be
+// a parser bug, caught by the asserts).
+unsafe impl Send for GeckoStyleSheet {}
+unsafe impl Sync for GeckoStyleSheet {}
+
+impl fmt::Debug for GeckoStyleSheet {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ let contents = self.contents();
+ formatter
+ .debug_struct("GeckoStyleSheet")
+ .field("origin", &contents.origin)
+ .field("url_data", &*contents.url_data.read())
+ .finish()
+ }
+}
+
+impl ToMediaListKey for crate::gecko::data::GeckoStyleSheet {
+ fn to_media_list_key(&self) -> MediaListKey {
+ use std::mem;
+ unsafe { MediaListKey::from_raw(mem::transmute(self.0)) }
+ }
+}
+
+impl GeckoStyleSheet {
+ /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer.
+ #[inline]
+ pub unsafe fn new(s: *const DomStyleSheet) -> Self {
+ debug_assert!(!s.is_null());
+ bindings::Gecko_StyleSheet_AddRef(s);
+ Self::from_addrefed(s)
+ }
+
+ /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer that
+ /// already holds a strong reference.
+ #[inline]
+ pub unsafe fn from_addrefed(s: *const DomStyleSheet) -> Self {
+ assert!(!s.is_null());
+ GeckoStyleSheet(s)
+ }
+
+ /// HACK(emilio): This is so that we can avoid crashing release due to
+ /// bug 1719963 and can hopefully get a useful report from fuzzers.
+ #[inline]
+ pub fn hack_is_null(&self) -> bool {
+ self.0.is_null()
+ }
+
+ /// Get the raw `StyleSheet` that we're wrapping.
+ pub fn raw(&self) -> &DomStyleSheet {
+ unsafe { &*self.0 }
+ }
+
+ fn inner(&self) -> &StyleSheetInfo {
+ unsafe { &*(self.raw().mInner as *const StyleSheetInfo) }
+ }
+}
+
+impl Drop for GeckoStyleSheet {
+ fn drop(&mut self) {
+ unsafe { bindings::Gecko_StyleSheet_Release(self.0) };
+ }
+}
+
+impl Clone for GeckoStyleSheet {
+ fn clone(&self) -> Self {
+ unsafe { bindings::Gecko_StyleSheet_AddRef(self.0) };
+ GeckoStyleSheet(self.0)
+ }
+}
+
+impl StylesheetInDocument for GeckoStyleSheet {
+ fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
+ use crate::gecko_bindings::structs::mozilla::dom::MediaList as DomMediaList;
+ unsafe {
+ let dom_media_list = self.raw().mMedia.mRawPtr as *const DomMediaList;
+ if dom_media_list.is_null() {
+ return None;
+ }
+ let list = &*(*dom_media_list).mRawList.mRawPtr;
+ Some(list.read_with(guard))
+ }
+ }
+
+ // All the stylesheets Servo knows about are enabled, because that state is
+ // handled externally by Gecko.
+ #[inline]
+ fn enabled(&self) -> bool {
+ true
+ }
+
+ #[inline]
+ fn contents(&self) -> &StylesheetContents {
+ debug_assert!(!self.inner().mContents.mRawPtr.is_null());
+ unsafe { &*self.inner().mContents.mRawPtr }
+ }
+}
+
+/// The container for data that a Servo-backed Gecko document needs to style
+/// itself.
+pub struct PerDocumentStyleDataImpl {
+ /// Rule processor.
+ pub stylist: Stylist,
+
+ /// A cache from element to resolved style.
+ pub undisplayed_style_cache: crate::traversal::UndisplayedStyleCache,
+
+ /// The generation for which our cache is valid.
+ pub undisplayed_style_cache_generation: u64,
+}
+
+/// The data itself is an `AtomicRefCell`, which guarantees the proper semantics
+/// and unexpected races while trying to mutate it.
+pub struct PerDocumentStyleData(AtomicRefCell<PerDocumentStyleDataImpl>);
+
+impl PerDocumentStyleData {
+ /// Create a `PerDocumentStyleData`.
+ pub fn new(document: *const structs::Document) -> Self {
+ let device = Device::new(document);
+ let quirks_mode = device.document().mCompatMode;
+
+ PerDocumentStyleData(AtomicRefCell::new(PerDocumentStyleDataImpl {
+ stylist: Stylist::new(device, quirks_mode.into()),
+ undisplayed_style_cache: Default::default(),
+ undisplayed_style_cache_generation: 0,
+ }))
+ }
+
+ /// Get an immutable reference to this style data.
+ pub fn borrow(&self) -> AtomicRef<PerDocumentStyleDataImpl> {
+ self.0.borrow()
+ }
+
+ /// Get an mutable reference to this style data.
+ pub fn borrow_mut(&self) -> AtomicRefMut<PerDocumentStyleDataImpl> {
+ self.0.borrow_mut()
+ }
+}
+
+impl PerDocumentStyleDataImpl {
+ /// Recreate the style data if the stylesheets have changed.
+ pub fn flush_stylesheets<E>(
+ &mut self,
+ guard: &SharedRwLockReadGuard,
+ document_element: Option<E>,
+ snapshots: Option<&SnapshotMap>,
+ ) -> bool
+ where
+ E: TElement,
+ {
+ self.stylist
+ .flush(&StylesheetGuards::same(guard), document_element, snapshots)
+ }
+
+ /// Get the default computed values for this document.
+ pub fn default_computed_values(&self) -> &Arc<ComputedValues> {
+ self.stylist.device().default_computed_values_arc()
+ }
+
+ /// Measure heap usage.
+ pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) {
+ self.stylist.add_size_of(ops, sizes);
+ }
+}
+
+/// The gecko-specific AuthorStyles instantiation.
+pub type AuthorStyles = crate::author_styles::AuthorStyles<GeckoStyleSheet>;
diff --git a/servo/components/style/gecko/media_features.rs b/servo/components/style/gecko/media_features.rs
new file mode 100644
index 0000000000..c9ad30b28b
--- /dev/null
+++ b/servo/components/style/gecko/media_features.rs
@@ -0,0 +1,1003 @@
+/* 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/. */
+
+//! Gecko's media feature list and evaluator.
+
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs;
+use crate::gecko_bindings::structs::ScreenColorGamut;
+use crate::media_queries::{Device, MediaType};
+use crate::queries::condition::KleeneValue;
+use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription};
+use crate::queries::values::Orientation;
+use crate::values::computed::{CSSPixelLength, Context, Ratio, Resolution};
+use crate::values::AtomString;
+use app_units::Au;
+use euclid::default::Size2D;
+
+fn device_size(device: &Device) -> Size2D<Au> {
+ let mut width = 0;
+ let mut height = 0;
+ unsafe {
+ bindings::Gecko_MediaFeatures_GetDeviceSize(device.document(), &mut width, &mut height);
+ }
+ Size2D::new(Au(width), Au(height))
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#width
+fn eval_width(context: &Context) -> CSSPixelLength {
+ CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px())
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#device-width
+fn eval_device_width(context: &Context) -> CSSPixelLength {
+ CSSPixelLength::new(device_size(context.device()).width.to_f32_px())
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#height
+fn eval_height(context: &Context) -> CSSPixelLength {
+ CSSPixelLength::new(context.device().au_viewport_size().height.to_f32_px())
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#device-height
+fn eval_device_height(context: &Context) -> CSSPixelLength {
+ CSSPixelLength::new(device_size(context.device()).height.to_f32_px())
+}
+
+fn eval_aspect_ratio_for<F>(context: &Context, get_size: F) -> Ratio
+where
+ F: FnOnce(&Device) -> Size2D<Au>,
+{
+ let size = get_size(context.device());
+ Ratio::new(size.width.0 as f32, size.height.0 as f32)
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#aspect-ratio
+fn eval_aspect_ratio(context: &Context) -> Ratio {
+ eval_aspect_ratio_for(context, Device::au_viewport_size)
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#device-aspect-ratio
+fn eval_device_aspect_ratio(context: &Context) -> Ratio {
+ eval_aspect_ratio_for(context, device_size)
+}
+
+/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio
+fn eval_device_pixel_ratio(context: &Context) -> f32 {
+ eval_resolution(context).dppx()
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#orientation
+fn eval_orientation(context: &Context, value: Option<Orientation>) -> bool {
+ Orientation::eval(context.device().au_viewport_size(), value)
+}
+
+/// FIXME: There's no spec for `-moz-device-orientation`.
+fn eval_device_orientation(context: &Context, value: Option<Orientation>) -> bool {
+ Orientation::eval(device_size(context.device()), value)
+}
+
+/// Values for the display-mode media feature.
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum DisplayMode {
+ Browser = 0,
+ MinimalUi,
+ Standalone,
+ Fullscreen,
+}
+
+/// https://w3c.github.io/manifest/#the-display-mode-media-feature
+fn eval_display_mode(context: &Context, query_value: Option<DisplayMode>) -> bool {
+ match query_value {
+ Some(v) => {
+ v == unsafe {
+ bindings::Gecko_MediaFeatures_GetDisplayMode(context.device().document())
+ }
+ },
+ None => true,
+ }
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#grid
+fn eval_grid(_: &Context) -> bool {
+ // Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature
+ // is always 0.
+ false
+}
+
+/// https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d
+fn eval_transform_3d(_: &Context) -> bool {
+ true
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum Scan {
+ Progressive,
+ Interlace,
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#scan
+fn eval_scan(_: &Context, _: Option<Scan>) -> bool {
+ // Since Gecko doesn't support the 'tv' media type, the 'scan' feature never
+ // matches.
+ false
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#color
+fn eval_color(context: &Context) -> i32 {
+ unsafe { bindings::Gecko_MediaFeatures_GetColorDepth(context.device().document()) }
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#color-index
+fn eval_color_index(_: &Context) -> i32 {
+ // We should return zero if the device does not use a color lookup table.
+ 0
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#monochrome
+fn eval_monochrome(context: &Context) -> i32 {
+ // For color devices we should return 0.
+ unsafe { bindings::Gecko_MediaFeatures_GetMonochromeBitsPerPixel(context.device().document()) }
+}
+
+/// Values for the color-gamut media feature.
+/// This implements PartialOrd so that lower values will correctly match
+/// higher capabilities.
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)]
+#[repr(u8)]
+enum ColorGamut {
+ /// The sRGB gamut.
+ Srgb,
+ /// The gamut specified by the Display P3 Color Space.
+ P3,
+ /// The gamut specified by the ITU-R Recommendation BT.2020 Color Space.
+ Rec2020,
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#color-gamut
+fn eval_color_gamut(context: &Context, query_value: Option<ColorGamut>) -> bool {
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return false,
+ };
+ let color_gamut =
+ unsafe { bindings::Gecko_MediaFeatures_ColorGamut(context.device().document()) };
+ // Match if our color gamut is at least as wide as the query value
+ query_value <=
+ match color_gamut {
+ // EndGuard_ is not a valid color gamut, so the default color-gamut is used.
+ ScreenColorGamut::Srgb | ScreenColorGamut::EndGuard_ => ColorGamut::Srgb,
+ ScreenColorGamut::P3 => ColorGamut::P3,
+ ScreenColorGamut::Rec2020 => ColorGamut::Rec2020,
+ }
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#resolution
+fn eval_resolution(context: &Context) -> Resolution {
+ let resolution_dppx =
+ unsafe { bindings::Gecko_MediaFeatures_GetResolution(context.device().document()) };
+ Resolution::from_dppx(resolution_dppx)
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum PrefersReducedMotion {
+ NoPreference,
+ Reduce,
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum PrefersReducedTransparency {
+ NoPreference,
+ Reduce,
+}
+
+/// Values for the prefers-color-scheme media feature.
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum PrefersColorScheme {
+ Light,
+ Dark,
+}
+
+/// Values for the dynamic-range and video-dynamic-range media features.
+/// https://drafts.csswg.org/mediaqueries-5/#dynamic-range
+/// This implements PartialOrd so that lower values will correctly match
+/// higher capabilities.
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum DynamicRange {
+ Standard,
+ High,
+}
+
+/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion
+fn eval_prefers_reduced_motion(
+ context: &Context,
+ query_value: Option<PrefersReducedMotion>,
+) -> bool {
+ let prefers_reduced =
+ unsafe { bindings::Gecko_MediaFeatures_PrefersReducedMotion(context.device().document()) };
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return prefers_reduced,
+ };
+
+ match query_value {
+ PrefersReducedMotion::NoPreference => !prefers_reduced,
+ PrefersReducedMotion::Reduce => prefers_reduced,
+ }
+}
+
+/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-transparency
+fn eval_prefers_reduced_transparency(
+ context: &Context,
+ query_value: Option<PrefersReducedTransparency>,
+) -> bool {
+ let prefers_reduced = unsafe {
+ bindings::Gecko_MediaFeatures_PrefersReducedTransparency(context.device().document())
+ };
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return prefers_reduced,
+ };
+
+ match query_value {
+ PrefersReducedTransparency::NoPreference => !prefers_reduced,
+ PrefersReducedTransparency::Reduce => prefers_reduced,
+ }
+}
+
+/// Possible values for prefers-contrast media query.
+/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)]
+#[repr(u8)]
+pub enum PrefersContrast {
+ /// More contrast is preferred.
+ More,
+ /// Low contrast is preferred.
+ Less,
+ /// Custom (not more, not less).
+ Custom,
+ /// The default value if neither high or low contrast is enabled.
+ NoPreference,
+}
+
+/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast
+fn eval_prefers_contrast(context: &Context, query_value: Option<PrefersContrast>) -> bool {
+ let prefers_contrast =
+ unsafe { bindings::Gecko_MediaFeatures_PrefersContrast(context.device().document()) };
+ match query_value {
+ Some(v) => v == prefers_contrast,
+ None => prefers_contrast != PrefersContrast::NoPreference,
+ }
+}
+
+/// Possible values for the forced-colors media query.
+/// https://drafts.csswg.org/mediaqueries-5/#forced-colors
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)]
+#[repr(u8)]
+pub enum ForcedColors {
+ /// Page colors are not being forced.
+ None,
+ /// Page colors are being forced.
+ Active,
+}
+
+/// https://drafts.csswg.org/mediaqueries-5/#forced-colors
+fn eval_forced_colors(context: &Context, query_value: Option<ForcedColors>) -> bool {
+ let forced = !context.device().use_document_colors();
+ match query_value {
+ Some(query_value) => forced == (query_value == ForcedColors::Active),
+ None => forced,
+ }
+}
+
+/// Possible values for the inverted-colors media query.
+/// https://drafts.csswg.org/mediaqueries-5/#inverted
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum InvertedColors {
+ /// Colors are displayed normally.
+ None,
+ /// All pixels within the displayed area have been inverted.
+ Inverted,
+}
+
+/// https://drafts.csswg.org/mediaqueries-5/#inverted
+fn eval_inverted_colors(context: &Context, query_value: Option<InvertedColors>) -> bool {
+ let inverted_colors =
+ unsafe { bindings::Gecko_MediaFeatures_InvertedColors(context.device().document()) };
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return inverted_colors,
+ };
+
+ match query_value {
+ InvertedColors::None => !inverted_colors,
+ InvertedColors::Inverted => inverted_colors,
+ }
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum OverflowBlock {
+ None,
+ Scroll,
+ Paged,
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-block
+fn eval_overflow_block(context: &Context, query_value: Option<OverflowBlock>) -> bool {
+ // For the time being, assume that printing (including previews)
+ // is the only time when we paginate, and we are otherwise always
+ // scrolling. This is true at the moment in Firefox, but may need
+ // updating in the future (e.g., ebook readers built with Stylo, a
+ // billboard mode that doesn't support overflow at all).
+ //
+ // If this ever changes, don't forget to change eval_overflow_inline too.
+ let scrolling = context.device().media_type() != MediaType::print();
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return true,
+ };
+
+ match query_value {
+ OverflowBlock::None => false,
+ OverflowBlock::Scroll => scrolling,
+ OverflowBlock::Paged => !scrolling,
+ }
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum OverflowInline {
+ None,
+ Scroll,
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-inline
+fn eval_overflow_inline(context: &Context, query_value: Option<OverflowInline>) -> bool {
+ // See the note in eval_overflow_block.
+ let scrolling = context.device().media_type() != MediaType::print();
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return scrolling,
+ };
+
+ match query_value {
+ OverflowInline::None => !scrolling,
+ OverflowInline::Scroll => scrolling,
+ }
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum Update {
+ None,
+ Slow,
+ Fast,
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#update
+fn eval_update(context: &Context, query_value: Option<Update>) -> bool {
+ // This has similar caveats to those described in eval_overflow_block.
+ // For now, we report that print (incl. print media simulation,
+ // which can in fact update but is limited to the developer tools)
+ // is `update: none` and that all other contexts are `update: fast`,
+ // which may not be true for future platforms, like e-ink devices.
+ let can_update = context.device().media_type() != MediaType::print();
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return can_update,
+ };
+
+ match query_value {
+ Update::None => !can_update,
+ Update::Slow => false,
+ Update::Fast => can_update,
+ }
+}
+
+fn do_eval_prefers_color_scheme(
+ context: &Context,
+ use_content: bool,
+ query_value: Option<PrefersColorScheme>,
+) -> bool {
+ let prefers_color_scheme = unsafe {
+ bindings::Gecko_MediaFeatures_PrefersColorScheme(context.device().document(), use_content)
+ };
+ match query_value {
+ Some(v) => prefers_color_scheme == v,
+ None => true,
+ }
+}
+
+/// https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme
+fn eval_prefers_color_scheme(context: &Context, query_value: Option<PrefersColorScheme>) -> bool {
+ do_eval_prefers_color_scheme(context, /* use_content = */ false, query_value)
+}
+
+fn eval_content_prefers_color_scheme(
+ context: &Context,
+ query_value: Option<PrefersColorScheme>,
+) -> bool {
+ do_eval_prefers_color_scheme(context, /* use_content = */ true, query_value)
+}
+
+/// https://drafts.csswg.org/mediaqueries-5/#dynamic-range
+fn eval_dynamic_range(context: &Context, query_value: Option<DynamicRange>) -> bool {
+ let dynamic_range =
+ unsafe { bindings::Gecko_MediaFeatures_DynamicRange(context.device().document()) };
+ match query_value {
+ Some(v) => dynamic_range >= v,
+ None => false,
+ }
+}
+/// https://drafts.csswg.org/mediaqueries-5/#video-dynamic-range
+fn eval_video_dynamic_range(context: &Context, query_value: Option<DynamicRange>) -> bool {
+ let dynamic_range =
+ unsafe { bindings::Gecko_MediaFeatures_VideoDynamicRange(context.device().document()) };
+ match query_value {
+ Some(v) => dynamic_range >= v,
+ None => false,
+ }
+}
+
+bitflags! {
+ /// https://drafts.csswg.org/mediaqueries-4/#mf-interaction
+ struct PointerCapabilities: u8 {
+ const COARSE = structs::PointerCapabilities_Coarse;
+ const FINE = structs::PointerCapabilities_Fine;
+ const HOVER = structs::PointerCapabilities_Hover;
+ }
+}
+
+fn primary_pointer_capabilities(context: &Context) -> PointerCapabilities {
+ PointerCapabilities::from_bits_truncate(unsafe {
+ bindings::Gecko_MediaFeatures_PrimaryPointerCapabilities(context.device().document())
+ })
+}
+
+fn all_pointer_capabilities(context: &Context) -> PointerCapabilities {
+ PointerCapabilities::from_bits_truncate(unsafe {
+ bindings::Gecko_MediaFeatures_AllPointerCapabilities(context.device().document())
+ })
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum Pointer {
+ None,
+ Coarse,
+ Fine,
+}
+
+fn eval_pointer_capabilities(
+ query_value: Option<Pointer>,
+ pointer_capabilities: PointerCapabilities,
+) -> bool {
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return !pointer_capabilities.is_empty(),
+ };
+
+ match query_value {
+ Pointer::None => pointer_capabilities.is_empty(),
+ Pointer::Coarse => pointer_capabilities.intersects(PointerCapabilities::COARSE),
+ Pointer::Fine => pointer_capabilities.intersects(PointerCapabilities::FINE),
+ }
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#pointer
+fn eval_pointer(context: &Context, query_value: Option<Pointer>) -> bool {
+ eval_pointer_capabilities(query_value, primary_pointer_capabilities(context))
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-pointer
+fn eval_any_pointer(context: &Context, query_value: Option<Pointer>) -> bool {
+ eval_pointer_capabilities(query_value, all_pointer_capabilities(context))
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum Hover {
+ None,
+ Hover,
+}
+
+fn eval_hover_capabilities(
+ query_value: Option<Hover>,
+ pointer_capabilities: PointerCapabilities,
+) -> bool {
+ let can_hover = pointer_capabilities.intersects(PointerCapabilities::HOVER);
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return can_hover,
+ };
+
+ match query_value {
+ Hover::None => !can_hover,
+ Hover::Hover => can_hover,
+ }
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#hover
+fn eval_hover(context: &Context, query_value: Option<Hover>) -> bool {
+ eval_hover_capabilities(query_value, primary_pointer_capabilities(context))
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-hover
+fn eval_any_hover(context: &Context, query_value: Option<Hover>) -> bool {
+ eval_hover_capabilities(query_value, all_pointer_capabilities(context))
+}
+
+fn eval_moz_is_glyph(context: &Context) -> bool {
+ context.device().document().mIsSVGGlyphsDocument()
+}
+
+fn eval_moz_print_preview(context: &Context) -> bool {
+ let is_print_preview = context.device().is_print_preview();
+ if is_print_preview {
+ debug_assert_eq!(context.device().media_type(), MediaType::print());
+ }
+ is_print_preview
+}
+
+fn eval_moz_is_resource_document(context: &Context) -> bool {
+ unsafe { bindings::Gecko_MediaFeatures_IsResourceDocument(context.device().document()) }
+}
+
+/// Allows front-end CSS to discern platform via media queries.
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+pub enum Platform {
+ /// Matches any Android version.
+ Android,
+ /// For our purposes here, "linux" is just "gtk" (so unix-but-not-mac).
+ /// There's no need for our front-end code to differentiate between those
+ /// platforms and they already use the "linux" string elsewhere (e.g.,
+ /// toolkit/themes/linux).
+ Linux,
+ /// Matches any macOS version.
+ Macos,
+ /// Matches any Windows version.
+ Windows,
+}
+
+fn eval_moz_platform(_: &Context, query_value: Option<Platform>) -> bool {
+ let query_value = match query_value {
+ Some(v) => v,
+ None => return false,
+ };
+
+ unsafe { bindings::Gecko_MediaFeatures_MatchesPlatform(query_value) }
+}
+
+/// Allows front-end CSS to discern gtk theme via media queries.
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)]
+#[repr(u8)]
+pub enum GtkThemeFamily {
+ /// Unknown theme family.
+ Unknown = 0,
+ /// Adwaita, the default GTK theme.
+ Adwaita,
+ /// Breeze, the default KDE theme.
+ Breeze,
+ /// Yaru, the default Ubuntu theme.
+ Yaru,
+}
+
+fn eval_gtk_theme_family(_: &Context, query_value: Option<GtkThemeFamily>) -> bool {
+ let family = unsafe { bindings::Gecko_MediaFeatures_GtkThemeFamily() };
+ match query_value {
+ Some(v) => v == family,
+ None => return family != GtkThemeFamily::Unknown,
+ }
+}
+
+/// Values for the scripting media feature.
+/// https://drafts.csswg.org/mediaqueries-5/#scripting
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)]
+#[repr(u8)]
+pub enum Scripting {
+ /// Scripting is not supported or not enabled
+ None,
+ /// Scripting is supported and enabled, but only for initial page load
+ /// We will never match this value as it is intended for non-browser user agents,
+ /// but it is part of the spec so we should still parse it.
+ /// See: https://github.com/w3c/csswg-drafts/issues/8621
+ InitialOnly,
+ /// Scripting is supported and enabled
+ Enabled,
+}
+
+/// https://drafts.csswg.org/mediaqueries-5/#scripting
+fn eval_scripting(context: &Context, query_value: Option<Scripting>) -> bool {
+ let scripting = unsafe { bindings::Gecko_MediaFeatures_Scripting(context.device().document()) };
+ match query_value {
+ Some(v) => v == scripting,
+ None => scripting != Scripting::None,
+ }
+}
+
+fn eval_moz_overlay_scrollbars(context: &Context) -> bool {
+ unsafe { bindings::Gecko_MediaFeatures_UseOverlayScrollbars(context.device().document()) }
+}
+
+fn eval_moz_bool_pref(_: &Context, pref: Option<&AtomString>) -> KleeneValue {
+ let Some(pref) = pref else {
+ return KleeneValue::False;
+ };
+ KleeneValue::from(unsafe { bindings::Gecko_ComputeBoolPrefMediaQuery(pref.as_ptr()) })
+}
+
+fn get_lnf_int(int_id: i32) -> i32 {
+ unsafe { bindings::Gecko_GetLookAndFeelInt(int_id) }
+}
+
+fn get_lnf_int_as_bool(int_id: i32) -> bool {
+ get_lnf_int(int_id) != 0
+}
+
+fn get_scrollbar_start_backward(int_id: i32) -> bool {
+ (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_StartBackward as i32) != 0
+}
+
+fn get_scrollbar_start_forward(int_id: i32) -> bool {
+ (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_StartForward as i32) != 0
+}
+
+fn get_scrollbar_end_backward(int_id: i32) -> bool {
+ (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_EndBackward as i32) != 0
+}
+
+fn get_scrollbar_end_forward(int_id: i32) -> bool {
+ (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_EndForward as i32) != 0
+}
+
+macro_rules! lnf_int_feature {
+ ($feature_name:expr, $int_id:ident, $get_value:ident) => {{
+ fn __eval(_: &Context) -> bool {
+ $get_value(bindings::LookAndFeel_IntID::$int_id as i32)
+ }
+
+ feature!(
+ $feature_name,
+ AllowsRanges::No,
+ Evaluator::BoolInteger(__eval),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ )
+ }};
+ ($feature_name:expr, $int_id:ident) => {{
+ lnf_int_feature!($feature_name, $int_id, get_lnf_int_as_bool)
+ }};
+}
+
+/// Adding new media features requires (1) adding the new feature to this
+/// array, with appropriate entries (and potentially any new code needed
+/// to support new types in these entries and (2) ensuring that either
+/// nsPresContext::MediaFeatureValuesChanged is called when the value that
+/// would be returned by the evaluator function could change.
+pub static MEDIA_FEATURES: [QueryFeatureDescription; 59] = [
+ feature!(
+ atom!("width"),
+ AllowsRanges::Yes,
+ Evaluator::Length(eval_width),
+ FeatureFlags::VIEWPORT_DEPENDENT,
+ ),
+ feature!(
+ atom!("height"),
+ AllowsRanges::Yes,
+ Evaluator::Length(eval_height),
+ FeatureFlags::VIEWPORT_DEPENDENT,
+ ),
+ feature!(
+ atom!("aspect-ratio"),
+ AllowsRanges::Yes,
+ Evaluator::NumberRatio(eval_aspect_ratio),
+ FeatureFlags::VIEWPORT_DEPENDENT,
+ ),
+ feature!(
+ atom!("orientation"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_orientation, Orientation),
+ FeatureFlags::VIEWPORT_DEPENDENT,
+ ),
+ feature!(
+ atom!("device-width"),
+ AllowsRanges::Yes,
+ Evaluator::Length(eval_device_width),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("device-height"),
+ AllowsRanges::Yes,
+ Evaluator::Length(eval_device_height),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("device-aspect-ratio"),
+ AllowsRanges::Yes,
+ Evaluator::NumberRatio(eval_device_aspect_ratio),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("-moz-device-orientation"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_device_orientation, Orientation),
+ FeatureFlags::empty(),
+ ),
+ // Webkit extensions that we support for de-facto web compatibility.
+ // -webkit-{min|max}-device-pixel-ratio (controlled with its own pref):
+ feature!(
+ atom!("device-pixel-ratio"),
+ AllowsRanges::Yes,
+ Evaluator::Float(eval_device_pixel_ratio),
+ FeatureFlags::WEBKIT_PREFIX,
+ ),
+ // -webkit-transform-3d.
+ feature!(
+ atom!("transform-3d"),
+ AllowsRanges::No,
+ Evaluator::BoolInteger(eval_transform_3d),
+ FeatureFlags::WEBKIT_PREFIX,
+ ),
+ feature!(
+ atom!("-moz-device-pixel-ratio"),
+ AllowsRanges::Yes,
+ Evaluator::Float(eval_device_pixel_ratio),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("resolution"),
+ AllowsRanges::Yes,
+ Evaluator::Resolution(eval_resolution),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("display-mode"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_display_mode, DisplayMode),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("grid"),
+ AllowsRanges::No,
+ Evaluator::BoolInteger(eval_grid),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("scan"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_scan, Scan),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("color"),
+ AllowsRanges::Yes,
+ Evaluator::Integer(eval_color),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("color-index"),
+ AllowsRanges::Yes,
+ Evaluator::Integer(eval_color_index),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("monochrome"),
+ AllowsRanges::Yes,
+ Evaluator::Integer(eval_monochrome),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("color-gamut"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_color_gamut, ColorGamut),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("prefers-reduced-motion"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_prefers_reduced_motion, PrefersReducedMotion),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("prefers-reduced-transparency"),
+ AllowsRanges::No,
+ keyword_evaluator!(
+ eval_prefers_reduced_transparency,
+ PrefersReducedTransparency
+ ),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("prefers-contrast"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_prefers_contrast, PrefersContrast),
+ // Note: by default this is only enabled in browser chrome and
+ // ua. It can be enabled on the web via the
+ // layout.css.prefers-contrast.enabled preference. See
+ // disabed_by_pref in media_feature_expression.rs for how that
+ // is done.
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("forced-colors"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_forced_colors, ForcedColors),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("inverted-colors"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_inverted_colors, InvertedColors),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("overflow-block"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_overflow_block, OverflowBlock),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("overflow-inline"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_overflow_inline, OverflowInline),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("update"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_update, Update),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("prefers-color-scheme"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_prefers_color_scheme, PrefersColorScheme),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("dynamic-range"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_dynamic_range, DynamicRange),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("video-dynamic-range"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_video_dynamic_range, DynamicRange),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("scripting"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_scripting, Scripting),
+ FeatureFlags::empty(),
+ ),
+ // Evaluates to the preferred color scheme for content. Only useful in
+ // chrome context, where the chrome color-scheme and the content
+ // color-scheme might differ.
+ feature!(
+ atom!("-moz-content-prefers-color-scheme"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_content_prefers_color_scheme, PrefersColorScheme),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ ),
+ feature!(
+ atom!("pointer"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_pointer, Pointer),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("any-pointer"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_any_pointer, Pointer),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("hover"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_hover, Hover),
+ FeatureFlags::empty(),
+ ),
+ feature!(
+ atom!("any-hover"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_any_hover, Hover),
+ FeatureFlags::empty(),
+ ),
+ // Internal -moz-is-glyph media feature: applies only inside SVG glyphs.
+ // Internal because it is really only useful in the user agent anyway
+ // and therefore not worth standardizing.
+ feature!(
+ atom!("-moz-is-glyph"),
+ AllowsRanges::No,
+ Evaluator::BoolInteger(eval_moz_is_glyph),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ ),
+ feature!(
+ atom!("-moz-is-resource-document"),
+ AllowsRanges::No,
+ Evaluator::BoolInteger(eval_moz_is_resource_document),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ ),
+ feature!(
+ atom!("-moz-platform"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_moz_platform, Platform),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ ),
+ feature!(
+ atom!("-moz-gtk-theme-family"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_gtk_theme_family, GtkThemeFamily),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ ),
+ feature!(
+ atom!("-moz-print-preview"),
+ AllowsRanges::No,
+ Evaluator::BoolInteger(eval_moz_print_preview),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ ),
+ feature!(
+ atom!("-moz-overlay-scrollbars"),
+ AllowsRanges::No,
+ Evaluator::BoolInteger(eval_moz_overlay_scrollbars),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ ),
+ feature!(
+ atom!("-moz-bool-pref"),
+ AllowsRanges::No,
+ Evaluator::String(eval_moz_bool_pref),
+ FeatureFlags::CHROME_AND_UA_ONLY,
+ ),
+ lnf_int_feature!(
+ atom!("-moz-scrollbar-start-backward"),
+ ScrollArrowStyle,
+ get_scrollbar_start_backward
+ ),
+ lnf_int_feature!(
+ atom!("-moz-scrollbar-start-forward"),
+ ScrollArrowStyle,
+ get_scrollbar_start_forward
+ ),
+ lnf_int_feature!(
+ atom!("-moz-scrollbar-end-backward"),
+ ScrollArrowStyle,
+ get_scrollbar_end_backward
+ ),
+ lnf_int_feature!(
+ atom!("-moz-scrollbar-end-forward"),
+ ScrollArrowStyle,
+ get_scrollbar_end_forward
+ ),
+ lnf_int_feature!(atom!("-moz-menubar-drag"), MenuBarDrag),
+ lnf_int_feature!(atom!("-moz-mac-big-sur-theme"), MacBigSurTheme),
+ lnf_int_feature!(atom!("-moz-mac-rtl"), MacRTL),
+ lnf_int_feature!(
+ atom!("-moz-windows-accent-color-in-titlebar"),
+ WindowsAccentColorInTitlebar
+ ),
+ lnf_int_feature!(atom!("-moz-swipe-animation-enabled"), SwipeAnimationEnabled),
+ lnf_int_feature!(atom!("-moz-gtk-csd-available"), GTKCSDAvailable),
+ lnf_int_feature!(atom!("-moz-gtk-csd-minimize-button"), GTKCSDMinimizeButton),
+ lnf_int_feature!(atom!("-moz-gtk-csd-maximize-button"), GTKCSDMaximizeButton),
+ lnf_int_feature!(atom!("-moz-gtk-csd-close-button"), GTKCSDCloseButton),
+ lnf_int_feature!(
+ atom!("-moz-gtk-csd-reversed-placement"),
+ GTKCSDReversedPlacement
+ ),
+ lnf_int_feature!(atom!("-moz-system-dark-theme"), SystemUsesDarkTheme),
+ lnf_int_feature!(atom!("-moz-panel-animations"), PanelAnimations),
+];
diff --git a/servo/components/style/gecko/media_queries.rs b/servo/components/style/gecko/media_queries.rs
new file mode 100644
index 0000000000..ef156ab380
--- /dev/null
+++ b/servo/components/style/gecko/media_queries.rs
@@ -0,0 +1,593 @@
+/* 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/. */
+
+//! Gecko's media-query device and expression representation.
+
+use crate::color::AbsoluteColor;
+use crate::context::QuirksMode;
+use crate::custom_properties::CssEnvironment;
+use crate::font_metrics::FontMetrics;
+use crate::gecko::values::{convert_absolute_color_to_nscolor, convert_nscolor_to_absolute_color};
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs;
+use crate::logical_geometry::WritingMode;
+use crate::media_queries::MediaType;
+use crate::properties::ComputedValues;
+use crate::string_cache::Atom;
+use crate::values::computed::font::GenericFontFamily;
+use crate::values::computed::{ColorScheme, Length, NonNegativeLength};
+use crate::values::specified::color::SystemColor;
+use crate::values::specified::font::{FONT_MEDIUM_LINE_HEIGHT_PX, FONT_MEDIUM_PX};
+use crate::values::specified::ViewportVariant;
+use crate::values::{CustomIdent, KeyframesName};
+use app_units::{Au, AU_PER_PX};
+use euclid::default::Size2D;
+use euclid::{Scale, SideOffsets2D};
+use servo_arc::Arc;
+use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
+use std::{cmp, fmt};
+use style_traits::{CSSPixel, DevicePixel};
+
+/// The `Device` in Gecko wraps a pres context, has a default values computed,
+/// and contains all the viewport rule state.
+pub struct Device {
+ /// NB: The document owns the styleset, who owns the stylist, and thus the
+ /// `Device`, so having a raw document pointer here is fine.
+ document: *const structs::Document,
+ default_values: Arc<ComputedValues>,
+ /// The font size of the root element.
+ ///
+ /// This is set when computing the style of the root element, and used for
+ /// rem units in other elements.
+ ///
+ /// When computing the style of the root element, there can't be any other
+ /// style being computed at the same time, given we need the style of the
+ /// parent to compute everything else. So it is correct to just use a
+ /// relaxed atomic here.
+ root_font_size: AtomicU32,
+ /// Line height of the root element, used for rlh units in other elements.
+ root_line_height: AtomicU32,
+ /// The body text color, stored as an `nscolor`, used for the "tables
+ /// inherit from body" quirk.
+ ///
+ /// <https://quirks.spec.whatwg.org/#the-tables-inherit-color-from-body-quirk>
+ body_text_color: AtomicUsize,
+ /// Whether any styles computed in the document relied on the root font-size
+ /// by using rem units.
+ used_root_font_size: AtomicBool,
+ /// Whether any styles computed in the document relied on the root line-height
+ /// by using rlh units.
+ used_root_line_height: AtomicBool,
+ /// Whether any styles computed in the document relied on font metrics.
+ used_font_metrics: AtomicBool,
+ /// Whether any styles computed in the document relied on the viewport size
+ /// by using vw/vh/vmin/vmax units.
+ used_viewport_size: AtomicBool,
+ /// Whether any styles computed in the document relied on the viewport size
+ /// by using dvw/dvh/dvmin/dvmax units.
+ used_dynamic_viewport_size: AtomicBool,
+ /// The CssEnvironment object responsible of getting CSS environment
+ /// variables.
+ environment: CssEnvironment,
+}
+
+impl fmt::Debug for Device {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use nsstring::nsCString;
+
+ let mut doc_uri = nsCString::new();
+ unsafe {
+ bindings::Gecko_nsIURI_Debug((*self.document()).mDocumentURI.raw(), &mut doc_uri)
+ };
+
+ f.debug_struct("Device")
+ .field("document_url", &doc_uri)
+ .finish()
+ }
+}
+
+unsafe impl Sync for Device {}
+unsafe impl Send for Device {}
+
+impl Device {
+ /// Trivially constructs a new `Device`.
+ pub fn new(document: *const structs::Document) -> Self {
+ assert!(!document.is_null());
+ let doc = unsafe { &*document };
+ let prefs = unsafe { &*bindings::Gecko_GetPrefSheetPrefs(doc) };
+ Device {
+ document,
+ default_values: ComputedValues::default_values(doc),
+ root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()),
+ root_line_height: AtomicU32::new(FONT_MEDIUM_LINE_HEIGHT_PX.to_bits()),
+ // This gets updated when we see the <body>, so it doesn't really
+ // matter which color-scheme we look at here.
+ body_text_color: AtomicUsize::new(prefs.mLightColors.mDefault as usize),
+ used_root_font_size: AtomicBool::new(false),
+ used_root_line_height: AtomicBool::new(false),
+ used_font_metrics: AtomicBool::new(false),
+ used_viewport_size: AtomicBool::new(false),
+ used_dynamic_viewport_size: AtomicBool::new(false),
+ environment: CssEnvironment,
+ }
+ }
+
+ /// Get the relevant environment to resolve `env()` functions.
+ #[inline]
+ pub fn environment(&self) -> &CssEnvironment {
+ &self.environment
+ }
+
+ /// Returns the computed line-height for the font in a given computed values instance.
+ ///
+ /// If you pass down an element, then the used line-height is returned.
+ pub fn calc_line_height(
+ &self,
+ font: &crate::properties::style_structs::Font,
+ writing_mode: WritingMode,
+ element: Option<super::wrapper::GeckoElement>,
+ ) -> NonNegativeLength {
+ let pres_context = self.pres_context();
+ let line_height = font.clone_line_height();
+ let au = Au(unsafe {
+ bindings::Gecko_CalcLineHeight(
+ &line_height,
+ pres_context.map_or(std::ptr::null(), |pc| pc),
+ writing_mode.is_text_vertical(),
+ &**font,
+ element.map_or(std::ptr::null(), |e| e.0),
+ )
+ });
+ NonNegativeLength::new(au.to_f32_px())
+ }
+
+ /// Whether any animation name may be referenced from the style of any
+ /// element.
+ pub fn animation_name_may_be_referenced(&self, name: &KeyframesName) -> bool {
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return false,
+ };
+
+ unsafe {
+ bindings::Gecko_AnimationNameMayBeReferencedFromStyle(pc, name.as_atom().as_ptr())
+ }
+ }
+
+ /// Returns the default computed values as a reference, in order to match
+ /// Servo.
+ pub fn default_computed_values(&self) -> &ComputedValues {
+ &self.default_values
+ }
+
+ /// Returns the default computed values as an `Arc`.
+ pub fn default_computed_values_arc(&self) -> &Arc<ComputedValues> {
+ &self.default_values
+ }
+
+ /// Get the font size of the root element (for rem)
+ pub fn root_font_size(&self) -> Length {
+ self.used_root_font_size.store(true, Ordering::Relaxed);
+ Length::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed)))
+ }
+
+ /// Set the font size of the root element (for rem)
+ pub fn set_root_font_size(&self, size: Length) {
+ self.root_font_size
+ .store(size.px().to_bits(), Ordering::Relaxed)
+ }
+
+ /// Get the line height of the root element (for rlh)
+ pub fn root_line_height(&self) -> Length {
+ self.used_root_line_height.store(true, Ordering::Relaxed);
+ Length::new(f32::from_bits(
+ self.root_line_height.load(Ordering::Relaxed),
+ ))
+ }
+
+ /// Set the line height of the root element (for rlh)
+ pub fn set_root_line_height(&self, size: Length) {
+ self.root_line_height
+ .store(size.px().to_bits(), Ordering::Relaxed);
+ }
+
+ /// The quirks mode of the document.
+ pub fn quirks_mode(&self) -> QuirksMode {
+ self.document().mCompatMode.into()
+ }
+
+ /// Sets the body text color for the "inherit color from body" quirk.
+ ///
+ /// <https://quirks.spec.whatwg.org/#the-tables-inherit-color-from-body-quirk>
+ pub fn set_body_text_color(&self, color: AbsoluteColor) {
+ self.body_text_color.store(
+ convert_absolute_color_to_nscolor(&color) as usize,
+ Ordering::Relaxed,
+ )
+ }
+
+ /// Gets the base size given a generic font family and a language.
+ pub fn base_size_for_generic(&self, language: &Atom, generic: GenericFontFamily) -> Length {
+ unsafe { bindings::Gecko_GetBaseSize(self.document(), language.as_ptr(), generic) }
+ }
+
+ /// Gets the size of the scrollbar in CSS pixels.
+ pub fn scrollbar_inline_size(&self) -> Length {
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ // XXX: we could have a more reasonable default perhaps.
+ None => return Length::new(0.0),
+ };
+ Length::new(unsafe { bindings::Gecko_GetScrollbarInlineSize(pc) })
+ }
+
+ /// Queries font metrics
+ pub fn query_font_metrics(
+ &self,
+ vertical: bool,
+ font: &crate::properties::style_structs::Font,
+ base_size: Length,
+ in_media_query: bool,
+ retrieve_math_scales: bool,
+ ) -> FontMetrics {
+ self.used_font_metrics.store(true, Ordering::Relaxed);
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return Default::default(),
+ };
+ let gecko_metrics = unsafe {
+ bindings::Gecko_GetFontMetrics(
+ pc,
+ vertical,
+ &**font,
+ base_size,
+ // we don't use the user font set in a media query
+ !in_media_query,
+ retrieve_math_scales,
+ )
+ };
+ FontMetrics {
+ x_height: Some(gecko_metrics.mXSize),
+ zero_advance_measure: if gecko_metrics.mChSize.px() >= 0. {
+ Some(gecko_metrics.mChSize)
+ } else {
+ None
+ },
+ cap_height: if gecko_metrics.mCapHeight.px() >= 0. {
+ Some(gecko_metrics.mCapHeight)
+ } else {
+ None
+ },
+ ic_width: if gecko_metrics.mIcWidth.px() >= 0. {
+ Some(gecko_metrics.mIcWidth)
+ } else {
+ None
+ },
+ ascent: gecko_metrics.mAscent,
+ script_percent_scale_down: if gecko_metrics.mScriptPercentScaleDown > 0. {
+ Some(gecko_metrics.mScriptPercentScaleDown)
+ } else {
+ None
+ },
+ script_script_percent_scale_down: if gecko_metrics.mScriptScriptPercentScaleDown > 0. {
+ Some(gecko_metrics.mScriptScriptPercentScaleDown)
+ } else {
+ None
+ },
+ }
+ }
+
+ /// Returns the body text color.
+ pub fn body_text_color(&self) -> AbsoluteColor {
+ convert_nscolor_to_absolute_color(self.body_text_color.load(Ordering::Relaxed) as u32)
+ }
+
+ /// Gets the document pointer.
+ #[inline]
+ pub fn document(&self) -> &structs::Document {
+ unsafe { &*self.document }
+ }
+
+ /// Gets the pres context associated with this document.
+ #[inline]
+ pub fn pres_context(&self) -> Option<&structs::nsPresContext> {
+ unsafe {
+ self.document()
+ .mPresShell
+ .as_ref()?
+ .mPresContext
+ .mRawPtr
+ .as_ref()
+ }
+ }
+
+ /// Gets the preference stylesheet prefs for our document.
+ #[inline]
+ pub fn pref_sheet_prefs(&self) -> &structs::PreferenceSheet_Prefs {
+ unsafe { &*bindings::Gecko_GetPrefSheetPrefs(self.document()) }
+ }
+
+ /// Recreates the default computed values.
+ pub fn reset_computed_values(&mut self) {
+ self.default_values = ComputedValues::default_values(self.document());
+ }
+
+ /// Rebuild all the cached data.
+ pub fn rebuild_cached_data(&mut self) {
+ self.reset_computed_values();
+ self.used_root_font_size.store(false, Ordering::Relaxed);
+ self.used_root_line_height.store(false, Ordering::Relaxed);
+ self.used_font_metrics.store(false, Ordering::Relaxed);
+ self.used_viewport_size.store(false, Ordering::Relaxed);
+ self.used_dynamic_viewport_size
+ .store(false, Ordering::Relaxed);
+ }
+
+ /// Returns whether we ever looked up the root font size of the device.
+ pub fn used_root_font_size(&self) -> bool {
+ self.used_root_font_size.load(Ordering::Relaxed)
+ }
+
+ /// Returns whether we ever looked up the root line-height of the device.
+ pub fn used_root_line_height(&self) -> bool {
+ self.used_root_line_height.load(Ordering::Relaxed)
+ }
+
+ /// Recreates all the temporary state that the `Device` stores.
+ ///
+ /// This includes the viewport override from `@viewport` rules, and also the
+ /// default computed values.
+ pub fn reset(&mut self) {
+ self.reset_computed_values();
+ }
+
+ /// Returns whether this document is in print preview.
+ pub fn is_print_preview(&self) -> bool {
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return false,
+ };
+ pc.mType == structs::nsPresContext_nsPresContextType_eContext_PrintPreview
+ }
+
+ /// Returns the current media type of the device.
+ pub fn media_type(&self) -> MediaType {
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return MediaType::screen(),
+ };
+
+ // Gecko allows emulating random media with mMediaEmulationData.mMedium.
+ let medium_to_use = if !pc.mMediaEmulationData.mMedium.mRawPtr.is_null() {
+ pc.mMediaEmulationData.mMedium.mRawPtr
+ } else {
+ pc.mMedium as *const structs::nsAtom as *mut _
+ };
+
+ MediaType(CustomIdent(unsafe { Atom::from_raw(medium_to_use) }))
+ }
+
+ // It may make sense to account for @page rule margins here somehow, however
+ // it's not clear how that'd work, see:
+ // https://github.com/w3c/csswg-drafts/issues/5437
+ fn page_size_minus_default_margin(&self, pc: &structs::nsPresContext) -> Size2D<Au> {
+ debug_assert!(pc.mIsRootPaginatedDocument() != 0);
+ let area = &pc.mPageSize;
+ let margin = &pc.mDefaultPageMargin;
+ let width = area.width - margin.left - margin.right;
+ let height = area.height - margin.top - margin.bottom;
+ Size2D::new(Au(cmp::max(width, 0)), Au(cmp::max(height, 0)))
+ }
+
+ /// Returns the current viewport size in app units.
+ pub fn au_viewport_size(&self) -> Size2D<Au> {
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return Size2D::new(Au(0), Au(0)),
+ };
+
+ if pc.mIsRootPaginatedDocument() != 0 {
+ return self.page_size_minus_default_margin(pc);
+ }
+
+ let area = &pc.mVisibleArea;
+ Size2D::new(Au(area.width), Au(area.height))
+ }
+
+ /// Returns the current viewport size in app units, recording that it's been
+ /// used for viewport unit resolution.
+ pub fn au_viewport_size_for_viewport_unit_resolution(
+ &self,
+ variant: ViewportVariant,
+ ) -> Size2D<Au> {
+ self.used_viewport_size.store(true, Ordering::Relaxed);
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return Size2D::new(Au(0), Au(0)),
+ };
+
+ if pc.mIsRootPaginatedDocument() != 0 {
+ return self.page_size_minus_default_margin(pc);
+ }
+
+ match variant {
+ ViewportVariant::UADefault => {
+ let size = &pc.mSizeForViewportUnits;
+ Size2D::new(Au(size.width), Au(size.height))
+ },
+ ViewportVariant::Small => {
+ let size = &pc.mVisibleArea;
+ Size2D::new(Au(size.width), Au(size.height))
+ },
+ ViewportVariant::Large => {
+ let size = &pc.mVisibleArea;
+ // Looks like IntCoordTyped is treated as if it's u32 in Rust.
+ debug_assert!(
+ /* pc.mDynamicToolbarMaxHeight >=0 && */
+ pc.mDynamicToolbarMaxHeight < i32::MAX as u32
+ );
+ Size2D::new(
+ Au(size.width),
+ Au(size.height +
+ pc.mDynamicToolbarMaxHeight as i32 * pc.mCurAppUnitsPerDevPixel),
+ )
+ },
+ ViewportVariant::Dynamic => {
+ self.used_dynamic_viewport_size
+ .store(true, Ordering::Relaxed);
+ let size = &pc.mVisibleArea;
+ // Looks like IntCoordTyped is treated as if it's u32 in Rust.
+ debug_assert!(
+ /* pc.mDynamicToolbarHeight >=0 && */
+ pc.mDynamicToolbarHeight < i32::MAX as u32
+ );
+ Size2D::new(
+ Au(size.width),
+ Au(size.height +
+ (pc.mDynamicToolbarMaxHeight - pc.mDynamicToolbarHeight) as i32 *
+ pc.mCurAppUnitsPerDevPixel),
+ )
+ },
+ }
+ }
+
+ /// Returns whether we ever looked up the viewport size of the Device.
+ pub fn used_viewport_size(&self) -> bool {
+ self.used_viewport_size.load(Ordering::Relaxed)
+ }
+
+ /// Returns whether we ever looked up the dynamic viewport size of the Device.
+ pub fn used_dynamic_viewport_size(&self) -> bool {
+ self.used_dynamic_viewport_size.load(Ordering::Relaxed)
+ }
+
+ /// Returns whether font metrics have been queried.
+ pub fn used_font_metrics(&self) -> bool {
+ self.used_font_metrics.load(Ordering::Relaxed)
+ }
+
+ /// Returns whether visited styles are enabled.
+ pub fn visited_styles_enabled(&self) -> bool {
+ unsafe { bindings::Gecko_VisitedStylesEnabled(self.document()) }
+ }
+
+ /// Returns the number of app units per device pixel we're using currently.
+ pub fn app_units_per_device_pixel(&self) -> i32 {
+ match self.pres_context() {
+ Some(pc) => pc.mCurAppUnitsPerDevPixel,
+ None => AU_PER_PX,
+ }
+ }
+
+ /// Returns the device pixel ratio.
+ pub fn device_pixel_ratio(&self) -> Scale<f32, CSSPixel, DevicePixel> {
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return Scale::new(1.),
+ };
+
+ if pc.mMediaEmulationData.mDPPX > 0.0 {
+ return Scale::new(pc.mMediaEmulationData.mDPPX);
+ }
+
+ let au_per_dpx = pc.mCurAppUnitsPerDevPixel as f32;
+ let au_per_px = AU_PER_PX as f32;
+ Scale::new(au_per_px / au_per_dpx)
+ }
+
+ /// Returns whether document colors are enabled.
+ #[inline]
+ pub fn use_document_colors(&self) -> bool {
+ let doc = self.document();
+ if doc.mIsBeingUsedAsImage() {
+ return true;
+ }
+ self.pref_sheet_prefs().mUseDocumentColors
+ }
+
+ /// Computes a system color and returns it as an nscolor.
+ pub(crate) fn system_nscolor(
+ &self,
+ system_color: SystemColor,
+ color_scheme: &ColorScheme,
+ ) -> u32 {
+ unsafe { bindings::Gecko_ComputeSystemColor(system_color, self.document(), color_scheme) }
+ }
+
+ /// Returns whether the used color-scheme for `color-scheme` should be dark.
+ pub(crate) fn is_dark_color_scheme(&self, color_scheme: &ColorScheme) -> bool {
+ unsafe { bindings::Gecko_IsDarkColorScheme(self.document(), color_scheme) }
+ }
+
+ /// Returns the default background color.
+ ///
+ /// This is only for forced-colors/high-contrast, so looking at light colors
+ /// is ok.
+ pub fn default_background_color(&self) -> AbsoluteColor {
+ let normal = ColorScheme::normal();
+ convert_nscolor_to_absolute_color(self.system_nscolor(SystemColor::Canvas, &normal))
+ }
+
+ /// Returns the default foreground color.
+ ///
+ /// See above for looking at light colors only.
+ pub fn default_color(&self) -> AbsoluteColor {
+ let normal = ColorScheme::normal();
+ convert_nscolor_to_absolute_color(self.system_nscolor(SystemColor::Canvastext, &normal))
+ }
+
+ /// Returns the current effective text zoom.
+ #[inline]
+ fn text_zoom(&self) -> f32 {
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return 1.,
+ };
+ pc.mTextZoom
+ }
+
+ /// Applies text zoom to a font-size or line-height value (see nsStyleFont::ZoomText).
+ #[inline]
+ pub fn zoom_text(&self, size: Length) -> Length {
+ size.scale_by(self.text_zoom())
+ }
+
+ /// Un-apply text zoom.
+ #[inline]
+ pub fn unzoom_text(&self, size: Length) -> Length {
+ size.scale_by(1. / self.text_zoom())
+ }
+
+ /// Returns safe area insets
+ pub fn safe_area_insets(&self) -> SideOffsets2D<f32, CSSPixel> {
+ let pc = match self.pres_context() {
+ Some(pc) => pc,
+ None => return SideOffsets2D::zero(),
+ };
+ let mut top = 0.0;
+ let mut right = 0.0;
+ let mut bottom = 0.0;
+ let mut left = 0.0;
+ unsafe {
+ bindings::Gecko_GetSafeAreaInsets(pc, &mut top, &mut right, &mut bottom, &mut left)
+ };
+ SideOffsets2D::new(top, right, bottom, left)
+ }
+
+ /// Returns true if the given MIME type is supported
+ pub fn is_supported_mime_type(&self, mime_type: &str) -> bool {
+ unsafe {
+ bindings::Gecko_IsSupportedImageMimeType(mime_type.as_ptr(), mime_type.len() as u32)
+ }
+ }
+
+ /// Return whether the document is a chrome document.
+ ///
+ /// This check is consistent with how we enable chrome rules for chrome:// and resource://
+ /// stylesheets (and thus chrome:// documents).
+ #[inline]
+ pub fn chrome_rules_enabled_for_document(&self) -> bool {
+ self.document().mChromeRulesEnabled()
+ }
+}
diff --git a/servo/components/style/gecko/mod.rs b/servo/components/style/gecko/mod.rs
new file mode 100644
index 0000000000..c32ded14f3
--- /dev/null
+++ b/servo/components/style/gecko/mod.rs
@@ -0,0 +1,23 @@
+/* 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/. */
+
+//! Gecko-specific style-system bits.
+
+#[macro_use]
+mod non_ts_pseudo_class_list;
+
+pub mod arc_types;
+pub mod conversions;
+pub mod data;
+pub mod media_features;
+pub mod media_queries;
+pub mod pseudo_element;
+pub mod restyle_damage;
+pub mod selector_parser;
+pub mod snapshot;
+pub mod snapshot_helpers;
+pub mod traversal;
+pub mod url;
+pub mod values;
+pub mod wrapper;
diff --git a/servo/components/style/gecko/non_ts_pseudo_class_list.rs b/servo/components/style/gecko/non_ts_pseudo_class_list.rs
new file mode 100644
index 0000000000..cc7495dd9c
--- /dev/null
+++ b/servo/components/style/gecko/non_ts_pseudo_class_list.rs
@@ -0,0 +1,106 @@
+/* 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/. */
+
+/*
+ * This file contains a helper macro includes all supported non-tree-structural
+ * pseudo-classes.
+ *
+ * FIXME: Find a way to autogenerate this file.
+ *
+ * Expected usage is as follows:
+ * ```
+ * macro_rules! pseudo_class_macro{
+ * ([$(($css:expr, $name:ident, $gecko_type:tt, $state:tt, $flags:tt),)*]) => {
+ * // do stuff
+ * }
+ * }
+ * apply_non_ts_list!(pseudo_class_macro)
+ * ```
+ *
+ * $gecko_type can be either "_" or an ident in Gecko's CSSPseudoClassType.
+ * $state can be either "_" or an expression of type ElementState. If present,
+ * the semantics are that the pseudo-class matches if any of the bits in
+ * $state are set on the element.
+ * $flags can be either "_" or an expression of type NonTSPseudoClassFlag,
+ * see selector_parser.rs for more details.
+ */
+
+macro_rules! apply_non_ts_list {
+ ($apply_macro:ident) => {
+ $apply_macro! {
+ [
+ ("-moz-table-border-nonzero", MozTableBorderNonzero, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-select-list-box", MozSelectListBox, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("link", Link, UNVISITED, _),
+ ("any-link", AnyLink, VISITED_OR_UNVISITED, _),
+ ("visited", Visited, VISITED, _),
+ ("active", Active, ACTIVE, _),
+ ("autofill", Autofill, AUTOFILL, _),
+ ("checked", Checked, CHECKED, _),
+ ("defined", Defined, DEFINED, _),
+ ("disabled", Disabled, DISABLED, _),
+ ("enabled", Enabled, ENABLED, _),
+ ("focus", Focus, FOCUS, _),
+ ("focus-within", FocusWithin, FOCUS_WITHIN, _),
+ ("focus-visible", FocusVisible, FOCUSRING, _),
+ ("hover", Hover, HOVER, _),
+ ("-moz-drag-over", MozDragOver, DRAGOVER, _),
+ ("target", Target, URLTARGET, _),
+ ("indeterminate", Indeterminate, INDETERMINATE, _),
+ ("-moz-inert", MozInert, INERT, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-devtools-highlighted", MozDevtoolsHighlighted, DEVTOOLS_HIGHLIGHTED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-styleeditor-transitioning", MozStyleeditorTransitioning, STYLEEDITOR_TRANSITIONING, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("fullscreen", Fullscreen, FULLSCREEN, _),
+ ("modal", Modal, MODAL, _),
+ ("-moz-topmost-modal", MozTopmostModal, TOPMOST_MODAL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-broken", MozBroken, BROKEN, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME),
+ ("-moz-has-dir-attr", MozHasDirAttr, HAS_DIR_ATTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-dir-attr-ltr", MozDirAttrLTR, HAS_DIR_ATTR_LTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-dir-attr-rtl", MozDirAttrRTL, HAS_DIR_ATTR_RTL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-dir-attr-like-auto", MozDirAttrLikeAuto, HAS_DIR_ATTR_LIKE_AUTO, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+
+ ("-moz-autofill-preview", MozAutofillPreview, AUTOFILL_PREVIEW, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME),
+ ("-moz-value-empty", MozValueEmpty, VALUE_EMPTY, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-revealed", MozRevealed, REVEALED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+
+ ("-moz-math-increment-script-level", MozMathIncrementScriptLevel, INCREMENT_SCRIPT_LEVEL, _),
+
+ ("required", Required, REQUIRED, _),
+ ("popover-open", PopoverOpen, POPOVER_OPEN, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME),
+ ("optional", Optional, OPTIONAL_, _),
+ ("valid", Valid, VALID, _),
+ ("invalid", Invalid, INVALID, _),
+ ("in-range", InRange, INRANGE, _),
+ ("out-of-range", OutOfRange, OUTOFRANGE, _),
+ ("default", Default, DEFAULT, _),
+ ("placeholder-shown", PlaceholderShown, PLACEHOLDER_SHOWN, _),
+ ("read-only", ReadOnly, READONLY, _),
+ ("read-write", ReadWrite, READWRITE, _),
+ ("user-valid", UserValid, USER_VALID, _),
+ ("user-invalid", UserInvalid, USER_INVALID, _),
+ ("-moz-meter-optimum", MozMeterOptimum, OPTIMUM, _),
+ ("-moz-meter-sub-optimum", MozMeterSubOptimum, SUB_OPTIMUM, _),
+ ("-moz-meter-sub-sub-optimum", MozMeterSubSubOptimum, SUB_SUB_OPTIMUM, _),
+
+ ("-moz-first-node", MozFirstNode, _, _),
+ ("-moz-last-node", MozLastNode, _, _),
+ ("-moz-only-whitespace", MozOnlyWhitespace, _, _),
+ ("-moz-native-anonymous", MozNativeAnonymous, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-placeholder", MozPlaceholder, _, _),
+
+ // NOTE(emilio): Pseudo-classes below only depend on document state, and thus
+ // conceptually they should probably be media queries instead.
+ //
+ // However that has a set of trade-offs that might not be worth making. In
+ // particular, such media queries would prevent documents that match them from
+ // sharing user-agent stylesheets with documents that don't. Also, changes between
+ // media query results are more expensive than document state changes. So for now
+ // making them pseudo-classes is probably the right trade-off.
+ ("-moz-is-html", MozIsHTML, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS),
+ ("-moz-lwtheme", MozLWTheme, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME),
+ ("-moz-window-inactive", MozWindowInactive, _, _),
+ ]
+ }
+ }
+}
diff --git a/servo/components/style/gecko/pseudo_element.rs b/servo/components/style/gecko/pseudo_element.rs
new file mode 100644
index 0000000000..3bcd873455
--- /dev/null
+++ b/servo/components/style/gecko/pseudo_element.rs
@@ -0,0 +1,233 @@
+/* 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/. */
+
+//! Gecko's definition of a pseudo-element.
+//!
+//! Note that a few autogenerated bits of this live in
+//! `pseudo_element_definition.mako.rs`. If you touch that file, you probably
+//! need to update the checked-in files for Servo.
+
+use crate::gecko_bindings::structs::{self, PseudoStyleType};
+use crate::properties::longhands::display::computed_value::T as Display;
+use crate::properties::{ComputedValues, PropertyFlags};
+use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl};
+use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
+use crate::string_cache::Atom;
+use crate::values::serialize_atom_identifier;
+use crate::values::AtomIdent;
+use cssparser::ToCss;
+use static_prefs::pref;
+use std::fmt;
+
+include!(concat!(
+ env!("OUT_DIR"),
+ "/gecko/pseudo_element_definition.rs"
+));
+
+impl ::selectors::parser::PseudoElement for PseudoElement {
+ type Impl = SelectorImpl;
+
+ // ::slotted() should support all tree-abiding pseudo-elements, see
+ // https://drafts.csswg.org/css-scoping/#slotted-pseudo
+ // https://drafts.csswg.org/css-pseudo-4/#treelike
+ #[inline]
+ fn valid_after_slotted(&self) -> bool {
+ matches!(
+ *self,
+ Self::Before |
+ Self::After |
+ Self::Marker |
+ Self::Placeholder |
+ Self::FileSelectorButton
+ )
+ }
+
+ #[inline]
+ fn accepts_state_pseudo_classes(&self) -> bool {
+ self.supports_user_action_state()
+ }
+}
+
+impl PseudoElement {
+ /// Returns the kind of cascade type that a given pseudo is going to use.
+ ///
+ /// In Gecko we only compute ::before and ::after eagerly. We save the rules
+ /// for anonymous boxes separately, so we resolve them as precomputed
+ /// pseudos.
+ ///
+ /// We resolve the others lazily, see `Servo_ResolvePseudoStyle`.
+ pub fn cascade_type(&self) -> PseudoElementCascadeType {
+ if self.is_eager() {
+ debug_assert!(!self.is_anon_box());
+ return PseudoElementCascadeType::Eager;
+ }
+
+ if self.is_precomputed() {
+ return PseudoElementCascadeType::Precomputed;
+ }
+
+ PseudoElementCascadeType::Lazy
+ }
+
+ /// Gets the canonical index of this eagerly-cascaded pseudo-element.
+ #[inline]
+ pub fn eager_index(&self) -> usize {
+ EAGER_PSEUDOS
+ .iter()
+ .position(|p| p == self)
+ .expect("Not an eager pseudo")
+ }
+
+ /// Creates a pseudo-element from an eager index.
+ #[inline]
+ pub fn from_eager_index(i: usize) -> Self {
+ EAGER_PSEUDOS[i].clone()
+ }
+
+ /// Whether animations for the current pseudo element are stored in the
+ /// parent element.
+ #[inline]
+ pub fn animations_stored_in_parent(&self) -> bool {
+ matches!(*self, Self::Before | Self::After | Self::Marker)
+ }
+
+ /// Whether the current pseudo element is ::before or ::after.
+ #[inline]
+ pub fn is_before_or_after(&self) -> bool {
+ matches!(*self, Self::Before | Self::After)
+ }
+
+ /// Whether this pseudo-element is the ::before pseudo.
+ #[inline]
+ pub fn is_before(&self) -> bool {
+ *self == PseudoElement::Before
+ }
+
+ /// Whether this pseudo-element is the ::after pseudo.
+ #[inline]
+ pub fn is_after(&self) -> bool {
+ *self == PseudoElement::After
+ }
+
+ /// Whether this pseudo-element is the ::marker pseudo.
+ #[inline]
+ pub fn is_marker(&self) -> bool {
+ *self == PseudoElement::Marker
+ }
+
+ /// Whether this pseudo-element is the ::selection pseudo.
+ #[inline]
+ pub fn is_selection(&self) -> bool {
+ *self == PseudoElement::Selection
+ }
+
+ /// Whether this pseudo-element is ::first-letter.
+ #[inline]
+ pub fn is_first_letter(&self) -> bool {
+ *self == PseudoElement::FirstLetter
+ }
+
+ /// Whether this pseudo-element is ::first-line.
+ #[inline]
+ pub fn is_first_line(&self) -> bool {
+ *self == PseudoElement::FirstLine
+ }
+
+ /// Whether this pseudo-element is the ::-moz-color-swatch pseudo.
+ #[inline]
+ pub fn is_color_swatch(&self) -> bool {
+ *self == PseudoElement::MozColorSwatch
+ }
+
+ /// Whether this pseudo-element is lazily-cascaded.
+ #[inline]
+ pub fn is_lazy(&self) -> bool {
+ !self.is_eager() && !self.is_precomputed()
+ }
+
+ /// The identifier of the highlight this pseudo-element represents.
+ pub fn highlight_name(&self) -> Option<&AtomIdent> {
+ match *self {
+ Self::Highlight(ref name) => Some(name),
+ _ => None,
+ }
+ }
+
+ /// Whether this pseudo-element is the ::highlight pseudo.
+ pub fn is_highlight(&self) -> bool {
+ matches!(*self, Self::Highlight(_))
+ }
+
+ /// Whether this pseudo-element supports user action selectors.
+ pub fn supports_user_action_state(&self) -> bool {
+ (self.flags() & structs::CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE) != 0
+ }
+
+ /// Whether this pseudo-element is enabled for all content.
+ pub fn enabled_in_content(&self) -> bool {
+ match *self {
+ Self::Highlight(..) => pref!("dom.customHighlightAPI.enabled"),
+ Self::SliderFill | Self::SliderTrack | Self::SliderThumb => {
+ pref!("layout.css.modern-range-pseudos.enabled")
+ },
+ // If it's not explicitly enabled in UA sheets or chrome, then we're enabled for
+ // content.
+ _ => (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME) == 0,
+ }
+ }
+
+ /// Whether this pseudo is enabled explicitly in UA sheets.
+ pub fn enabled_in_ua_sheets(&self) -> bool {
+ (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS) != 0
+ }
+
+ /// Whether this pseudo is enabled explicitly in chrome sheets.
+ pub fn enabled_in_chrome(&self) -> bool {
+ (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_CHROME) != 0
+ }
+
+ /// Whether this pseudo-element skips flex/grid container display-based
+ /// fixup.
+ #[inline]
+ pub fn skip_item_display_fixup(&self) -> bool {
+ (self.flags() & structs::CSS_PSEUDO_ELEMENT_IS_FLEX_OR_GRID_ITEM) == 0
+ }
+
+ /// Whether this pseudo-element is precomputed.
+ #[inline]
+ pub fn is_precomputed(&self) -> bool {
+ self.is_anon_box() && !self.is_tree_pseudo_element()
+ }
+
+ /// Property flag that properties must have to apply to this pseudo-element.
+ #[inline]
+ pub fn property_restriction(&self) -> Option<PropertyFlags> {
+ Some(match *self {
+ PseudoElement::FirstLetter => PropertyFlags::APPLIES_TO_FIRST_LETTER,
+ PseudoElement::FirstLine => PropertyFlags::APPLIES_TO_FIRST_LINE,
+ PseudoElement::Placeholder => PropertyFlags::APPLIES_TO_PLACEHOLDER,
+ PseudoElement::Cue => PropertyFlags::APPLIES_TO_CUE,
+ PseudoElement::Marker if static_prefs::pref!("layout.css.marker.restricted") => {
+ PropertyFlags::APPLIES_TO_MARKER
+ },
+ _ => return None,
+ })
+ }
+
+ /// Whether this pseudo-element should actually exist if it has
+ /// the given styles.
+ pub fn should_exist(&self, style: &ComputedValues) -> bool {
+ debug_assert!(self.is_eager());
+
+ if style.get_box().clone_display() == Display::None {
+ return false;
+ }
+
+ if self.is_before_or_after() && style.ineffective_content_property() {
+ return false;
+ }
+
+ true
+ }
+}
diff --git a/servo/components/style/gecko/pseudo_element_definition.mako.rs b/servo/components/style/gecko/pseudo_element_definition.mako.rs
new file mode 100644
index 0000000000..48f0618502
--- /dev/null
+++ b/servo/components/style/gecko/pseudo_element_definition.mako.rs
@@ -0,0 +1,278 @@
+/* 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/. */
+
+/// Gecko's pseudo-element definition.
+///
+/// We intentionally double-box legacy ::-moz-tree pseudo-elements to keep the
+/// size of PseudoElement (and thus selector components) small.
+#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)]
+pub enum PseudoElement {
+ % for pseudo in PSEUDOS:
+ /// ${pseudo.value}
+ % if pseudo.is_tree_pseudo_element():
+ ${pseudo.capitalized_pseudo()}(thin_vec::ThinVec<Atom>),
+ % elif pseudo.pseudo_ident == "highlight":
+ ${pseudo.capitalized_pseudo()}(AtomIdent),
+ % else:
+ ${pseudo.capitalized_pseudo()},
+ % endif
+ % endfor
+ /// ::-webkit-* that we don't recognize
+ /// https://github.com/whatwg/compat/issues/103
+ UnknownWebkit(Atom),
+}
+
+/// Important: If you change this, you should also update Gecko's
+/// nsCSSPseudoElements::IsEagerlyCascadedInServo.
+<% EAGER_PSEUDOS = ["Before", "After", "FirstLine", "FirstLetter"] %>
+<% TREE_PSEUDOS = [pseudo for pseudo in PSEUDOS if pseudo.is_tree_pseudo_element()] %>
+<% SIMPLE_PSEUDOS = [pseudo for pseudo in PSEUDOS if pseudo.is_simple_pseudo_element()] %>
+
+/// The number of eager pseudo-elements.
+pub const EAGER_PSEUDO_COUNT: usize = ${len(EAGER_PSEUDOS)};
+
+/// The number of non-functional pseudo-elements.
+pub const SIMPLE_PSEUDO_COUNT: usize = ${len(SIMPLE_PSEUDOS)};
+
+/// The number of tree pseudo-elements.
+pub const TREE_PSEUDO_COUNT: usize = ${len(TREE_PSEUDOS)};
+
+/// The number of all pseudo-elements.
+pub const PSEUDO_COUNT: usize = ${len(PSEUDOS)};
+
+/// The list of eager pseudos.
+pub const EAGER_PSEUDOS: [PseudoElement; EAGER_PSEUDO_COUNT] = [
+ % for eager_pseudo_name in EAGER_PSEUDOS:
+ PseudoElement::${eager_pseudo_name},
+ % endfor
+];
+
+<%def name="pseudo_element_variant(pseudo, tree_arg='..')">\
+PseudoElement::${pseudo.capitalized_pseudo()}${"({})".format(tree_arg) if not pseudo.is_simple_pseudo_element() else ""}\
+</%def>
+
+impl PseudoElement {
+ /// Returns an index of the pseudo-element.
+ #[inline]
+ pub fn index(&self) -> usize {
+ match *self {
+ % for i, pseudo in enumerate(PSEUDOS):
+ ${pseudo_element_variant(pseudo)} => ${i},
+ % endfor
+ PseudoElement::UnknownWebkit(..) => unreachable!(),
+ }
+ }
+
+ /// Returns an array of `None` values.
+ ///
+ /// FIXME(emilio): Integer generics can't come soon enough.
+ pub fn pseudo_none_array<T>() -> [Option<T>; PSEUDO_COUNT] {
+ [
+ ${",\n ".join(["None" for pseudo in PSEUDOS])}
+ ]
+ }
+
+ /// Whether this pseudo-element is an anonymous box.
+ #[inline]
+ pub fn is_anon_box(&self) -> bool {
+ match *self {
+ % for pseudo in PSEUDOS:
+ % if pseudo.is_anon_box():
+ ${pseudo_element_variant(pseudo)} => true,
+ % endif
+ % endfor
+ _ => false,
+ }
+ }
+
+ /// Whether this pseudo-element is eagerly-cascaded.
+ #[inline]
+ pub fn is_eager(&self) -> bool {
+ matches!(*self,
+ ${" | ".join(map(lambda name: "PseudoElement::{}".format(name), EAGER_PSEUDOS))})
+ }
+
+ /// Whether this pseudo-element is tree pseudo-element.
+ #[inline]
+ pub fn is_tree_pseudo_element(&self) -> bool {
+ match *self {
+ % for pseudo in TREE_PSEUDOS:
+ ${pseudo_element_variant(pseudo)} => true,
+ % endfor
+ _ => false,
+ }
+ }
+
+ /// Whether this pseudo-element is an unknown Webkit-prefixed pseudo-element.
+ #[inline]
+ pub fn is_unknown_webkit_pseudo_element(&self) -> bool {
+ matches!(*self, PseudoElement::UnknownWebkit(..))
+ }
+
+ /// Gets the flags associated to this pseudo-element, or 0 if it's an
+ /// anonymous box.
+ pub fn flags(&self) -> u32 {
+ match *self {
+ % for pseudo in PSEUDOS:
+ ${pseudo_element_variant(pseudo)} =>
+ % if pseudo.is_tree_pseudo_element():
+ structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME,
+ % elif pseudo.is_anon_box():
+ structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS,
+ % else:
+ structs::SERVO_CSS_PSEUDO_ELEMENT_FLAGS_${pseudo.pseudo_ident},
+ % endif
+ % endfor
+ PseudoElement::UnknownWebkit(..) => 0,
+ }
+ }
+
+ /// Construct a pseudo-element from a `PseudoStyleType`.
+ #[inline]
+ pub fn from_pseudo_type(type_: PseudoStyleType, functional_pseudo_parameter: Option<AtomIdent>) -> Option<Self> {
+ match type_ {
+ % for pseudo in PSEUDOS:
+ % if pseudo.is_simple_pseudo_element():
+ PseudoStyleType::${pseudo.pseudo_ident} => {
+ debug_assert!(functional_pseudo_parameter.is_none());
+ Some(${pseudo_element_variant(pseudo)})
+ },
+ % endif
+ % endfor
+ PseudoStyleType::highlight => {
+ match functional_pseudo_parameter {
+ Some(p) => Some(PseudoElement::Highlight(p)),
+ None => None
+ }
+ }
+ _ => None,
+ }
+ }
+
+ /// Construct a `PseudoStyleType` from a pseudo-element
+ #[inline]
+ pub fn pseudo_type(&self) -> PseudoStyleType {
+ match *self {
+ % for pseudo in PSEUDOS:
+ % if pseudo.is_tree_pseudo_element():
+ PseudoElement::${pseudo.capitalized_pseudo()}(..) => PseudoStyleType::XULTree,
+ % elif pseudo.pseudo_ident == "highlight":
+ PseudoElement::${pseudo.capitalized_pseudo()}(..) => PseudoStyleType::${pseudo.pseudo_ident},
+ % else:
+ PseudoElement::${pseudo.capitalized_pseudo()} => PseudoStyleType::${pseudo.pseudo_ident},
+ % endif
+ % endfor
+ PseudoElement::UnknownWebkit(..) => unreachable!(),
+ }
+ }
+
+ /// Get the argument list of a tree pseudo-element.
+ #[inline]
+ pub fn tree_pseudo_args(&self) -> Option<<&[Atom]> {
+ match *self {
+ % for pseudo in TREE_PSEUDOS:
+ PseudoElement::${pseudo.capitalized_pseudo()}(ref args) => Some(args),
+ % endfor
+ _ => None,
+ }
+ }
+
+ /// Construct a tree pseudo-element from atom and args.
+ #[inline]
+ pub fn from_tree_pseudo_atom(atom: &Atom, args: Box<[Atom]>) -> Option<Self> {
+ % for pseudo in PSEUDOS:
+ % if pseudo.is_tree_pseudo_element():
+ if atom == &atom!("${pseudo.value}") {
+ return Some(PseudoElement::${pseudo.capitalized_pseudo()}(args.into()));
+ }
+ % endif
+ % endfor
+ None
+ }
+
+ /// Constructs a pseudo-element from a string of text.
+ ///
+ /// Returns `None` if the pseudo-element is not recognised.
+ #[inline]
+ pub fn from_slice(name: &str, allow_unkown_webkit: bool) -> Option<Self> {
+ // We don't need to support tree pseudos because functional
+ // pseudo-elements needs arguments, and thus should be created
+ // via other methods.
+ ascii_case_insensitive_phf_map! {
+ pseudo -> PseudoElement = {
+ % for pseudo in SIMPLE_PSEUDOS:
+ "${pseudo.value[1:]}" => ${pseudo_element_variant(pseudo)},
+ % endfor
+ // Alias some legacy prefixed pseudos to their standardized name at parse time:
+ "-moz-selection" => PseudoElement::Selection,
+ "-moz-placeholder" => PseudoElement::Placeholder,
+ "-moz-list-bullet" => PseudoElement::Marker,
+ "-moz-list-number" => PseudoElement::Marker,
+ }
+ }
+ if let Some(p) = pseudo::get(name) {
+ return Some(p.clone());
+ }
+ if starts_with_ignore_ascii_case(name, "-moz-tree-") {
+ return PseudoElement::tree_pseudo_element(name, Default::default())
+ }
+ const WEBKIT_PREFIX: &str = "-webkit-";
+ if allow_unkown_webkit && starts_with_ignore_ascii_case(name, WEBKIT_PREFIX) {
+ let part = string_as_ascii_lowercase(&name[WEBKIT_PREFIX.len()..]);
+ return Some(PseudoElement::UnknownWebkit(part.into()));
+ }
+ None
+ }
+
+ /// Constructs a tree pseudo-element from the given name and arguments.
+ /// "name" must start with "-moz-tree-".
+ ///
+ /// Returns `None` if the pseudo-element is not recognized.
+ #[inline]
+ pub fn tree_pseudo_element(name: &str, args: thin_vec::ThinVec<Atom>) -> Option<Self> {
+ debug_assert!(starts_with_ignore_ascii_case(name, "-moz-tree-"));
+ let tree_part = &name[10..];
+ % for pseudo in TREE_PSEUDOS:
+ if tree_part.eq_ignore_ascii_case("${pseudo.value[11:]}") {
+ return Some(${pseudo_element_variant(pseudo, "args")});
+ }
+ % endfor
+ None
+ }
+}
+
+impl ToCss for PseudoElement {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write {
+ dest.write_char(':')?;
+ match *self {
+ % for pseudo in (p for p in PSEUDOS if p.pseudo_ident != "highlight"):
+ ${pseudo_element_variant(pseudo)} => dest.write_str("${pseudo.value}")?,
+ % endfor
+ PseudoElement::Highlight(ref name) => {
+ dest.write_str(":highlight(")?;
+ serialize_atom_identifier(name, dest)?;
+ dest.write_char(')')?;
+ }
+ PseudoElement::UnknownWebkit(ref atom) => {
+ dest.write_str(":-webkit-")?;
+ serialize_atom_identifier(atom, dest)?;
+ }
+ }
+ if let Some(args) = self.tree_pseudo_args() {
+ if !args.is_empty() {
+ dest.write_char('(')?;
+ let mut iter = args.iter();
+ if let Some(first) = iter.next() {
+ serialize_atom_identifier(&first, dest)?;
+ for item in iter {
+ dest.write_str(", ")?;
+ serialize_atom_identifier(item, dest)?;
+ }
+ }
+ dest.write_char(')')?;
+ }
+ }
+ Ok(())
+ }
+}
diff --git a/servo/components/style/gecko/regen_atoms.py b/servo/components/style/gecko/regen_atoms.py
new file mode 100755
index 0000000000..61f2fc4c63
--- /dev/null
+++ b/servo/components/style/gecko/regen_atoms.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python
+
+# 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
+import os
+import sys
+
+from io import BytesIO
+
+GECKO_DIR = os.path.dirname(__file__.replace("\\", "/"))
+sys.path.insert(0, os.path.join(os.path.dirname(GECKO_DIR), "properties"))
+
+import build
+
+
+# Matches lines like `GK_ATOM(foo, "foo", 0x12345678, true, nsStaticAtom, PseudoElementAtom)`.
+PATTERN = re.compile(
+ '^GK_ATOM\(([^,]*),[^"]*"([^"]*)",\s*(0x[0-9a-f]+),\s*[^,]*,\s*([^,]*),\s*([^)]*)\)',
+ re.MULTILINE,
+)
+FILE = "include/nsGkAtomList.h"
+
+
+def map_atom(ident):
+ if ident in {
+ "box",
+ "loop",
+ "match",
+ "mod",
+ "ref",
+ "self",
+ "type",
+ "use",
+ "where",
+ "in",
+ }:
+ return ident + "_"
+ return ident
+
+
+class Atom:
+ def __init__(self, ident, value, hash, ty, atom_type):
+ self.ident = "nsGkAtoms_{}".format(ident)
+ self.original_ident = ident
+ self.value = value
+ self.hash = hash
+ # The Gecko type: "nsStaticAtom", "nsCSSPseudoElementStaticAtom", or
+ # "nsAnonBoxPseudoStaticAtom".
+ self.ty = ty
+ # The type of atom: "Atom", "PseudoElement", "NonInheritingAnonBox",
+ # or "InheritingAnonBox".
+ self.atom_type = atom_type
+
+ if (
+ self.is_pseudo_element()
+ or self.is_anon_box()
+ or self.is_tree_pseudo_element()
+ ):
+ self.pseudo_ident = (ident.split("_", 1))[1]
+
+ if self.is_anon_box():
+ assert self.is_inheriting_anon_box() or self.is_non_inheriting_anon_box()
+
+ def type(self):
+ return self.ty
+
+ def capitalized_pseudo(self):
+ return self.pseudo_ident[0].upper() + self.pseudo_ident[1:]
+
+ def is_pseudo_element(self):
+ return self.atom_type == "PseudoElementAtom"
+
+ def is_anon_box(self):
+ if self.is_tree_pseudo_element():
+ return False
+ return self.is_non_inheriting_anon_box() or self.is_inheriting_anon_box()
+
+ def is_non_inheriting_anon_box(self):
+ assert not self.is_tree_pseudo_element()
+ return self.atom_type == "NonInheritingAnonBoxAtom"
+
+ def is_inheriting_anon_box(self):
+ if self.is_tree_pseudo_element():
+ return False
+ return self.atom_type == "InheritingAnonBoxAtom"
+
+ def is_tree_pseudo_element(self):
+ return self.value.startswith(":-moz-tree-")
+
+ def is_simple_pseudo_element(self) -> bool:
+ return not (self.is_tree_pseudo_element() or self.pseudo_ident == "highlight")
+
+
+def collect_atoms(objdir):
+ atoms = []
+ path = os.path.abspath(os.path.join(objdir, FILE))
+ print("cargo:rerun-if-changed={}".format(path))
+ with open(path) as f:
+ content = f.read()
+ for result in PATTERN.finditer(content):
+ atoms.append(
+ Atom(
+ result.group(1),
+ result.group(2),
+ result.group(3),
+ result.group(4),
+ result.group(5),
+ )
+ )
+ return atoms
+
+
+class FileAvoidWrite(BytesIO):
+ """File-like object that buffers output and only writes if content changed."""
+
+ def __init__(self, filename):
+ BytesIO.__init__(self)
+ self.name = filename
+
+ def write(self, buf):
+ if isinstance(buf, str):
+ buf = buf.encode("utf-8")
+ BytesIO.write(self, buf)
+
+ def close(self):
+ buf = self.getvalue()
+ BytesIO.close(self)
+ try:
+ with open(self.name, "rb") as f:
+ old_content = f.read()
+ if old_content == buf:
+ print("{} is not changed, skip".format(self.name))
+ return
+ except IOError:
+ pass
+ with open(self.name, "wb") as f:
+ f.write(buf)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ if not self.closed:
+ self.close()
+
+
+PRELUDE = """
+/* 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/. */
+
+// Autogenerated file created by components/style/gecko/regen_atoms.py.
+// DO NOT EDIT DIRECTLY
+"""[
+ 1:
+]
+
+RULE_TEMPLATE = """
+ ("{atom}") => {{{{
+ #[allow(unsafe_code)] #[allow(unused_unsafe)]
+ unsafe {{ $crate::string_cache::Atom::from_index_unchecked({index}) }}
+ }}}};
+"""[
+ 1:
+]
+
+MACRO_TEMPLATE = """
+/// Returns a static atom by passing the literal string it represents.
+#[macro_export]
+macro_rules! atom {{
+{body}\
+}}
+"""
+
+
+def write_atom_macro(atoms, file_name):
+ with FileAvoidWrite(file_name) as f:
+ f.write(PRELUDE)
+ macro_rules = [
+ RULE_TEMPLATE.format(atom=atom.value, name=atom.ident, index=i)
+ for (i, atom) in enumerate(atoms)
+ ]
+ f.write(MACRO_TEMPLATE.format(body="".join(macro_rules)))
+
+
+def write_pseudo_elements(atoms, target_filename):
+ pseudos = []
+ for atom in atoms:
+ if (
+ atom.type() == "nsCSSPseudoElementStaticAtom"
+ or atom.type() == "nsCSSAnonBoxPseudoStaticAtom"
+ ):
+ pseudos.append(atom)
+
+ pseudo_definition_template = os.path.join(
+ GECKO_DIR, "pseudo_element_definition.mako.rs"
+ )
+ print("cargo:rerun-if-changed={}".format(pseudo_definition_template))
+ contents = build.render(pseudo_definition_template, PSEUDOS=pseudos)
+
+ with FileAvoidWrite(target_filename) as f:
+ f.write(contents)
+
+
+def generate_atoms(dist, out):
+ atoms = collect_atoms(dist)
+ write_atom_macro(atoms, os.path.join(out, "atom_macro.rs"))
+ write_pseudo_elements(atoms, os.path.join(out, "pseudo_element_definition.rs"))
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 3:
+ print("Usage: {} dist out".format(sys.argv[0]))
+ exit(2)
+ generate_atoms(sys.argv[1], sys.argv[2])
diff --git a/servo/components/style/gecko/restyle_damage.rs b/servo/components/style/gecko/restyle_damage.rs
new file mode 100644
index 0000000000..4749daea18
--- /dev/null
+++ b/servo/components/style/gecko/restyle_damage.rs
@@ -0,0 +1,121 @@
+/* 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/. */
+
+//! Gecko's restyle damage computation (aka change hints, aka `nsChangeHint`).
+
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs;
+use crate::gecko_bindings::structs::nsChangeHint;
+use crate::matching::{StyleChange, StyleDifference};
+use crate::properties::ComputedValues;
+use std::ops::{BitAnd, BitOr, BitOrAssign, Not};
+
+/// The representation of Gecko's restyle damage is just a wrapper over
+/// `nsChangeHint`.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct GeckoRestyleDamage(nsChangeHint);
+
+impl GeckoRestyleDamage {
+ /// Trivially construct a new `GeckoRestyleDamage`.
+ #[inline]
+ pub fn new(raw: nsChangeHint) -> Self {
+ GeckoRestyleDamage(raw)
+ }
+
+ /// Get the inner change hint for this damage.
+ #[inline]
+ pub fn as_change_hint(&self) -> nsChangeHint {
+ self.0
+ }
+
+ /// Get an empty change hint, that is (`nsChangeHint(0)`).
+ #[inline]
+ pub fn empty() -> Self {
+ GeckoRestyleDamage(nsChangeHint(0))
+ }
+
+ /// Returns whether this restyle damage represents the empty damage.
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.0 == nsChangeHint(0)
+ }
+
+ /// Computes the `StyleDifference` (including the appropriate change hint)
+ /// given an old and a new style.
+ pub fn compute_style_difference(
+ old_style: &ComputedValues,
+ new_style: &ComputedValues,
+ ) -> StyleDifference {
+ let mut any_style_changed = false;
+ let mut reset_only = false;
+ let hint = unsafe {
+ bindings::Gecko_CalcStyleDifference(
+ old_style.as_gecko_computed_style(),
+ new_style.as_gecko_computed_style(),
+ &mut any_style_changed,
+ &mut reset_only,
+ )
+ };
+ if reset_only && !old_style.custom_properties_equal(new_style) {
+ // The Gecko_CalcStyleDifference call only checks the non-custom
+ // property structs, so we check the custom properties here. Since
+ // they generate no damage themselves, we can skip this check if we
+ // already know we had some inherited (regular) property
+ // differences.
+ any_style_changed = true;
+ reset_only = false;
+ }
+ let change = if any_style_changed {
+ StyleChange::Changed { reset_only }
+ } else {
+ StyleChange::Unchanged
+ };
+ let damage = GeckoRestyleDamage(nsChangeHint(hint));
+ StyleDifference { damage, change }
+ }
+
+ /// Returns true if this restyle damage contains all the damage of |other|.
+ pub fn contains(self, other: Self) -> bool {
+ self & other == other
+ }
+
+ /// Gets restyle damage to reconstruct the entire frame, subsuming all
+ /// other damage.
+ pub fn reconstruct() -> Self {
+ GeckoRestyleDamage(structs::nsChangeHint::nsChangeHint_ReconstructFrame)
+ }
+}
+
+impl Default for GeckoRestyleDamage {
+ fn default() -> Self {
+ Self::empty()
+ }
+}
+
+impl BitOr for GeckoRestyleDamage {
+ type Output = Self;
+ fn bitor(self, other: Self) -> Self {
+ GeckoRestyleDamage(self.0 | other.0)
+ }
+}
+
+impl BitOrAssign for GeckoRestyleDamage {
+ fn bitor_assign(&mut self, other: Self) {
+ *self = *self | other;
+ }
+}
+
+impl BitAnd for GeckoRestyleDamage {
+ type Output = Self;
+ fn bitand(self, other: Self) -> Self {
+ GeckoRestyleDamage(nsChangeHint((self.0).0 & (other.0).0))
+ }
+}
+
+impl Not for GeckoRestyleDamage {
+ type Output = Self;
+ fn not(self) -> Self {
+ GeckoRestyleDamage(nsChangeHint(!(self.0).0))
+ }
+}
diff --git a/servo/components/style/gecko/selector_parser.rs b/servo/components/style/gecko/selector_parser.rs
new file mode 100644
index 0000000000..203e6a3609
--- /dev/null
+++ b/servo/components/style/gecko/selector_parser.rs
@@ -0,0 +1,519 @@
+/* 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/. */
+
+//! Gecko-specific bits for selector-parsing.
+
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::invalidation::element::document_state::InvalidationMatchingData;
+use crate::properties::ComputedValues;
+use crate::selector_parser::{Direction, HorizontalDirection, SelectorParser};
+use crate::str::starts_with_ignore_ascii_case;
+use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace};
+use crate::values::{AtomIdent, AtomString};
+use cssparser::{BasicParseError, BasicParseErrorKind, Parser};
+use cssparser::{CowRcStr, SourceLocation, ToCss, Token};
+use dom::{DocumentState, ElementState};
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss as ToCss_};
+use thin_vec::ThinVec;
+
+pub use crate::gecko::pseudo_element::{
+ PseudoElement, EAGER_PSEUDOS, EAGER_PSEUDO_COUNT, PSEUDO_COUNT,
+};
+pub use crate::gecko::snapshot::SnapshotMap;
+
+bitflags! {
+ // See NonTSPseudoClass::is_enabled_in()
+ #[derive(Copy, Clone)]
+ struct NonTSPseudoClassFlag: u8 {
+ const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS = 1 << 0;
+ const PSEUDO_CLASS_ENABLED_IN_CHROME = 1 << 1;
+ const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME =
+ NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS.bits() |
+ NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME.bits();
+ }
+}
+
+/// The type used to store the language argument to the `:lang` pseudo-class.
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+#[css(comma)]
+pub struct Lang(#[css(iterable)] pub ThinVec<AtomIdent>);
+
+/// The type used to store the state argument to the `:state` pseudo-class.
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub struct CustomState(pub AtomIdent);
+
+macro_rules! pseudo_class_name {
+ ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
+ /// Our representation of a non tree-structural pseudo-class.
+ #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+ pub enum NonTSPseudoClass {
+ $(
+ #[doc = $css]
+ $name,
+ )*
+ /// The `:lang` pseudo-class.
+ Lang(Lang),
+ /// The `:dir` pseudo-class.
+ Dir(Direction),
+ /// The :state` pseudo-class.
+ CustomState(CustomState),
+ /// The non-standard `:-moz-locale-dir` pseudo-class.
+ MozLocaleDir(Direction),
+ }
+ }
+}
+apply_non_ts_list!(pseudo_class_name);
+
+impl ToCss for NonTSPseudoClass {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ macro_rules! pseudo_class_serialize {
+ ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
+ match *self {
+ $(NonTSPseudoClass::$name => concat!(":", $css),)*
+ NonTSPseudoClass::Lang(ref lang) => {
+ dest.write_str(":lang(")?;
+ lang.to_css(&mut CssWriter::new(dest))?;
+ return dest.write_char(')');
+ },
+ NonTSPseudoClass::CustomState(ref state) => {
+ dest.write_str(":state(")?;
+ state.to_css(&mut CssWriter::new(dest))?;
+ return dest.write_char(')');
+ },
+ NonTSPseudoClass::MozLocaleDir(ref dir) => {
+ dest.write_str(":-moz-locale-dir(")?;
+ dir.to_css(&mut CssWriter::new(dest))?;
+ return dest.write_char(')')
+ },
+ NonTSPseudoClass::Dir(ref dir) => {
+ dest.write_str(":dir(")?;
+ dir.to_css(&mut CssWriter::new(dest))?;
+ return dest.write_char(')')
+ },
+ }
+ }
+ }
+ let ser = apply_non_ts_list!(pseudo_class_serialize);
+ dest.write_str(ser)
+ }
+}
+
+impl NonTSPseudoClass {
+ /// Parses the name and returns a non-ts-pseudo-class if succeeds.
+ /// None otherwise. It doesn't check whether the pseudo-class is enabled
+ /// in a particular state.
+ pub fn parse_non_functional(name: &str) -> Option<Self> {
+ macro_rules! pseudo_class_parse {
+ ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
+ match_ignore_ascii_case! { &name,
+ $($css => Some(NonTSPseudoClass::$name),)*
+ "-moz-full-screen" => Some(NonTSPseudoClass::Fullscreen),
+ "-moz-read-only" => Some(NonTSPseudoClass::ReadOnly),
+ "-moz-read-write" => Some(NonTSPseudoClass::ReadWrite),
+ "-moz-focusring" => Some(NonTSPseudoClass::FocusVisible),
+ "-moz-ui-valid" => Some(NonTSPseudoClass::UserValid),
+ "-moz-ui-invalid" => Some(NonTSPseudoClass::UserInvalid),
+ "-webkit-autofill" => Some(NonTSPseudoClass::Autofill),
+ _ => None,
+ }
+ }
+ }
+ apply_non_ts_list!(pseudo_class_parse)
+ }
+
+ /// Returns true if this pseudo-class has any of the given flags set.
+ fn has_any_flag(&self, flags: NonTSPseudoClassFlag) -> bool {
+ macro_rules! check_flag {
+ (_) => {
+ false
+ };
+ ($flags:ident) => {
+ NonTSPseudoClassFlag::$flags.intersects(flags)
+ };
+ }
+ macro_rules! pseudo_class_check_is_enabled_in {
+ ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
+ match *self {
+ $(NonTSPseudoClass::$name => check_flag!($flags),)*
+ NonTSPseudoClass::MozLocaleDir(_) => check_flag!(PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME),
+ NonTSPseudoClass::CustomState(_) |
+ NonTSPseudoClass::Lang(_) |
+ NonTSPseudoClass::Dir(_) => false,
+ }
+ }
+ }
+ apply_non_ts_list!(pseudo_class_check_is_enabled_in)
+ }
+
+ /// Returns whether the pseudo-class is enabled in content sheets.
+ #[inline]
+ fn is_enabled_in_content(&self) -> bool {
+ if matches!(*self, Self::PopoverOpen) {
+ return static_prefs::pref!("dom.element.popover.enabled");
+ }
+ if matches!(*self, Self::CustomState(_)) {
+ return static_prefs::pref!("dom.element.customstateset.enabled");
+ }
+ !self.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME)
+ }
+
+ /// Get the state flag associated with a pseudo-class, if any.
+ pub fn state_flag(&self) -> ElementState {
+ macro_rules! flag {
+ (_) => {
+ ElementState::empty()
+ };
+ ($state:ident) => {
+ ElementState::$state
+ };
+ }
+ macro_rules! pseudo_class_state {
+ ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => {
+ match *self {
+ $(NonTSPseudoClass::$name => flag!($state),)*
+ NonTSPseudoClass::Dir(ref dir) => dir.element_state(),
+ NonTSPseudoClass::MozLocaleDir(..) |
+ NonTSPseudoClass::CustomState(..) |
+ NonTSPseudoClass::Lang(..) => ElementState::empty(),
+ }
+ }
+ }
+ apply_non_ts_list!(pseudo_class_state)
+ }
+
+ /// Get the document state flag associated with a pseudo-class, if any.
+ pub fn document_state_flag(&self) -> DocumentState {
+ match *self {
+ NonTSPseudoClass::MozLocaleDir(ref dir) => match dir.as_horizontal_direction() {
+ Some(HorizontalDirection::Ltr) => DocumentState::LTR_LOCALE,
+ Some(HorizontalDirection::Rtl) => DocumentState::RTL_LOCALE,
+ None => DocumentState::empty(),
+ },
+ NonTSPseudoClass::MozWindowInactive => DocumentState::WINDOW_INACTIVE,
+ NonTSPseudoClass::MozLWTheme => DocumentState::LWTHEME,
+ _ => DocumentState::empty(),
+ }
+ }
+
+ /// Returns true if the given pseudoclass should trigger style sharing cache
+ /// revalidation.
+ pub fn needs_cache_revalidation(&self) -> bool {
+ self.state_flag().is_empty() &&
+ !matches!(
+ *self,
+ // :dir() depends on state only, but may have an empty state_flag for invalid
+ // arguments.
+ NonTSPseudoClass::Dir(_) |
+ // We prevent style sharing for NAC.
+ NonTSPseudoClass::MozNativeAnonymous |
+ // :-moz-placeholder is parsed but never matches.
+ NonTSPseudoClass::MozPlaceholder |
+ // :-moz-is-html, :-moz-lwtheme, :-moz-locale-dir and :-moz-window-inactive
+ // depend only on the state of the document, which is invariant across all
+ // elements involved in a given style cache.
+ NonTSPseudoClass::MozIsHTML |
+ NonTSPseudoClass::MozLWTheme |
+ NonTSPseudoClass::MozLocaleDir(_) |
+ NonTSPseudoClass::MozWindowInactive
+ )
+ }
+}
+
+impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass {
+ type Impl = SelectorImpl;
+
+ #[inline]
+ fn is_active_or_hover(&self) -> bool {
+ matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover)
+ }
+
+ /// We intentionally skip the link-related ones.
+ #[inline]
+ fn is_user_action_state(&self) -> bool {
+ matches!(
+ *self,
+ NonTSPseudoClass::Hover | NonTSPseudoClass::Active | NonTSPseudoClass::Focus
+ )
+ }
+}
+
+/// The dummy struct we use to implement our selector parsing.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct SelectorImpl;
+
+/// A set of extra data to carry along with the matching context, either for
+/// selector-matching or invalidation.
+#[derive(Default)]
+pub struct ExtraMatchingData<'a> {
+ /// The invalidation data to invalidate doc-state pseudo-classes correctly.
+ pub invalidation_data: InvalidationMatchingData,
+
+ /// The invalidation bits from matching container queries. These are here
+ /// just for convenience mostly.
+ pub cascade_input_flags: ComputedValueFlags,
+
+ /// The style of the originating element in order to evaluate @container
+ /// size queries affecting pseudo-elements.
+ pub originating_element_style: Option<&'a ComputedValues>,
+}
+
+impl ::selectors::SelectorImpl for SelectorImpl {
+ type ExtraMatchingData<'a> = ExtraMatchingData<'a>;
+ type AttrValue = AtomString;
+ type Identifier = AtomIdent;
+ type LocalName = AtomIdent;
+ type NamespacePrefix = AtomIdent;
+ type NamespaceUrl = Namespace;
+ type BorrowedNamespaceUrl = WeakNamespace;
+ type BorrowedLocalName = WeakAtom;
+
+ type PseudoElement = PseudoElement;
+ type NonTSPseudoClass = NonTSPseudoClass;
+
+ fn should_collect_attr_hash(name: &AtomIdent) -> bool {
+ !crate::bloom::is_attr_name_excluded_from_filter(name)
+ }
+}
+
+impl<'a> SelectorParser<'a> {
+ fn is_pseudo_class_enabled(&self, pseudo_class: &NonTSPseudoClass) -> bool {
+ if pseudo_class.is_enabled_in_content() {
+ return true;
+ }
+
+ if self.in_user_agent_stylesheet() &&
+ pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS)
+ {
+ return true;
+ }
+
+ if self.chrome_rules_enabled() &&
+ pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME)
+ {
+ return true;
+ }
+
+ if matches!(*pseudo_class, NonTSPseudoClass::MozBroken) {
+ return static_prefs::pref!("layout.css.moz-broken.content.enabled");
+ }
+
+ return false;
+ }
+
+ fn is_pseudo_element_enabled(&self, pseudo_element: &PseudoElement) -> bool {
+ if pseudo_element.enabled_in_content() {
+ return true;
+ }
+
+ if self.in_user_agent_stylesheet() && pseudo_element.enabled_in_ua_sheets() {
+ return true;
+ }
+
+ if self.chrome_rules_enabled() && pseudo_element.enabled_in_chrome() {
+ return true;
+ }
+
+ return false;
+ }
+}
+
+impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> {
+ type Impl = SelectorImpl;
+ type Error = StyleParseErrorKind<'i>;
+
+ #[inline]
+ fn parse_parent_selector(&self) -> bool {
+ true
+ }
+
+ #[inline]
+ fn parse_slotted(&self) -> bool {
+ true
+ }
+
+ #[inline]
+ fn parse_host(&self) -> bool {
+ true
+ }
+
+ #[inline]
+ fn parse_nth_child_of(&self) -> bool {
+ true
+ }
+
+ #[inline]
+ fn parse_is_and_where(&self) -> bool {
+ true
+ }
+
+ #[inline]
+ fn parse_has(&self) -> bool {
+ static_prefs::pref!("layout.css.has-selector.enabled")
+ }
+
+ #[inline]
+ fn parse_part(&self) -> bool {
+ true
+ }
+
+ #[inline]
+ fn is_is_alias(&self, function: &str) -> bool {
+ function.eq_ignore_ascii_case("-moz-any")
+ }
+
+ #[inline]
+ fn allow_forgiving_selectors(&self) -> bool {
+ !self.for_supports_rule
+ }
+
+ fn parse_non_ts_pseudo_class(
+ &self,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ ) -> Result<NonTSPseudoClass, ParseError<'i>> {
+ if let Some(pseudo_class) = NonTSPseudoClass::parse_non_functional(&name) {
+ if self.is_pseudo_class_enabled(&pseudo_class) {
+ return Ok(pseudo_class);
+ }
+ }
+ Err(
+ location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn parse_non_ts_functional_pseudo_class<'t>(
+ &self,
+ name: CowRcStr<'i>,
+ parser: &mut Parser<'i, 't>,
+ after_part: bool,
+ ) -> Result<NonTSPseudoClass, ParseError<'i>> {
+ let pseudo_class = match_ignore_ascii_case! { &name,
+ "lang" if !after_part => {
+ let result = parser.parse_comma_separated(|input| {
+ Ok(AtomIdent::from(input.expect_ident_or_string()?.as_ref()))
+ })?;
+ if result.is_empty() {
+ return Err(parser.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ NonTSPseudoClass::Lang(Lang(result.into()))
+ },
+ "state" => {
+ let result = AtomIdent::from(parser.expect_ident()?.as_ref());
+ NonTSPseudoClass::CustomState(CustomState(result))
+ },
+ "-moz-locale-dir" if !after_part => {
+ NonTSPseudoClass::MozLocaleDir(Direction::parse(parser)?)
+ },
+ "dir" if !after_part => {
+ NonTSPseudoClass::Dir(Direction::parse(parser)?)
+ },
+ _ => return Err(parser.new_custom_error(
+ SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone())
+ ))
+ };
+ if self.is_pseudo_class_enabled(&pseudo_class) {
+ Ok(pseudo_class)
+ } else {
+ Err(
+ parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+ }
+
+ fn parse_pseudo_element(
+ &self,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ ) -> Result<PseudoElement, ParseError<'i>> {
+ let allow_unkown_webkit = !self.for_supports_rule;
+ if let Some(pseudo) = PseudoElement::from_slice(&name, allow_unkown_webkit) {
+ if self.is_pseudo_element_enabled(&pseudo) {
+ return Ok(pseudo);
+ }
+ }
+
+ Err(
+ location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn parse_functional_pseudo_element<'t>(
+ &self,
+ name: CowRcStr<'i>,
+ parser: &mut Parser<'i, 't>,
+ ) -> Result<PseudoElement, ParseError<'i>> {
+ if starts_with_ignore_ascii_case(&name, "-moz-tree-") {
+ // Tree pseudo-elements can have zero or more arguments, separated
+ // by either comma or space.
+ let mut args = ThinVec::new();
+ loop {
+ let location = parser.current_source_location();
+ match parser.next() {
+ Ok(&Token::Ident(ref ident)) => args.push(Atom::from(ident.as_ref())),
+ Ok(&Token::Comma) => {},
+ Ok(t) => return Err(location.new_unexpected_token_error(t.clone())),
+ Err(BasicParseError {
+ kind: BasicParseErrorKind::EndOfInput,
+ ..
+ }) => break,
+ _ => unreachable!("Parser::next() shouldn't return any other error"),
+ }
+ }
+ if let Some(pseudo) = PseudoElement::tree_pseudo_element(&name, args) {
+ if self.is_pseudo_element_enabled(&pseudo) {
+ return Ok(pseudo);
+ }
+ }
+ } else if name.eq_ignore_ascii_case("highlight") {
+ let pseudo = PseudoElement::Highlight(AtomIdent::from(parser.expect_ident()?.as_ref()));
+ if self.is_pseudo_element_enabled(&pseudo) {
+ return Ok(pseudo);
+ }
+ }
+ Err(
+ parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement(
+ name,
+ )),
+ )
+ }
+
+ fn default_namespace(&self) -> Option<Namespace> {
+ self.namespaces.default.clone()
+ }
+
+ fn namespace_for_prefix(&self, prefix: &AtomIdent) -> Option<Namespace> {
+ self.namespaces.prefixes.get(prefix).cloned()
+ }
+}
+
+impl SelectorImpl {
+ /// A helper to traverse each eagerly cascaded pseudo-element, executing
+ /// `fun` on it.
+ #[inline]
+ pub fn each_eagerly_cascaded_pseudo_element<F>(mut fun: F)
+ where
+ F: FnMut(PseudoElement),
+ {
+ for pseudo in &EAGER_PSEUDOS {
+ fun(pseudo.clone())
+ }
+ }
+}
+
+// Selector and component sizes are important for matching performance.
+size_of_test!(selectors::parser::Selector<SelectorImpl>, 8);
+size_of_test!(selectors::parser::Component<SelectorImpl>, 24);
+size_of_test!(PseudoElement, 16);
+size_of_test!(NonTSPseudoClass, 16);
diff --git a/servo/components/style/gecko/snapshot.rs b/servo/components/style/gecko/snapshot.rs
new file mode 100644
index 0000000000..2ff04406ac
--- /dev/null
+++ b/servo/components/style/gecko/snapshot.rs
@@ -0,0 +1,174 @@
+/* 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/. */
+
+//! A gecko snapshot, that stores the element attributes and state before they
+//! change in order to properly calculate restyle hints.
+
+use crate::dom::TElement;
+use crate::gecko::snapshot_helpers;
+use crate::gecko::wrapper::GeckoElement;
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs::ServoElementSnapshot;
+use crate::gecko_bindings::structs::ServoElementSnapshotFlags as Flags;
+use crate::gecko_bindings::structs::ServoElementSnapshotTable;
+use crate::invalidation::element::element_wrapper::ElementSnapshot;
+use crate::selector_parser::AttrValue;
+use crate::string_cache::{Atom, Namespace};
+use crate::values::{AtomIdent, AtomString};
+use crate::LocalName;
+use crate::WeakAtom;
+use dom::ElementState;
+use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
+
+/// A snapshot of a Gecko element.
+pub type GeckoElementSnapshot = ServoElementSnapshot;
+
+/// A map from elements to snapshots for Gecko's style back-end.
+pub type SnapshotMap = ServoElementSnapshotTable;
+
+impl SnapshotMap {
+ /// Gets the snapshot for this element, if any.
+ ///
+ /// FIXME(emilio): The transmute() business we do here is kind of nasty, but
+ /// it's a consequence of the map being a OpaqueNode -> Snapshot table in
+ /// Servo and an Element -> Snapshot table in Gecko.
+ ///
+ /// We should be able to make this a more type-safe with type annotations by
+ /// making SnapshotMap a trait and moving the implementations outside, but
+ /// that's a pain because it implies parameterizing SharedStyleContext.
+ pub fn get<E: TElement>(&self, element: &E) -> Option<&GeckoElementSnapshot> {
+ debug_assert!(element.has_snapshot());
+
+ unsafe {
+ let element = ::std::mem::transmute::<&E, &GeckoElement>(element);
+ bindings::Gecko_GetElementSnapshot(self, element.0).as_ref()
+ }
+ }
+}
+
+impl GeckoElementSnapshot {
+ #[inline]
+ fn has_any(&self, flags: Flags) -> bool {
+ (self.mContains as u8 & flags as u8) != 0
+ }
+
+ /// Returns true if the snapshot has stored state for pseudo-classes
+ /// that depend on things other than `ElementState`.
+ #[inline]
+ pub fn has_other_pseudo_class_state(&self) -> bool {
+ self.has_any(Flags::OtherPseudoClassState)
+ }
+
+ /// Returns true if the snapshot recorded an id change.
+ #[inline]
+ pub fn id_changed(&self) -> bool {
+ self.mIdAttributeChanged()
+ }
+
+ /// Returns true if the snapshot recorded a class attribute change.
+ #[inline]
+ pub fn class_changed(&self) -> bool {
+ self.mClassAttributeChanged()
+ }
+
+ /// Executes the callback once for each attribute that changed.
+ #[inline]
+ pub fn each_attr_changed<F>(&self, mut callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ for attr in self.mChangedAttrNames.iter() {
+ unsafe { AtomIdent::with(attr.mRawPtr, &mut callback) }
+ }
+ }
+
+ /// selectors::Element::attr_matches
+ pub fn attr_matches(
+ &self,
+ ns: &NamespaceConstraint<&Namespace>,
+ local_name: &LocalName,
+ operation: &AttrSelectorOperation<&AttrValue>,
+ ) -> bool {
+ snapshot_helpers::attr_matches(&self.mAttrs, ns, local_name, operation)
+ }
+}
+
+impl ElementSnapshot for GeckoElementSnapshot {
+ fn debug_list_attributes(&self) -> String {
+ use nsstring::nsCString;
+ let mut string = nsCString::new();
+ unsafe {
+ bindings::Gecko_Snapshot_DebugListAttributes(self, &mut string);
+ }
+ String::from_utf8_lossy(&*string).into_owned()
+ }
+
+ fn state(&self) -> Option<ElementState> {
+ if self.has_any(Flags::State) {
+ Some(ElementState::from_bits_retain(self.mState))
+ } else {
+ None
+ }
+ }
+
+ #[inline]
+ fn has_attrs(&self) -> bool {
+ self.has_any(Flags::Attributes)
+ }
+
+ #[inline]
+ fn id_attr(&self) -> Option<&WeakAtom> {
+ if !self.has_any(Flags::Id) {
+ return None;
+ }
+
+ snapshot_helpers::get_id(&*self.mAttrs)
+ }
+
+ #[inline]
+ fn is_part(&self, name: &AtomIdent) -> bool {
+ let attr = match snapshot_helpers::find_attr(&*self.mAttrs, &atom!("part")) {
+ Some(attr) => attr,
+ None => return false,
+ };
+
+ snapshot_helpers::has_class_or_part(name, CaseSensitivity::CaseSensitive, attr)
+ }
+
+ #[inline]
+ fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> {
+ snapshot_helpers::imported_part(&*self.mAttrs, name)
+ }
+
+ #[inline]
+ fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
+ if !self.has_any(Flags::MaybeClass) {
+ return false;
+ }
+
+ snapshot_helpers::has_class_or_part(name, case_sensitivity, &self.mClass)
+ }
+
+ #[inline]
+ fn each_class<F>(&self, callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ if !self.has_any(Flags::MaybeClass) {
+ return;
+ }
+
+ snapshot_helpers::each_class_or_part(&self.mClass, callback)
+ }
+
+ #[inline]
+ fn lang_attr(&self) -> Option<AtomString> {
+ let ptr = unsafe { bindings::Gecko_SnapshotLangValue(self) };
+ if ptr.is_null() {
+ None
+ } else {
+ Some(AtomString(unsafe { Atom::from_addrefed(ptr) }))
+ }
+ }
+}
diff --git a/servo/components/style/gecko/snapshot_helpers.rs b/servo/components/style/gecko/snapshot_helpers.rs
new file mode 100644
index 0000000000..ab2d08eaf8
--- /dev/null
+++ b/servo/components/style/gecko/snapshot_helpers.rs
@@ -0,0 +1,316 @@
+/* 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/. */
+
+//! Element an snapshot common logic.
+
+use crate::dom::TElement;
+use crate::gecko::wrapper::namespace_id_to_atom;
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs::{self, nsAtom};
+use crate::invalidation::element::element_wrapper::ElementSnapshot;
+use crate::selector_parser::{AttrValue, SnapshotMap};
+use crate::string_cache::WeakAtom;
+use crate::values::AtomIdent;
+use crate::{Atom, CaseSensitivityExt, LocalName, Namespace};
+use selectors::attr::{
+ AttrSelectorOperation, AttrSelectorOperator, CaseSensitivity, NamespaceConstraint,
+};
+use smallvec::SmallVec;
+
+/// A function that, given an element of type `T`, allows you to get a single
+/// class or a class list.
+enum Class<'a> {
+ None,
+ One(*const nsAtom),
+ More(&'a [structs::RefPtr<nsAtom>]),
+}
+
+#[inline(always)]
+fn base_type(attr: &structs::nsAttrValue) -> structs::nsAttrValue_ValueBaseType {
+ (attr.mBits & structs::NS_ATTRVALUE_BASETYPE_MASK) as structs::nsAttrValue_ValueBaseType
+}
+
+#[inline(always)]
+unsafe fn ptr<T>(attr: &structs::nsAttrValue) -> *const T {
+ (attr.mBits & !structs::NS_ATTRVALUE_BASETYPE_MASK) as *const T
+}
+
+#[inline(always)]
+unsafe fn get_class_or_part_from_attr(attr: &structs::nsAttrValue) -> Class {
+ debug_assert!(bindings::Gecko_AssertClassAttrValueIsSane(attr));
+ let base_type = base_type(attr);
+ if base_type == structs::nsAttrValue_ValueBaseType_eAtomBase {
+ return Class::One(ptr::<nsAtom>(attr));
+ }
+ if base_type == structs::nsAttrValue_ValueBaseType_eOtherBase {
+ let container = ptr::<structs::MiscContainer>(attr);
+ debug_assert_eq!(
+ (*container).mType,
+ structs::nsAttrValue_ValueType_eAtomArray
+ );
+ // NOTE: Bindgen doesn't deal with AutoTArray, so cast it below.
+ let attr_array: *const _ = *(*container)
+ .__bindgen_anon_1
+ .mValue
+ .as_ref()
+ .__bindgen_anon_1
+ .mAtomArray
+ .as_ref();
+ let array =
+ (*attr_array).mArray.as_ptr() as *const structs::nsTArray<structs::RefPtr<nsAtom>>;
+ return Class::More(&**array);
+ }
+ debug_assert_eq!(base_type, structs::nsAttrValue_ValueBaseType_eStringBase);
+ Class::None
+}
+
+#[inline(always)]
+unsafe fn get_id_from_attr(attr: &structs::nsAttrValue) -> &WeakAtom {
+ debug_assert_eq!(
+ base_type(attr),
+ structs::nsAttrValue_ValueBaseType_eAtomBase
+ );
+ WeakAtom::new(ptr::<nsAtom>(attr))
+}
+
+impl structs::nsAttrName {
+ #[inline]
+ fn is_nodeinfo(&self) -> bool {
+ self.mBits & 1 != 0
+ }
+
+ #[inline]
+ unsafe fn as_nodeinfo(&self) -> &structs::NodeInfo {
+ debug_assert!(self.is_nodeinfo());
+ &*((self.mBits & !1) as *const structs::NodeInfo)
+ }
+
+ #[inline]
+ fn namespace_id(&self) -> i32 {
+ if !self.is_nodeinfo() {
+ return structs::kNameSpaceID_None;
+ }
+ unsafe { self.as_nodeinfo() }.mInner.mNamespaceID
+ }
+
+ /// Returns the attribute name as an atom pointer.
+ #[inline]
+ pub fn name(&self) -> *const nsAtom {
+ if self.is_nodeinfo() {
+ unsafe { self.as_nodeinfo() }.mInner.mName
+ } else {
+ self.mBits as *const nsAtom
+ }
+ }
+}
+
+/// Find an attribute value with a given name and no namespace.
+#[inline(always)]
+pub fn find_attr<'a>(
+ attrs: &'a [structs::AttrArray_InternalAttr],
+ name: &Atom,
+) -> Option<&'a structs::nsAttrValue> {
+ attrs
+ .iter()
+ .find(|attr| attr.mName.mBits == name.as_ptr() as usize)
+ .map(|attr| &attr.mValue)
+}
+
+/// Finds the id attribute from a list of attributes.
+#[inline(always)]
+pub fn get_id(attrs: &[structs::AttrArray_InternalAttr]) -> Option<&WeakAtom> {
+ Some(unsafe { get_id_from_attr(find_attr(attrs, &atom!("id"))?) })
+}
+
+#[inline(always)]
+pub(super) fn each_exported_part(
+ attrs: &[structs::AttrArray_InternalAttr],
+ name: &AtomIdent,
+ mut callback: impl FnMut(&AtomIdent),
+) {
+ let attr = match find_attr(attrs, &atom!("exportparts")) {
+ Some(attr) => attr,
+ None => return,
+ };
+ let mut length = 0;
+ let atoms = unsafe { bindings::Gecko_Element_ExportedParts(attr, name.as_ptr(), &mut length) };
+ if atoms.is_null() {
+ return;
+ }
+
+ unsafe {
+ for atom in std::slice::from_raw_parts(atoms, length) {
+ AtomIdent::with(*atom, &mut callback)
+ }
+ }
+}
+
+#[inline(always)]
+pub(super) fn imported_part(
+ attrs: &[structs::AttrArray_InternalAttr],
+ name: &AtomIdent,
+) -> Option<AtomIdent> {
+ let attr = find_attr(attrs, &atom!("exportparts"))?;
+ let atom = unsafe { bindings::Gecko_Element_ImportedPart(attr, name.as_ptr()) };
+ if atom.is_null() {
+ return None;
+ }
+ Some(AtomIdent(unsafe { Atom::from_raw(atom) }))
+}
+
+/// Given a class or part name, a case sensitivity, and an array of attributes,
+/// returns whether the attribute has that name.
+#[inline(always)]
+pub fn has_class_or_part(
+ name: &AtomIdent,
+ case_sensitivity: CaseSensitivity,
+ attr: &structs::nsAttrValue,
+) -> bool {
+ match unsafe { get_class_or_part_from_attr(attr) } {
+ Class::None => false,
+ Class::One(atom) => unsafe { case_sensitivity.eq_atom(name, WeakAtom::new(atom)) },
+ Class::More(atoms) => match case_sensitivity {
+ CaseSensitivity::CaseSensitive => {
+ let name_ptr = name.as_ptr();
+ atoms.iter().any(|atom| atom.mRawPtr == name_ptr)
+ },
+ CaseSensitivity::AsciiCaseInsensitive => unsafe {
+ atoms
+ .iter()
+ .any(|atom| WeakAtom::new(atom.mRawPtr).eq_ignore_ascii_case(name))
+ },
+ },
+ }
+}
+
+/// Given an item, a callback, and a getter, execute `callback` for each class
+/// or part name this `item` has.
+#[inline(always)]
+pub fn each_class_or_part<F>(attr: &structs::nsAttrValue, mut callback: F)
+where
+ F: FnMut(&AtomIdent),
+{
+ unsafe {
+ match get_class_or_part_from_attr(attr) {
+ Class::None => {},
+ Class::One(atom) => AtomIdent::with(atom, callback),
+ Class::More(atoms) => {
+ for atom in atoms {
+ AtomIdent::with(atom.mRawPtr, &mut callback)
+ }
+ },
+ }
+ }
+}
+
+/// Returns a list of classes that were either added to or removed from the
+/// element since the snapshot.
+pub fn classes_changed<E: TElement>(element: &E, snapshots: &SnapshotMap) -> SmallVec<[Atom; 8]> {
+ debug_assert!(element.has_snapshot(), "Why bothering?");
+ let snapshot = snapshots.get(element).expect("has_snapshot lied");
+ if !snapshot.class_changed() {
+ return SmallVec::new();
+ }
+
+ let mut classes_changed = SmallVec::<[Atom; 8]>::new();
+ snapshot.each_class(|c| {
+ if !element.has_class(c, CaseSensitivity::CaseSensitive) {
+ classes_changed.push(c.0.clone());
+ }
+ });
+ element.each_class(|c| {
+ if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) {
+ classes_changed.push(c.0.clone());
+ }
+ });
+
+ classes_changed
+}
+
+/// Returns whether a given attribute selector matches given the internal attrs.
+#[inline(always)]
+pub(crate) fn attr_matches(
+ attrs: &[structs::AttrArray_InternalAttr],
+ ns: &NamespaceConstraint<&Namespace>,
+ local_name: &LocalName,
+ operation: &AttrSelectorOperation<&AttrValue>,
+) -> bool {
+ let name_ptr = local_name.as_ptr();
+ for attr in attrs {
+ if attr.mName.name() != name_ptr {
+ continue;
+ }
+
+ if attr_matches_checked_name(attr, ns, operation) {
+ return true;
+ }
+
+ // The name matched but the value or namespace didn't. The only reason to check the other
+ // attributes now would be to find one with the same name but a different namespace.
+ if *ns != NamespaceConstraint::Any {
+ // We don't want to look for other namespaces, so we're done.
+ return false;
+ }
+ }
+ false
+}
+
+/// Returns whether a given attribute selector matches given a single attribute,
+/// for the case where the caller has already found an attribute with the right name.
+fn attr_matches_checked_name(
+ attr: &structs::AttrArray_InternalAttr,
+ ns: &NamespaceConstraint<&Namespace>,
+ operation: &AttrSelectorOperation<&AttrValue>,
+) -> bool {
+ let ns_matches = match *ns {
+ NamespaceConstraint::Any => true,
+ NamespaceConstraint::Specific(ns) => {
+ if *ns == ns!() {
+ !attr.mName.is_nodeinfo()
+ } else {
+ ns.as_ptr() == unsafe { namespace_id_to_atom(attr.mName.namespace_id()) }
+ }
+ },
+ };
+
+ if !ns_matches {
+ return false;
+ }
+
+ let (operator, case_sensitivity, value) = match *operation {
+ AttrSelectorOperation::Exists => return true,
+ AttrSelectorOperation::WithValue {
+ operator,
+ case_sensitivity,
+ value,
+ } => (operator, case_sensitivity, value),
+ };
+ let ignore_case = match case_sensitivity {
+ CaseSensitivity::CaseSensitive => false,
+ CaseSensitivity::AsciiCaseInsensitive => true,
+ };
+ let value = value.as_ptr();
+ unsafe {
+ match operator {
+ AttrSelectorOperator::Equal => {
+ bindings::Gecko_AttrEquals(&attr.mValue, value, ignore_case)
+ },
+ AttrSelectorOperator::Includes => {
+ bindings::Gecko_AttrIncludes(&attr.mValue, value, ignore_case)
+ },
+ AttrSelectorOperator::DashMatch => {
+ bindings::Gecko_AttrDashEquals(&attr.mValue, value, ignore_case)
+ },
+ AttrSelectorOperator::Prefix => {
+ bindings::Gecko_AttrHasPrefix(&attr.mValue, value, ignore_case)
+ },
+ AttrSelectorOperator::Suffix => {
+ bindings::Gecko_AttrHasSuffix(&attr.mValue, value, ignore_case)
+ },
+ AttrSelectorOperator::Substring => {
+ bindings::Gecko_AttrHasSubstring(&attr.mValue, value, ignore_case)
+ },
+ }
+ }
+}
diff --git a/servo/components/style/gecko/traversal.rs b/servo/components/style/gecko/traversal.rs
new file mode 100644
index 0000000000..71d1a2f949
--- /dev/null
+++ b/servo/components/style/gecko/traversal.rs
@@ -0,0 +1,53 @@
+/* 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/. */
+
+//! Gecko-specific bits for the styling DOM traversal.
+
+use crate::context::{SharedStyleContext, StyleContext};
+use crate::dom::{TElement, TNode};
+use crate::gecko::wrapper::{GeckoElement, GeckoNode};
+use crate::traversal::{recalc_style_at, DomTraversal, PerLevelTraversalData};
+
+/// This is the simple struct that Gecko uses to encapsulate a DOM traversal for
+/// styling.
+pub struct RecalcStyleOnly<'a> {
+ shared: SharedStyleContext<'a>,
+}
+
+impl<'a> RecalcStyleOnly<'a> {
+ /// Create a `RecalcStyleOnly` traversal from a `SharedStyleContext`.
+ pub fn new(shared: SharedStyleContext<'a>) -> Self {
+ RecalcStyleOnly { shared: shared }
+ }
+}
+
+impl<'recalc, 'le> DomTraversal<GeckoElement<'le>> for RecalcStyleOnly<'recalc> {
+ fn process_preorder<F>(
+ &self,
+ traversal_data: &PerLevelTraversalData,
+ context: &mut StyleContext<GeckoElement<'le>>,
+ node: GeckoNode<'le>,
+ note_child: F,
+ ) where
+ F: FnMut(GeckoNode<'le>),
+ {
+ if let Some(el) = node.as_element() {
+ let mut data = unsafe { el.ensure_data() };
+ recalc_style_at(self, traversal_data, context, el, &mut data, note_child);
+ }
+ }
+
+ fn process_postorder(&self, _: &mut StyleContext<GeckoElement<'le>>, _: GeckoNode<'le>) {
+ unreachable!();
+ }
+
+ /// We don't use the post-order traversal for anything.
+ fn needs_postorder_traversal() -> bool {
+ false
+ }
+
+ fn shared_context(&self) -> &SharedStyleContext {
+ &self.shared
+ }
+}
diff --git a/servo/components/style/gecko/url.rs b/servo/components/style/gecko/url.rs
new file mode 100644
index 0000000000..7fe32acc20
--- /dev/null
+++ b/servo/components/style/gecko/url.rs
@@ -0,0 +1,384 @@
+/* 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/. */
+
+//! Common handling for the specified value CSS url() values.
+
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs;
+use crate::parser::{Parse, ParserContext};
+use crate::stylesheets::{CorsMode, UrlExtraData};
+use crate::values::computed::{Context, ToComputedValue};
+use cssparser::Parser;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use nsstring::nsCString;
+use servo_arc::Arc;
+use std::collections::HashMap;
+use std::fmt::{self, Write};
+use std::mem::ManuallyDrop;
+use std::sync::RwLock;
+use style_traits::{CssWriter, ParseError, ToCss};
+use to_shmem::{self, SharedMemoryBuilder, ToShmem};
+
+/// A CSS url() value for gecko.
+#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+#[css(function = "url")]
+#[repr(C)]
+pub struct CssUrl(pub Arc<CssUrlData>);
+
+/// Data shared between CssUrls.
+///
+/// cbindgen:derive-eq=false
+/// cbindgen:derive-neq=false
+#[derive(Debug, SpecifiedValueInfo, ToCss, ToShmem)]
+#[repr(C)]
+pub struct CssUrlData {
+ /// The URL in unresolved string form.
+ serialization: crate::OwnedStr,
+
+ /// The URL extra data.
+ #[css(skip)]
+ pub extra_data: UrlExtraData,
+
+ /// The CORS mode that will be used for the load.
+ #[css(skip)]
+ cors_mode: CorsMode,
+
+ /// Data to trigger a load from Gecko. This is mutable in C++.
+ ///
+ /// TODO(emilio): Maybe we can eagerly resolve URLs and make this immutable?
+ #[css(skip)]
+ load_data: LoadDataSource,
+}
+
+impl PartialEq for CssUrlData {
+ fn eq(&self, other: &Self) -> bool {
+ self.serialization == other.serialization &&
+ self.extra_data == other.extra_data &&
+ self.cors_mode == other.cors_mode
+ }
+}
+
+impl CssUrl {
+ fn parse_with_cors_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ ) -> Result<Self, ParseError<'i>> {
+ let url = input.expect_url()?;
+ Ok(Self::parse_from_string(
+ url.as_ref().to_owned(),
+ context,
+ cors_mode,
+ ))
+ }
+
+ /// Parse a URL from a string value that is a valid CSS token for a URL.
+ pub fn parse_from_string(url: String, context: &ParserContext, cors_mode: CorsMode) -> Self {
+ CssUrl(Arc::new(CssUrlData {
+ serialization: url.into(),
+ extra_data: context.url_data.clone(),
+ cors_mode,
+ load_data: LoadDataSource::Owned(LoadData::default()),
+ }))
+ }
+
+ /// Returns true if the URL is definitely invalid. We don't eagerly resolve
+ /// URLs in gecko, so we just return false here.
+ /// use its |resolved| status.
+ pub fn is_invalid(&self) -> bool {
+ false
+ }
+
+ /// Returns true if this URL looks like a fragment.
+ /// See https://drafts.csswg.org/css-values/#local-urls
+ #[inline]
+ pub fn is_fragment(&self) -> bool {
+ self.0.is_fragment()
+ }
+
+ /// Return the unresolved url as string, or the empty string if it's
+ /// invalid.
+ #[inline]
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+}
+
+impl CssUrlData {
+ /// Returns true if this URL looks like a fragment.
+ /// See https://drafts.csswg.org/css-values/#local-urls
+ pub fn is_fragment(&self) -> bool {
+ self.as_str()
+ .as_bytes()
+ .iter()
+ .next()
+ .map_or(false, |b| *b == b'#')
+ }
+
+ /// Return the unresolved url as string, or the empty string if it's
+ /// invalid.
+ pub fn as_str(&self) -> &str {
+ &*self.serialization
+ }
+}
+
+impl Parse for CssUrl {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_cors_mode(context, input, CorsMode::None)
+ }
+}
+
+impl Eq for CssUrl {}
+
+impl MallocSizeOf for CssUrl {
+ fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
+ // XXX: measure `serialization` once bug 1397971 lands
+
+ // We ignore `extra_data`, because RefPtr is tricky, and there aren't
+ // many of them in practise (sharing is common).
+
+ 0
+ }
+}
+
+/// A key type for LOAD_DATA_TABLE.
+#[derive(Eq, Hash, PartialEq)]
+struct LoadDataKey(*const LoadDataSource);
+
+unsafe impl Sync for LoadDataKey {}
+unsafe impl Send for LoadDataKey {}
+
+bitflags! {
+ /// Various bits of mutable state that are kept for image loads.
+ #[derive(Debug)]
+ #[repr(C)]
+ pub struct LoadDataFlags: u8 {
+ /// Whether we tried to resolve the uri at least once.
+ const TRIED_TO_RESOLVE_URI = 1 << 0;
+ /// Whether we tried to resolve the image at least once.
+ const TRIED_TO_RESOLVE_IMAGE = 1 << 1;
+ }
+}
+
+/// This is usable and movable from multiple threads just fine, as long as it's
+/// not cloned (it is not clonable), and the methods that mutate it run only on
+/// the main thread (when all the other threads we care about are paused).
+unsafe impl Sync for LoadData {}
+unsafe impl Send for LoadData {}
+
+/// The load data for a given URL. This is mutable from C++, and shouldn't be
+/// accessed from rust for anything.
+#[repr(C)]
+#[derive(Debug)]
+pub struct LoadData {
+ /// A strong reference to the imgRequestProxy, if any, that should be
+ /// released on drop.
+ ///
+ /// These are raw pointers because they are not safe to reference-count off
+ /// the main thread.
+ resolved_image: *mut structs::imgRequestProxy,
+ /// A strong reference to the resolved URI of this image.
+ resolved_uri: *mut structs::nsIURI,
+ /// A few flags that are set when resolving the image or such.
+ flags: LoadDataFlags,
+}
+
+impl Drop for LoadData {
+ fn drop(&mut self) {
+ unsafe { bindings::Gecko_LoadData_Drop(self) }
+ }
+}
+
+impl Default for LoadData {
+ fn default() -> Self {
+ Self {
+ resolved_image: std::ptr::null_mut(),
+ resolved_uri: std::ptr::null_mut(),
+ flags: LoadDataFlags::empty(),
+ }
+ }
+}
+
+/// The data for a load, or a lazy-loaded, static member that will be stored in
+/// LOAD_DATA_TABLE, keyed by the memory location of this object, which is
+/// always in the heap because it's inside the CssUrlData object.
+///
+/// This type is meant not to be used from C++ so we don't derive helper
+/// methods.
+///
+/// cbindgen:derive-helper-methods=false
+#[derive(Debug)]
+#[repr(u8, C)]
+pub enum LoadDataSource {
+ /// An owned copy of the load data.
+ Owned(LoadData),
+ /// A lazily-resolved copy of it.
+ Lazy,
+}
+
+impl LoadDataSource {
+ /// Gets the load data associated with the source.
+ ///
+ /// This relies on the source on being in a stable location if lazy.
+ #[inline]
+ pub unsafe fn get(&self) -> *const LoadData {
+ match *self {
+ LoadDataSource::Owned(ref d) => return d,
+ LoadDataSource::Lazy => {},
+ }
+
+ let key = LoadDataKey(self);
+
+ {
+ let guard = LOAD_DATA_TABLE.read().unwrap();
+ if let Some(r) = guard.get(&key) {
+ return &**r;
+ }
+ }
+ let mut guard = LOAD_DATA_TABLE.write().unwrap();
+ let r = guard.entry(key).or_insert_with(Default::default);
+ &**r
+ }
+}
+
+impl ToShmem for LoadDataSource {
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ Ok(ManuallyDrop::new(match self {
+ LoadDataSource::Owned(..) => LoadDataSource::Lazy,
+ LoadDataSource::Lazy => LoadDataSource::Lazy,
+ }))
+ }
+}
+
+/// A specified non-image `url()` value.
+pub type SpecifiedUrl = CssUrl;
+
+/// Clears LOAD_DATA_TABLE. Entries in this table, which are for specified URL
+/// values that come from shared memory style sheets, would otherwise persist
+/// until the end of the process and be reported as leaks.
+pub fn shutdown() {
+ LOAD_DATA_TABLE.write().unwrap().clear();
+}
+
+impl ToComputedValue for SpecifiedUrl {
+ type ComputedValue = ComputedUrl;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ ComputedUrl(self.clone())
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed.0.clone()
+ }
+}
+
+/// A specified image `url()` value.
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct SpecifiedImageUrl(pub SpecifiedUrl);
+
+impl SpecifiedImageUrl {
+ /// Parse a URL from a string value that is a valid CSS token for a URL.
+ pub fn parse_from_string(url: String, context: &ParserContext, cors_mode: CorsMode) -> Self {
+ SpecifiedImageUrl(SpecifiedUrl::parse_from_string(url, context, cors_mode))
+ }
+
+ /// Provides an alternate method for parsing that associates the URL
+ /// with anonymous CORS headers.
+ pub fn parse_with_cors_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(SpecifiedImageUrl(SpecifiedUrl::parse_with_cors_mode(
+ context, input, cors_mode,
+ )?))
+ }
+}
+
+impl Parse for SpecifiedImageUrl {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ SpecifiedUrl::parse(context, input).map(SpecifiedImageUrl)
+ }
+}
+
+impl ToComputedValue for SpecifiedImageUrl {
+ type ComputedValue = ComputedImageUrl;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ ComputedImageUrl(self.0.to_computed_value(context))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ SpecifiedImageUrl(ToComputedValue::from_computed_value(&computed.0))
+ }
+}
+
+/// The computed value of a CSS non-image `url()`.
+///
+/// The only difference between specified and computed URLs is the
+/// serialization.
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq)]
+#[repr(C)]
+pub struct ComputedUrl(pub SpecifiedUrl);
+
+impl ComputedUrl {
+ fn serialize_with<W>(
+ &self,
+ function: unsafe extern "C" fn(*const Self, *mut nsCString),
+ dest: &mut CssWriter<W>,
+ ) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("url(")?;
+ unsafe {
+ let mut string = nsCString::new();
+ function(self, &mut string);
+ string.as_str_unchecked().to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl ToCss for ComputedUrl {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.serialize_with(bindings::Gecko_GetComputedURLSpec, dest)
+ }
+}
+
+/// The computed value of a CSS image `url()`.
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq)]
+#[repr(transparent)]
+pub struct ComputedImageUrl(pub ComputedUrl);
+
+impl ToCss for ComputedImageUrl {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0
+ .serialize_with(bindings::Gecko_GetComputedImageURLSpec, dest)
+ }
+}
+
+lazy_static! {
+ /// A table mapping CssUrlData objects to their lazily created LoadData
+ /// objects.
+ static ref LOAD_DATA_TABLE: RwLock<HashMap<LoadDataKey, Box<LoadData>>> = {
+ Default::default()
+ };
+}
diff --git a/servo/components/style/gecko/values.rs b/servo/components/style/gecko/values.rs
new file mode 100644
index 0000000000..d04c73c70f
--- /dev/null
+++ b/servo/components/style/gecko/values.rs
@@ -0,0 +1,77 @@
+/* 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/. */
+
+#![allow(unsafe_code)]
+
+//! Different kind of helpers to interact with Gecko values.
+
+use crate::color::{AbsoluteColor, ColorSpace};
+use crate::counter_style::{Symbol, Symbols};
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs::CounterStylePtr;
+use crate::values::generics::CounterStyle;
+use crate::values::Either;
+use crate::Atom;
+
+/// Convert a color value to `nscolor`.
+pub fn convert_absolute_color_to_nscolor(color: &AbsoluteColor) -> u32 {
+ let srgb = color.to_color_space(ColorSpace::Srgb);
+ u32::from_le_bytes([
+ (srgb.components.0 * 255.0).round() as u8,
+ (srgb.components.1 * 255.0).round() as u8,
+ (srgb.components.2 * 255.0).round() as u8,
+ (srgb.alpha * 255.0).round() as u8,
+ ])
+}
+
+/// Convert a given `nscolor` to a Servo AbsoluteColor value.
+pub fn convert_nscolor_to_absolute_color(color: u32) -> AbsoluteColor {
+ let [r, g, b, a] = color.to_le_bytes();
+ AbsoluteColor::srgb_legacy(r, g, b, a as f32 / 255.0)
+}
+
+#[test]
+fn convert_ns_color_to_absolute_color_should_be_in_legacy_syntax() {
+ use crate::color::ColorFlags;
+
+ let result = convert_nscolor_to_absolute_color(0x336699CC);
+ assert!(result.flags.contains(ColorFlags::IS_LEGACY_SRGB));
+
+ assert!(result.is_legacy_syntax());
+}
+
+impl CounterStyle {
+ /// Convert this counter style to a Gecko CounterStylePtr.
+ #[inline]
+ pub fn to_gecko_value(&self, gecko_value: &mut CounterStylePtr) {
+ unsafe { bindings::Gecko_CounterStyle_ToPtr(self, gecko_value) }
+ }
+
+ /// Convert Gecko CounterStylePtr to CounterStyle or String.
+ pub fn from_gecko_value(gecko_value: &CounterStylePtr) -> Either<Self, String> {
+ use crate::values::CustomIdent;
+
+ let name = unsafe { bindings::Gecko_CounterStyle_GetName(gecko_value) };
+ if !name.is_null() {
+ let name = unsafe { Atom::from_raw(name) };
+ debug_assert_ne!(name, atom!("none"));
+ Either::First(CounterStyle::Name(CustomIdent(name)))
+ } else {
+ let anonymous =
+ unsafe { bindings::Gecko_CounterStyle_GetAnonymous(gecko_value).as_ref() }.unwrap();
+ let symbols = &anonymous.mSymbols;
+ if anonymous.mSingleString {
+ debug_assert_eq!(symbols.len(), 1);
+ Either::Second(symbols[0].to_string())
+ } else {
+ let symbol_type = anonymous.mSymbolsType;
+ let symbols = symbols
+ .iter()
+ .map(|gecko_symbol| Symbol::String(gecko_symbol.to_string().into()))
+ .collect();
+ Either::First(CounterStyle::Symbols(symbol_type, Symbols(symbols)))
+ }
+ }
+ }
+}
diff --git a/servo/components/style/gecko/wrapper.rs b/servo/components/style/gecko/wrapper.rs
new file mode 100644
index 0000000000..61352ef9c0
--- /dev/null
+++ b/servo/components/style/gecko/wrapper.rs
@@ -0,0 +1,2211 @@
+/* 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/. */
+
+#![allow(unsafe_code)]
+
+//! Wrapper definitions on top of Gecko types in order to be used in the style
+//! system.
+//!
+//! This really follows the Servo pattern in
+//! `components/script/layout_wrapper.rs`.
+//!
+//! This theoretically should live in its own crate, but now it lives in the
+//! style system it's kind of pointless in the Stylo case, and only Servo forces
+//! the separation between the style system implementation and everything else.
+
+use crate::applicable_declarations::ApplicableDeclarationBlock;
+use crate::bloom::each_relevant_element_hash;
+use crate::context::{PostAnimationTasks, QuirksMode, SharedStyleContext, UpdateAnimationsTasks};
+use crate::data::ElementData;
+use crate::dom::{LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot};
+use crate::gecko::selector_parser::{CustomState, NonTSPseudoClass, PseudoElement, SelectorImpl};
+use crate::gecko::snapshot_helpers;
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::bindings::Gecko_ElementHasAnimations;
+use crate::gecko_bindings::bindings::Gecko_ElementHasCSSAnimations;
+use crate::gecko_bindings::bindings::Gecko_ElementHasCSSTransitions;
+use crate::gecko_bindings::bindings::Gecko_ElementState;
+use crate::gecko_bindings::bindings::Gecko_GetActiveLinkAttrDeclarationBlock;
+use crate::gecko_bindings::bindings::Gecko_GetAnimationEffectCount;
+use crate::gecko_bindings::bindings::Gecko_GetAnimationRule;
+use crate::gecko_bindings::bindings::Gecko_GetExtraContentStyleDeclarations;
+use crate::gecko_bindings::bindings::Gecko_GetHTMLPresentationAttrDeclarationBlock;
+use crate::gecko_bindings::bindings::Gecko_GetStyleAttrDeclarationBlock;
+use crate::gecko_bindings::bindings::Gecko_GetUnvisitedLinkAttrDeclarationBlock;
+use crate::gecko_bindings::bindings::Gecko_GetVisitedLinkAttrDeclarationBlock;
+use crate::gecko_bindings::bindings::Gecko_IsSignificantChild;
+use crate::gecko_bindings::bindings::Gecko_MatchLang;
+use crate::gecko_bindings::bindings::Gecko_UnsetDirtyStyleAttr;
+use crate::gecko_bindings::bindings::Gecko_UpdateAnimations;
+use crate::gecko_bindings::structs;
+use crate::gecko_bindings::structs::nsChangeHint;
+use crate::gecko_bindings::structs::EffectCompositor_CascadeLevel as CascadeLevel;
+use crate::gecko_bindings::structs::ELEMENT_HANDLED_SNAPSHOT;
+use crate::gecko_bindings::structs::ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO;
+use crate::gecko_bindings::structs::ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO;
+use crate::gecko_bindings::structs::ELEMENT_HAS_SNAPSHOT;
+use crate::gecko_bindings::structs::NODE_DESCENDANTS_NEED_FRAMES;
+use crate::gecko_bindings::structs::NODE_NEEDS_FRAME;
+use crate::gecko_bindings::structs::{nsAtom, nsIContent, nsINode_BooleanFlag};
+use crate::gecko_bindings::structs::{nsINode as RawGeckoNode, Element as RawGeckoElement};
+use crate::global_style_data::GLOBAL_STYLE_DATA;
+use crate::invalidation::element::restyle_hints::RestyleHint;
+use crate::media_queries::Device;
+use crate::properties::{
+ animated_properties::{AnimationValue, AnimationValueMap},
+ ComputedValues, Importance, OwnedPropertyDeclarationId, PropertyDeclaration,
+ PropertyDeclarationBlock, PropertyDeclarationId, PropertyDeclarationIdSet,
+};
+use crate::rule_tree::CascadeLevel as ServoCascadeLevel;
+use crate::selector_parser::{AttrValue, Lang};
+use crate::shared_lock::{Locked, SharedRwLock};
+use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace};
+use crate::stylist::CascadeData;
+use crate::values::computed::Display;
+use crate::values::{AtomIdent, AtomString};
+use crate::CaseSensitivityExt;
+use crate::LocalName;
+use app_units::Au;
+use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
+use dom::{DocumentState, ElementState};
+use euclid::default::Size2D;
+use fxhash::FxHashMap;
+use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
+use selectors::bloom::{BloomFilter, BLOOM_HASH_MASK};
+use selectors::matching::VisitedHandlingMode;
+use selectors::matching::{ElementSelectorFlags, MatchingContext};
+use selectors::sink::Push;
+use selectors::{Element, OpaqueElement};
+use servo_arc::{Arc, ArcBorrow};
+use std::cell::Cell;
+use std::fmt;
+use std::hash::{Hash, Hasher};
+use std::mem;
+use std::ptr;
+use std::sync::atomic::{AtomicU32, Ordering};
+
+#[inline]
+fn elements_with_id<'a, 'le>(
+ array: *const structs::nsTArray<*mut RawGeckoElement>,
+) -> &'a [GeckoElement<'le>] {
+ unsafe {
+ if array.is_null() {
+ return &[];
+ }
+
+ let elements: &[*mut RawGeckoElement] = &**array;
+
+ // NOTE(emilio): We rely on the in-memory representation of
+ // GeckoElement<'ld> and *mut RawGeckoElement being the same.
+ #[allow(dead_code)]
+ unsafe fn static_assert() {
+ mem::transmute::<*mut RawGeckoElement, GeckoElement<'static>>(0xbadc0de as *mut _);
+ }
+
+ mem::transmute(elements)
+ }
+}
+
+/// A simple wrapper over `Document`.
+#[derive(Clone, Copy)]
+pub struct GeckoDocument<'ld>(pub &'ld structs::Document);
+
+impl<'ld> TDocument for GeckoDocument<'ld> {
+ type ConcreteNode = GeckoNode<'ld>;
+
+ #[inline]
+ fn as_node(&self) -> Self::ConcreteNode {
+ GeckoNode(&self.0._base)
+ }
+
+ #[inline]
+ fn is_html_document(&self) -> bool {
+ self.0.mType == structs::Document_Type::eHTML
+ }
+
+ #[inline]
+ fn quirks_mode(&self) -> QuirksMode {
+ self.0.mCompatMode.into()
+ }
+
+ #[inline]
+ fn elements_with_id<'a>(&self, id: &AtomIdent) -> Result<&'a [GeckoElement<'ld>], ()>
+ where
+ Self: 'a,
+ {
+ Ok(elements_with_id(unsafe {
+ bindings::Gecko_Document_GetElementsWithId(self.0, id.as_ptr())
+ }))
+ }
+
+ fn shared_lock(&self) -> &SharedRwLock {
+ &GLOBAL_STYLE_DATA.shared_lock
+ }
+}
+
+/// A simple wrapper over `ShadowRoot`.
+#[derive(Clone, Copy)]
+pub struct GeckoShadowRoot<'lr>(pub &'lr structs::ShadowRoot);
+
+impl<'ln> fmt::Debug for GeckoShadowRoot<'ln> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ // TODO(emilio): Maybe print the host or something?
+ write!(f, "<shadow-root> ({:#x})", self.as_node().opaque().0)
+ }
+}
+
+impl<'lr> PartialEq for GeckoShadowRoot<'lr> {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.0 as *const _ == other.0 as *const _
+ }
+}
+
+impl<'lr> TShadowRoot for GeckoShadowRoot<'lr> {
+ type ConcreteNode = GeckoNode<'lr>;
+
+ #[inline]
+ fn as_node(&self) -> Self::ConcreteNode {
+ GeckoNode(&self.0._base._base._base._base)
+ }
+
+ #[inline]
+ fn host(&self) -> GeckoElement<'lr> {
+ GeckoElement(unsafe { &*self.0._base.mHost.mRawPtr })
+ }
+
+ #[inline]
+ fn style_data<'a>(&self) -> Option<&'a CascadeData>
+ where
+ Self: 'a,
+ {
+ let author_styles = unsafe { self.0.mServoStyles.mPtr.as_ref()? };
+ Some(&author_styles.data)
+ }
+
+ #[inline]
+ fn elements_with_id<'a>(&self, id: &AtomIdent) -> Result<&'a [GeckoElement<'lr>], ()>
+ where
+ Self: 'a,
+ {
+ Ok(elements_with_id(unsafe {
+ bindings::Gecko_ShadowRoot_GetElementsWithId(self.0, id.as_ptr())
+ }))
+ }
+
+ #[inline]
+ fn parts<'a>(&self) -> &[<Self::ConcreteNode as TNode>::ConcreteElement]
+ where
+ Self: 'a,
+ {
+ let slice: &[*const RawGeckoElement] = &*self.0.mParts;
+
+ #[allow(dead_code)]
+ unsafe fn static_assert() {
+ mem::transmute::<*const RawGeckoElement, GeckoElement<'static>>(0xbadc0de as *const _);
+ }
+
+ unsafe { mem::transmute(slice) }
+ }
+}
+
+/// A simple wrapper over a non-null Gecko node (`nsINode`) pointer.
+///
+/// Important: We don't currently refcount the DOM, because the wrapper lifetime
+/// magic guarantees that our LayoutFoo references won't outlive the root, and
+/// we don't mutate any of the references on the Gecko side during restyle.
+///
+/// We could implement refcounting if need be (at a potentially non-trivial
+/// performance cost) by implementing Drop and making LayoutFoo non-Copy.
+#[derive(Clone, Copy)]
+pub struct GeckoNode<'ln>(pub &'ln RawGeckoNode);
+
+impl<'ln> PartialEq for GeckoNode<'ln> {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.0 as *const _ == other.0 as *const _
+ }
+}
+
+impl<'ln> fmt::Debug for GeckoNode<'ln> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ if let Some(el) = self.as_element() {
+ return el.fmt(f);
+ }
+
+ if self.is_text_node() {
+ return write!(f, "<text node> ({:#x})", self.opaque().0);
+ }
+
+ if self.is_document() {
+ return write!(f, "<document> ({:#x})", self.opaque().0);
+ }
+
+ if let Some(sr) = self.as_shadow_root() {
+ return sr.fmt(f);
+ }
+
+ write!(f, "<non-text node> ({:#x})", self.opaque().0)
+ }
+}
+
+impl<'ln> GeckoNode<'ln> {
+ #[inline]
+ fn is_document(&self) -> bool {
+ // This is a DOM constant that isn't going to change.
+ const DOCUMENT_NODE: u16 = 9;
+ self.node_info().mInner.mNodeType == DOCUMENT_NODE
+ }
+
+ #[inline]
+ fn is_shadow_root(&self) -> bool {
+ self.is_in_shadow_tree() && self.parent_node().is_none()
+ }
+
+ #[inline]
+ fn from_content(content: &'ln nsIContent) -> Self {
+ GeckoNode(&content._base)
+ }
+
+ #[inline]
+ fn set_flags(&self, flags: u32) {
+ self.flags_atomic().fetch_or(flags, Ordering::Relaxed);
+ }
+
+ fn flags_atomic_for(flags: &Cell<u32>) -> &AtomicU32 {
+ const_assert!(std::mem::size_of::<Cell<u32>>() == std::mem::size_of::<AtomicU32>());
+ const_assert!(std::mem::align_of::<Cell<u32>>() == std::mem::align_of::<AtomicU32>());
+
+ // Rust doesn't provide standalone atomic functions like GCC/clang do
+ // (via the atomic intrinsics) or via std::atomic_ref, but it guarantees
+ // that the memory representation of u32 and AtomicU32 matches:
+ // https://doc.rust-lang.org/std/sync/atomic/struct.AtomicU32.html
+ unsafe { std::mem::transmute::<&Cell<u32>, &AtomicU32>(flags) }
+ }
+
+ #[inline]
+ fn flags_atomic(&self) -> &AtomicU32 {
+ Self::flags_atomic_for(&self.0._base._base_1.mFlags)
+ }
+
+ #[inline]
+ fn flags(&self) -> u32 {
+ self.flags_atomic().load(Ordering::Relaxed)
+ }
+
+ #[inline]
+ fn selector_flags_atomic(&self) -> &AtomicU32 {
+ Self::flags_atomic_for(&self.0.mSelectorFlags)
+ }
+
+ #[inline]
+ fn selector_flags(&self) -> u32 {
+ self.selector_flags_atomic().load(Ordering::Relaxed)
+ }
+
+ #[inline]
+ fn set_selector_flags(&self, flags: u32) {
+ self.selector_flags_atomic()
+ .fetch_or(flags, Ordering::Relaxed);
+ }
+
+ #[inline]
+ fn node_info(&self) -> &structs::NodeInfo {
+ debug_assert!(!self.0.mNodeInfo.mRawPtr.is_null());
+ unsafe { &*self.0.mNodeInfo.mRawPtr }
+ }
+
+ // These live in different locations depending on processor architecture.
+ #[cfg(target_pointer_width = "64")]
+ #[inline]
+ fn bool_flags(&self) -> u32 {
+ (self.0)._base._base_1.mBoolFlags
+ }
+
+ #[cfg(target_pointer_width = "32")]
+ #[inline]
+ fn bool_flags(&self) -> u32 {
+ (self.0).mBoolFlags
+ }
+
+ #[inline]
+ fn get_bool_flag(&self, flag: nsINode_BooleanFlag) -> bool {
+ self.bool_flags() & (1u32 << flag as u32) != 0
+ }
+
+ /// This logic is duplicate in Gecko's nsINode::IsInShadowTree().
+ #[inline]
+ fn is_in_shadow_tree(&self) -> bool {
+ use crate::gecko_bindings::structs::NODE_IS_IN_SHADOW_TREE;
+ self.flags() & NODE_IS_IN_SHADOW_TREE != 0
+ }
+
+ /// Returns true if we know for sure that `flattened_tree_parent` and `parent_node` return the
+ /// same thing.
+ ///
+ /// TODO(emilio): Measure and consider not doing this fast-path, it's only a function call and
+ /// from profiles it seems that keeping this fast path makes the compiler not inline
+ /// `flattened_tree_parent` as a whole, so we're not gaining much either.
+ #[inline]
+ fn flattened_tree_parent_is_parent(&self) -> bool {
+ use crate::gecko_bindings::structs::*;
+ let flags = self.flags();
+
+ let parent = match self.parent_node() {
+ Some(p) => p,
+ None => return true,
+ };
+
+ if parent.is_shadow_root() {
+ return false;
+ }
+
+ if let Some(parent) = parent.as_element() {
+ if flags & NODE_IS_NATIVE_ANONYMOUS_ROOT != 0 && parent.is_root() {
+ return false;
+ }
+ if parent.shadow_root().is_some() || parent.is_html_slot_element() {
+ return false;
+ }
+ }
+
+ true
+ }
+
+ #[inline]
+ fn flattened_tree_parent(&self) -> Option<Self> {
+ if self.flattened_tree_parent_is_parent() {
+ debug_assert_eq!(
+ unsafe {
+ bindings::Gecko_GetFlattenedTreeParentNode(self.0)
+ .as_ref()
+ .map(GeckoNode)
+ },
+ self.parent_node(),
+ "Fast path stopped holding!"
+ );
+ return self.parent_node();
+ }
+
+ // NOTE(emilio): If this call is too expensive, we could manually inline more aggressively.
+ unsafe {
+ bindings::Gecko_GetFlattenedTreeParentNode(self.0)
+ .as_ref()
+ .map(GeckoNode)
+ }
+ }
+
+ #[inline]
+ fn contains_non_whitespace_content(&self) -> bool {
+ unsafe { Gecko_IsSignificantChild(self.0, false) }
+ }
+
+ /// Returns the previous sibling of this node that is an element.
+ #[inline]
+ pub fn prev_sibling_element(&self) -> Option<GeckoElement> {
+ let mut prev = self.prev_sibling();
+ while let Some(p) = prev {
+ if let Some(e) = p.as_element() {
+ return Some(e);
+ }
+ prev = p.prev_sibling();
+ }
+ None
+ }
+
+ /// Returns the next sibling of this node that is an element.
+ #[inline]
+ pub fn next_sibling_element(&self) -> Option<GeckoElement> {
+ let mut next = self.next_sibling();
+ while let Some(n) = next {
+ if let Some(e) = n.as_element() {
+ return Some(e);
+ }
+ next = n.next_sibling();
+ }
+ None
+ }
+
+ /// Returns last child sibling of this node that is an element.
+ #[inline]
+ pub fn last_child_element(&self) -> Option<GeckoElement<'ln>> {
+ let last = match self.last_child() {
+ Some(n) => n,
+ None => return None,
+ };
+ if let Some(e) = last.as_element() {
+ return Some(e);
+ }
+ None
+ }
+}
+
+impl<'ln> NodeInfo for GeckoNode<'ln> {
+ #[inline]
+ fn is_element(&self) -> bool {
+ self.get_bool_flag(nsINode_BooleanFlag::NodeIsElement)
+ }
+
+ fn is_text_node(&self) -> bool {
+ // This is a DOM constant that isn't going to change.
+ const TEXT_NODE: u16 = 3;
+ self.node_info().mInner.mNodeType == TEXT_NODE
+ }
+}
+
+impl<'ln> TNode for GeckoNode<'ln> {
+ type ConcreteDocument = GeckoDocument<'ln>;
+ type ConcreteShadowRoot = GeckoShadowRoot<'ln>;
+ type ConcreteElement = GeckoElement<'ln>;
+
+ #[inline]
+ fn parent_node(&self) -> Option<Self> {
+ unsafe { self.0.mParent.as_ref().map(GeckoNode) }
+ }
+
+ #[inline]
+ fn first_child(&self) -> Option<Self> {
+ unsafe {
+ self.0
+ .mFirstChild
+ .raw()
+ .as_ref()
+ .map(GeckoNode::from_content)
+ }
+ }
+
+ #[inline]
+ fn last_child(&self) -> Option<Self> {
+ unsafe { bindings::Gecko_GetLastChild(self.0).as_ref().map(GeckoNode) }
+ }
+
+ #[inline]
+ fn prev_sibling(&self) -> Option<Self> {
+ unsafe {
+ let prev_or_last = GeckoNode::from_content(self.0.mPreviousOrLastSibling.as_ref()?);
+ if prev_or_last.0.mNextSibling.raw().is_null() {
+ return None;
+ }
+ Some(prev_or_last)
+ }
+ }
+
+ #[inline]
+ fn next_sibling(&self) -> Option<Self> {
+ unsafe {
+ self.0
+ .mNextSibling
+ .raw()
+ .as_ref()
+ .map(GeckoNode::from_content)
+ }
+ }
+
+ #[inline]
+ fn owner_doc(&self) -> Self::ConcreteDocument {
+ debug_assert!(!self.node_info().mDocument.is_null());
+ GeckoDocument(unsafe { &*self.node_info().mDocument })
+ }
+
+ #[inline]
+ fn is_in_document(&self) -> bool {
+ self.get_bool_flag(nsINode_BooleanFlag::IsInDocument)
+ }
+
+ fn traversal_parent(&self) -> Option<GeckoElement<'ln>> {
+ self.flattened_tree_parent().and_then(|n| n.as_element())
+ }
+
+ #[inline]
+ fn opaque(&self) -> OpaqueNode {
+ let ptr: usize = self.0 as *const _ as usize;
+ OpaqueNode(ptr)
+ }
+
+ fn debug_id(self) -> usize {
+ unimplemented!()
+ }
+
+ #[inline]
+ fn as_element(&self) -> Option<GeckoElement<'ln>> {
+ if !self.is_element() {
+ return None;
+ }
+
+ Some(GeckoElement(unsafe {
+ &*(self.0 as *const _ as *const RawGeckoElement)
+ }))
+ }
+
+ #[inline]
+ fn as_document(&self) -> Option<Self::ConcreteDocument> {
+ if !self.is_document() {
+ return None;
+ }
+
+ debug_assert_eq!(self.owner_doc().as_node(), *self, "How?");
+ Some(self.owner_doc())
+ }
+
+ #[inline]
+ fn as_shadow_root(&self) -> Option<Self::ConcreteShadowRoot> {
+ if !self.is_shadow_root() {
+ return None;
+ }
+
+ Some(GeckoShadowRoot(unsafe {
+ &*(self.0 as *const _ as *const structs::ShadowRoot)
+ }))
+ }
+}
+
+/// A wrapper on top of two kind of iterators, depending on the parent being
+/// iterated.
+///
+/// We generally iterate children by traversing the light-tree siblings of the
+/// first child like Servo does.
+///
+/// However, for nodes with anonymous children, we use a custom (heavier-weight)
+/// Gecko-implemented iterator.
+///
+/// FIXME(emilio): If we take into account shadow DOM, we're going to need the
+/// flat tree pretty much always. We can try to optimize the case where there's
+/// no shadow root sibling, probably.
+pub enum GeckoChildrenIterator<'a> {
+ /// A simple iterator that tracks the current node being iterated and
+ /// replaces it with the next sibling when requested.
+ Current(Option<GeckoNode<'a>>),
+ /// A Gecko-implemented iterator we need to drop appropriately.
+ GeckoIterator(structs::StyleChildrenIterator),
+}
+
+impl<'a> Drop for GeckoChildrenIterator<'a> {
+ fn drop(&mut self) {
+ if let GeckoChildrenIterator::GeckoIterator(ref mut it) = *self {
+ unsafe {
+ bindings::Gecko_DestroyStyleChildrenIterator(it);
+ }
+ }
+ }
+}
+
+impl<'a> Iterator for GeckoChildrenIterator<'a> {
+ type Item = GeckoNode<'a>;
+ fn next(&mut self) -> Option<GeckoNode<'a>> {
+ match *self {
+ GeckoChildrenIterator::Current(curr) => {
+ let next = curr.and_then(|node| node.next_sibling());
+ *self = GeckoChildrenIterator::Current(next);
+ curr
+ },
+ GeckoChildrenIterator::GeckoIterator(ref mut it) => unsafe {
+ // We do this unsafe lengthening of the lifetime here because
+ // structs::StyleChildrenIterator is actually StyleChildrenIterator<'a>,
+ // however we can't express this easily with bindgen, and it would
+ // introduce functions with two input lifetimes into bindgen,
+ // which would be out of scope for elision.
+ bindings::Gecko_GetNextStyleChild(&mut *(it as *mut _))
+ .as_ref()
+ .map(GeckoNode)
+ },
+ }
+ }
+}
+
+/// A simple wrapper over a non-null Gecko `Element` pointer.
+#[derive(Clone, Copy)]
+pub struct GeckoElement<'le>(pub &'le RawGeckoElement);
+
+impl<'le> fmt::Debug for GeckoElement<'le> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use nsstring::nsCString;
+
+ write!(f, "<{}", self.local_name())?;
+
+ let mut attrs = nsCString::new();
+ unsafe {
+ bindings::Gecko_Element_DebugListAttributes(self.0, &mut attrs);
+ }
+ write!(f, "{}", attrs)?;
+ write!(f, "> ({:#x})", self.as_node().opaque().0)
+ }
+}
+
+impl<'le> GeckoElement<'le> {
+ /// Gets the raw `ElementData` refcell for the element.
+ #[inline(always)]
+ pub fn get_data(&self) -> Option<&AtomicRefCell<ElementData>> {
+ unsafe { self.0.mServoData.get().as_ref() }
+ }
+
+ /// Returns whether any animation applies to this element.
+ #[inline]
+ pub fn has_any_animation(&self) -> bool {
+ self.may_have_animations() && unsafe { Gecko_ElementHasAnimations(self.0) }
+ }
+
+ #[inline(always)]
+ fn attrs(&self) -> &[structs::AttrArray_InternalAttr] {
+ unsafe {
+ match self.0.mAttrs.mImpl.mPtr.as_ref() {
+ Some(attrs) => attrs.mBuffer.as_slice(attrs.mAttrCount as usize),
+ None => return &[],
+ }
+ }
+ }
+
+ #[inline(always)]
+ fn get_part_attr(&self) -> Option<&structs::nsAttrValue> {
+ if !self.has_part_attr() {
+ return None;
+ }
+ snapshot_helpers::find_attr(self.attrs(), &atom!("part"))
+ }
+
+ #[inline(always)]
+ fn get_class_attr(&self) -> Option<&structs::nsAttrValue> {
+ if !self.may_have_class() {
+ return None;
+ }
+
+ if self.is_svg_element() {
+ let svg_class = unsafe { bindings::Gecko_GetSVGAnimatedClass(self.0).as_ref() };
+ if let Some(c) = svg_class {
+ return Some(c);
+ }
+ }
+
+ snapshot_helpers::find_attr(self.attrs(), &atom!("class"))
+ }
+
+ #[inline]
+ fn may_have_anonymous_children(&self) -> bool {
+ self.as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveAnonymousChildren)
+ }
+
+ #[inline]
+ fn flags(&self) -> u32 {
+ self.as_node().flags()
+ }
+
+ #[inline]
+ fn set_flags(&self, flags: u32) {
+ self.as_node().set_flags(flags);
+ }
+
+ #[inline]
+ unsafe fn unset_flags(&self, flags: u32) {
+ self.as_node()
+ .flags_atomic()
+ .fetch_and(!flags, Ordering::Relaxed);
+ }
+
+ /// Returns true if this element has descendants for lazy frame construction.
+ #[inline]
+ pub fn descendants_need_frames(&self) -> bool {
+ self.flags() & NODE_DESCENDANTS_NEED_FRAMES != 0
+ }
+
+ /// Returns true if this element needs lazy frame construction.
+ #[inline]
+ pub fn needs_frame(&self) -> bool {
+ self.flags() & NODE_NEEDS_FRAME != 0
+ }
+
+ /// Returns a reference to the DOM slots for this Element, if they exist.
+ #[inline]
+ fn dom_slots(&self) -> Option<&structs::FragmentOrElement_nsDOMSlots> {
+ let slots = self.as_node().0.mSlots as *const structs::FragmentOrElement_nsDOMSlots;
+ unsafe { slots.as_ref() }
+ }
+
+ /// Returns a reference to the extended DOM slots for this Element.
+ #[inline]
+ fn extended_slots(&self) -> Option<&structs::FragmentOrElement_nsExtendedDOMSlots> {
+ self.dom_slots().and_then(|s| unsafe {
+ // For the bit usage, see nsContentSlots::GetExtendedSlots.
+ let e_slots = s._base.mExtendedSlots &
+ !structs::nsIContent_nsContentSlots_sNonOwningExtendedSlotsFlag;
+ (e_slots as *const structs::FragmentOrElement_nsExtendedDOMSlots).as_ref()
+ })
+ }
+
+ #[inline]
+ fn namespace_id(&self) -> i32 {
+ self.as_node().node_info().mInner.mNamespaceID
+ }
+
+ #[inline]
+ fn has_id(&self) -> bool {
+ self.as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ElementHasID)
+ }
+
+ #[inline]
+ fn state_internal(&self) -> u64 {
+ if !self
+ .as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ElementHasLockedStyleStates)
+ {
+ return self.0.mState.bits;
+ }
+ unsafe { Gecko_ElementState(self.0) }
+ }
+
+ #[inline]
+ fn document_state(&self) -> DocumentState {
+ DocumentState::from_bits_retain(self.as_node().owner_doc().0.mState.bits)
+ }
+
+ #[inline]
+ fn may_have_class(&self) -> bool {
+ self.as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveClass)
+ }
+
+ #[inline]
+ fn has_properties(&self) -> bool {
+ use crate::gecko_bindings::structs::NODE_HAS_PROPERTIES;
+
+ self.flags() & NODE_HAS_PROPERTIES != 0
+ }
+
+ #[inline]
+ fn before_or_after_pseudo(&self, is_before: bool) -> Option<Self> {
+ if !self.has_properties() {
+ return None;
+ }
+
+ unsafe {
+ bindings::Gecko_GetBeforeOrAfterPseudo(self.0, is_before)
+ .as_ref()
+ .map(GeckoElement)
+ }
+ }
+
+ #[inline]
+ fn may_have_style_attribute(&self) -> bool {
+ self.as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveStyle)
+ }
+
+ /// Only safe to call on the main thread, with exclusive access to the
+ /// element and its ancestors.
+ ///
+ /// This function is also called after display property changed for SMIL
+ /// animation.
+ ///
+ /// Also this function schedules style flush.
+ pub unsafe fn note_explicit_hints(&self, restyle_hint: RestyleHint, change_hint: nsChangeHint) {
+ use crate::gecko::restyle_damage::GeckoRestyleDamage;
+
+ let damage = GeckoRestyleDamage::new(change_hint);
+ debug!(
+ "note_explicit_hints: {:?}, restyle_hint={:?}, change_hint={:?}",
+ self, restyle_hint, change_hint
+ );
+
+ debug_assert!(
+ !(restyle_hint.has_animation_hint() && restyle_hint.has_non_animation_hint()),
+ "Animation restyle hints should not appear with non-animation restyle hints"
+ );
+
+ let mut data = match self.mutate_data() {
+ Some(d) => d,
+ None => {
+ debug!("(Element not styled, discarding hints)");
+ return;
+ },
+ };
+
+ debug_assert!(data.has_styles(), "how?");
+
+ // Propagate the bit up the chain.
+ if restyle_hint.has_animation_hint() {
+ bindings::Gecko_NoteAnimationOnlyDirtyElement(self.0);
+ } else {
+ bindings::Gecko_NoteDirtyElement(self.0);
+ }
+
+ data.hint.insert(restyle_hint);
+ data.damage |= damage;
+ }
+
+ /// This logic is duplicated in Gecko's nsIContent::IsRootOfNativeAnonymousSubtree.
+ #[inline]
+ fn is_root_of_native_anonymous_subtree(&self) -> bool {
+ use crate::gecko_bindings::structs::NODE_IS_NATIVE_ANONYMOUS_ROOT;
+ return self.flags() & NODE_IS_NATIVE_ANONYMOUS_ROOT != 0;
+ }
+
+ fn css_transitions_info(&self) -> FxHashMap<OwnedPropertyDeclarationId, Arc<AnimationValue>> {
+ use crate::gecko_bindings::bindings::Gecko_ElementTransitions_EndValueAt;
+ use crate::gecko_bindings::bindings::Gecko_ElementTransitions_Length;
+
+ let collection_length = unsafe { Gecko_ElementTransitions_Length(self.0) } as usize;
+ let mut map = FxHashMap::with_capacity_and_hasher(collection_length, Default::default());
+
+ for i in 0..collection_length {
+ let end_value =
+ unsafe { Arc::from_raw_addrefed(Gecko_ElementTransitions_EndValueAt(self.0, i)) };
+ let property = end_value.id();
+ debug_assert!(!property.is_logical());
+ map.insert(property.to_owned(), end_value);
+ }
+ map
+ }
+
+ fn needs_transitions_update_per_property(
+ &self,
+ property_declaration_id: PropertyDeclarationId,
+ combined_duration_seconds: f32,
+ before_change_style: &ComputedValues,
+ after_change_style: &ComputedValues,
+ existing_transitions: &FxHashMap<OwnedPropertyDeclarationId, Arc<AnimationValue>>,
+ ) -> bool {
+ use crate::values::animated::{Animate, Procedure};
+ debug_assert!(!property_declaration_id.is_logical());
+
+ // If there is an existing transition, update only if the end value
+ // differs.
+ //
+ // If the end value has not changed, we should leave the currently
+ // running transition as-is since we don't want to interrupt its timing
+ // function.
+ if let Some(ref existing) = existing_transitions.get(&property_declaration_id.to_owned()) {
+ let after_value =
+ AnimationValue::from_computed_values(property_declaration_id, after_change_style)
+ .unwrap();
+
+ return ***existing != after_value;
+ }
+
+ let from =
+ AnimationValue::from_computed_values(property_declaration_id, before_change_style);
+ let to = AnimationValue::from_computed_values(property_declaration_id, after_change_style);
+
+ debug_assert_eq!(to.is_some(), from.is_some());
+
+ combined_duration_seconds > 0.0f32 &&
+ from != to &&
+ from.unwrap()
+ .animate(
+ to.as_ref().unwrap(),
+ Procedure::Interpolate { progress: 0.5 },
+ )
+ .is_ok()
+ }
+
+ /// Get slow selector flags required for nth-of invalidation.
+ pub fn slow_selector_flags(&self) -> ElementSelectorFlags {
+ slow_selector_flags_from_node_selector_flags(self.as_node().selector_flags())
+ }
+}
+
+/// Convert slow selector flags from the raw `NodeSelectorFlags`.
+pub fn slow_selector_flags_from_node_selector_flags(flags: u32) -> ElementSelectorFlags {
+ use crate::gecko_bindings::structs::NodeSelectorFlags;
+ let mut result = ElementSelectorFlags::empty();
+ if flags & NodeSelectorFlags::HasSlowSelector.0 != 0 {
+ result.insert(ElementSelectorFlags::HAS_SLOW_SELECTOR);
+ }
+ if flags & NodeSelectorFlags::HasSlowSelectorLaterSiblings.0 != 0 {
+ result.insert(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS);
+ }
+ result
+}
+
+/// Converts flags from the layout used by rust-selectors to the layout used
+/// by Gecko. We could align these and then do this without conditionals, but
+/// it's probably not worth the trouble.
+fn selector_flags_to_node_flags(flags: ElementSelectorFlags) -> u32 {
+ use crate::gecko_bindings::structs::NodeSelectorFlags;
+ let mut gecko_flags = 0u32;
+ if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR) {
+ gecko_flags |= NodeSelectorFlags::HasSlowSelector.0;
+ }
+ if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS) {
+ gecko_flags |= NodeSelectorFlags::HasSlowSelectorLaterSiblings.0;
+ }
+ if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH) {
+ gecko_flags |= NodeSelectorFlags::HasSlowSelectorNth.0;
+ }
+ if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF) {
+ gecko_flags |= NodeSelectorFlags::HasSlowSelectorNthOf.0;
+ }
+ if flags.contains(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) {
+ gecko_flags |= NodeSelectorFlags::HasEdgeChildSelector.0;
+ }
+ if flags.contains(ElementSelectorFlags::HAS_EMPTY_SELECTOR) {
+ gecko_flags |= NodeSelectorFlags::HasEmptySelector.0;
+ }
+ if flags.contains(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR) {
+ gecko_flags |= NodeSelectorFlags::RelativeSelectorAnchor.0;
+ }
+ if flags.contains(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT) {
+ gecko_flags |= NodeSelectorFlags::RelativeSelectorAnchorNonSubject.0;
+ }
+ if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR) {
+ gecko_flags |= NodeSelectorFlags::RelativeSelectorSearchDirectionAncestor.0;
+ }
+ if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING) {
+ gecko_flags |= NodeSelectorFlags::RelativeSelectorSearchDirectionSibling.0;
+ }
+
+ gecko_flags
+}
+
+fn get_animation_rule(
+ element: &GeckoElement,
+ cascade_level: CascadeLevel,
+) -> Option<Arc<Locked<PropertyDeclarationBlock>>> {
+ // There's a very rough correlation between the number of effects
+ // (animations) on an element and the number of properties it is likely to
+ // animate, so we use that as an initial guess for the size of the
+ // AnimationValueMap in order to reduce the number of re-allocations needed.
+ let effect_count = unsafe { Gecko_GetAnimationEffectCount(element.0) };
+ // Also, we should try to reuse the PDB, to avoid creating extra rule nodes.
+ let mut animation_values = AnimationValueMap::with_capacity_and_hasher(
+ effect_count.min(crate::properties::property_counts::ANIMATABLE),
+ Default::default(),
+ );
+ if unsafe { Gecko_GetAnimationRule(element.0, cascade_level, &mut animation_values) } {
+ let shared_lock = &GLOBAL_STYLE_DATA.shared_lock;
+ Some(Arc::new(shared_lock.wrap(
+ PropertyDeclarationBlock::from_animation_value_map(&animation_values),
+ )))
+ } else {
+ None
+ }
+}
+
+/// Turns a gecko namespace id into an atom. Might panic if you pass any random thing that isn't a
+/// namespace id.
+#[inline(always)]
+pub unsafe fn namespace_id_to_atom(id: i32) -> *mut nsAtom {
+ unsafe {
+ let namespace_manager = structs::nsNameSpaceManager_sInstance.mRawPtr;
+ (*namespace_manager).mURIArray[id as usize].mRawPtr
+ }
+}
+
+impl<'le> TElement for GeckoElement<'le> {
+ type ConcreteNode = GeckoNode<'le>;
+ type TraversalChildrenIterator = GeckoChildrenIterator<'le>;
+
+ fn inheritance_parent(&self) -> Option<Self> {
+ if self.is_pseudo_element() {
+ return self.pseudo_element_originating_element();
+ }
+
+ self.as_node()
+ .flattened_tree_parent()
+ .and_then(|n| n.as_element())
+ }
+
+ fn traversal_children(&self) -> LayoutIterator<GeckoChildrenIterator<'le>> {
+ // This condition is similar to the check that
+ // StyleChildrenIterator::IsNeeded does, except that it might return
+ // true if we used to (but no longer) have anonymous content from
+ // ::before/::after, or nsIAnonymousContentCreators.
+ if self.is_html_slot_element() ||
+ self.shadow_root().is_some() ||
+ self.may_have_anonymous_children()
+ {
+ unsafe {
+ let mut iter: structs::StyleChildrenIterator = ::std::mem::zeroed();
+ bindings::Gecko_ConstructStyleChildrenIterator(self.0, &mut iter);
+ return LayoutIterator(GeckoChildrenIterator::GeckoIterator(iter));
+ }
+ }
+
+ LayoutIterator(GeckoChildrenIterator::Current(self.as_node().first_child()))
+ }
+
+ fn before_pseudo_element(&self) -> Option<Self> {
+ self.before_or_after_pseudo(/* is_before = */ true)
+ }
+
+ fn after_pseudo_element(&self) -> Option<Self> {
+ self.before_or_after_pseudo(/* is_before = */ false)
+ }
+
+ fn marker_pseudo_element(&self) -> Option<Self> {
+ if !self.has_properties() {
+ return None;
+ }
+
+ unsafe {
+ bindings::Gecko_GetMarkerPseudo(self.0)
+ .as_ref()
+ .map(GeckoElement)
+ }
+ }
+
+ #[inline]
+ fn is_html_element(&self) -> bool {
+ self.namespace_id() == structs::kNameSpaceID_XHTML as i32
+ }
+
+ #[inline]
+ fn is_mathml_element(&self) -> bool {
+ self.namespace_id() == structs::kNameSpaceID_MathML as i32
+ }
+
+ #[inline]
+ fn is_svg_element(&self) -> bool {
+ self.namespace_id() == structs::kNameSpaceID_SVG as i32
+ }
+
+ #[inline]
+ fn is_xul_element(&self) -> bool {
+ self.namespace_id() == structs::root::kNameSpaceID_XUL as i32
+ }
+
+ #[inline]
+ fn local_name(&self) -> &WeakAtom {
+ unsafe { WeakAtom::new(self.as_node().node_info().mInner.mName) }
+ }
+
+ #[inline]
+ fn namespace(&self) -> &WeakNamespace {
+ unsafe { WeakNamespace::new(namespace_id_to_atom(self.namespace_id())) }
+ }
+
+ #[inline]
+ fn query_container_size(&self, display: &Display) -> Size2D<Option<Au>> {
+ // If an element gets 'display: contents' and its nsIFrame has not been removed yet,
+ // Gecko_GetQueryContainerSize will not notice that it can't have size containment.
+ // Other cases like 'display: inline' will be handled once the new nsIFrame is created.
+ if display.is_contents() {
+ return Size2D::new(None, None);
+ }
+
+ let mut width = -1;
+ let mut height = -1;
+ unsafe {
+ bindings::Gecko_GetQueryContainerSize(self.0, &mut width, &mut height);
+ }
+ Size2D::new(
+ if width >= 0 { Some(Au(width)) } else { None },
+ if height >= 0 { Some(Au(height)) } else { None },
+ )
+ }
+
+ /// Return the list of slotted nodes of this node.
+ #[inline]
+ fn slotted_nodes(&self) -> &[Self::ConcreteNode] {
+ if !self.is_html_slot_element() || !self.as_node().is_in_shadow_tree() {
+ return &[];
+ }
+
+ let slot: &structs::HTMLSlotElement = unsafe { mem::transmute(self.0) };
+
+ if cfg!(debug_assertions) {
+ let base: &RawGeckoElement = &slot._base._base._base;
+ assert_eq!(base as *const _, self.0 as *const _, "Bad cast");
+ }
+
+ // FIXME(emilio): Workaround a bindgen bug on Android that causes
+ // mAssignedNodes to be at the wrong offset. See bug 1466406.
+ //
+ // Bug 1466580 tracks running the Android layout tests on automation.
+ //
+ // The actual bindgen bug still needs reduction.
+ let assigned_nodes: &[structs::RefPtr<structs::nsINode>] = if !cfg!(target_os = "android") {
+ debug_assert_eq!(
+ unsafe { bindings::Gecko_GetAssignedNodes(self.0) },
+ &slot.mAssignedNodes as *const _,
+ );
+
+ &*slot.mAssignedNodes
+ } else {
+ unsafe { &**bindings::Gecko_GetAssignedNodes(self.0) }
+ };
+
+ debug_assert_eq!(
+ mem::size_of::<structs::RefPtr<structs::nsINode>>(),
+ mem::size_of::<Self::ConcreteNode>(),
+ "Bad cast!"
+ );
+
+ unsafe { mem::transmute(assigned_nodes) }
+ }
+
+ #[inline]
+ fn shadow_root(&self) -> Option<GeckoShadowRoot<'le>> {
+ let slots = self.extended_slots()?;
+ unsafe { slots.mShadowRoot.mRawPtr.as_ref().map(GeckoShadowRoot) }
+ }
+
+ #[inline]
+ fn containing_shadow(&self) -> Option<GeckoShadowRoot<'le>> {
+ let slots = self.extended_slots()?;
+ unsafe {
+ slots
+ ._base
+ .mContainingShadow
+ .mRawPtr
+ .as_ref()
+ .map(GeckoShadowRoot)
+ }
+ }
+
+ fn each_anonymous_content_child<F>(&self, mut f: F)
+ where
+ F: FnMut(Self),
+ {
+ if !self.may_have_anonymous_children() {
+ return;
+ }
+
+ let array: *mut structs::nsTArray<*mut nsIContent> =
+ unsafe { bindings::Gecko_GetAnonymousContentForElement(self.0) };
+
+ if array.is_null() {
+ return;
+ }
+
+ for content in unsafe { &**array } {
+ let node = GeckoNode::from_content(unsafe { &**content });
+ let element = match node.as_element() {
+ Some(e) => e,
+ None => continue,
+ };
+
+ f(element);
+ }
+
+ unsafe { bindings::Gecko_DestroyAnonymousContentList(array) };
+ }
+
+ #[inline]
+ fn as_node(&self) -> Self::ConcreteNode {
+ unsafe { GeckoNode(&*(self.0 as *const _ as *const RawGeckoNode)) }
+ }
+
+ fn owner_doc_matches_for_testing(&self, device: &Device) -> bool {
+ self.as_node().owner_doc().0 as *const structs::Document == device.document() as *const _
+ }
+
+ fn style_attribute(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>> {
+ if !self.may_have_style_attribute() {
+ return None;
+ }
+
+ unsafe {
+ let declarations = Gecko_GetStyleAttrDeclarationBlock(self.0).as_ref()?;
+ Some(ArcBorrow::from_ref(declarations))
+ }
+ }
+
+ fn unset_dirty_style_attribute(&self) {
+ if !self.may_have_style_attribute() {
+ return;
+ }
+
+ unsafe { Gecko_UnsetDirtyStyleAttr(self.0) };
+ }
+
+ fn smil_override(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>> {
+ unsafe {
+ let slots = self.extended_slots()?;
+
+ let declaration: &structs::DeclarationBlock =
+ slots.mSMILOverrideStyleDeclaration.mRawPtr.as_ref()?;
+
+ let raw: &structs::StyleLockedDeclarationBlock = declaration.mRaw.mRawPtr.as_ref()?;
+ Some(ArcBorrow::from_ref(raw))
+ }
+ }
+
+ fn animation_rule(
+ &self,
+ _: &SharedStyleContext,
+ ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> {
+ get_animation_rule(self, CascadeLevel::Animations)
+ }
+
+ fn transition_rule(
+ &self,
+ _: &SharedStyleContext,
+ ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> {
+ get_animation_rule(self, CascadeLevel::Transitions)
+ }
+
+ #[inline]
+ fn state(&self) -> ElementState {
+ ElementState::from_bits_retain(self.state_internal())
+ }
+
+ #[inline]
+ fn has_custom_state(&self, state: &CustomState) -> bool {
+ if !self.is_html_element() {
+ return false;
+ }
+ let check_state_ptr: *const nsAtom = state.0.as_ptr();
+ self.extended_slots().map_or(false, |slot| {
+ (&slot.mCustomStates).iter().any(|setstate| {
+ let setstate_ptr: *const nsAtom = setstate.mRawPtr;
+ setstate_ptr == check_state_ptr
+ })
+ })
+ }
+
+ #[inline]
+ fn has_part_attr(&self) -> bool {
+ self.as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ElementHasPart)
+ }
+
+ #[inline]
+ fn exports_any_part(&self) -> bool {
+ snapshot_helpers::find_attr(self.attrs(), &atom!("exportparts")).is_some()
+ }
+
+ // FIXME(emilio): we should probably just return a reference to the Atom.
+ #[inline]
+ fn id(&self) -> Option<&WeakAtom> {
+ if !self.has_id() {
+ return None;
+ }
+ snapshot_helpers::get_id(self.attrs())
+ }
+
+ fn each_attr_name<F>(&self, mut callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ for attr in self.attrs() {
+ unsafe { AtomIdent::with(attr.mName.name(), |a| callback(a)) }
+ }
+ }
+
+ fn each_class<F>(&self, callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ let attr = match self.get_class_attr() {
+ Some(c) => c,
+ None => return,
+ };
+
+ snapshot_helpers::each_class_or_part(attr, callback)
+ }
+
+ #[inline]
+ fn each_exported_part<F>(&self, name: &AtomIdent, callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ snapshot_helpers::each_exported_part(self.attrs(), name, callback)
+ }
+
+ fn each_part<F>(&self, callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ let attr = match self.get_part_attr() {
+ Some(c) => c,
+ None => return,
+ };
+
+ snapshot_helpers::each_class_or_part(attr, callback)
+ }
+
+ #[inline]
+ fn has_snapshot(&self) -> bool {
+ self.flags() & ELEMENT_HAS_SNAPSHOT != 0
+ }
+
+ #[inline]
+ fn handled_snapshot(&self) -> bool {
+ self.flags() & ELEMENT_HANDLED_SNAPSHOT != 0
+ }
+
+ unsafe fn set_handled_snapshot(&self) {
+ debug_assert!(self.has_data());
+ self.set_flags(ELEMENT_HANDLED_SNAPSHOT)
+ }
+
+ #[inline]
+ fn has_dirty_descendants(&self) -> bool {
+ self.flags() & ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO != 0
+ }
+
+ unsafe fn set_dirty_descendants(&self) {
+ debug_assert!(self.has_data());
+ debug!("Setting dirty descendants: {:?}", self);
+ self.set_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO)
+ }
+
+ unsafe fn unset_dirty_descendants(&self) {
+ self.unset_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO)
+ }
+
+ #[inline]
+ fn has_animation_only_dirty_descendants(&self) -> bool {
+ self.flags() & ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO != 0
+ }
+
+ unsafe fn set_animation_only_dirty_descendants(&self) {
+ self.set_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO)
+ }
+
+ unsafe fn unset_animation_only_dirty_descendants(&self) {
+ self.unset_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO)
+ }
+
+ unsafe fn clear_descendant_bits(&self) {
+ self.unset_flags(
+ ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO |
+ ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO |
+ NODE_DESCENDANTS_NEED_FRAMES,
+ )
+ }
+
+ fn is_visited_link(&self) -> bool {
+ self.state().intersects(ElementState::VISITED)
+ }
+
+ /// We want to match rules from the same tree in all cases, except for native anonymous content
+ /// that _isn't_ part directly of a UA widget (e.g., such generated by form controls, or
+ /// pseudo-elements).
+ #[inline]
+ fn matches_user_and_content_rules(&self) -> bool {
+ use crate::gecko_bindings::structs::{
+ NODE_HAS_BEEN_IN_UA_WIDGET, NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE,
+ };
+ let flags = self.flags();
+ (flags & NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE) == 0 ||
+ (flags & NODE_HAS_BEEN_IN_UA_WIDGET) != 0
+ }
+
+ #[inline]
+ fn implemented_pseudo_element(&self) -> Option<PseudoElement> {
+ if self.matches_user_and_content_rules() {
+ return None;
+ }
+
+ if !self.has_properties() {
+ return None;
+ }
+
+ PseudoElement::from_pseudo_type(
+ unsafe { bindings::Gecko_GetImplementedPseudo(self.0) },
+ None,
+ )
+ }
+
+ #[inline]
+ fn store_children_to_process(&self, _: isize) {
+ // This is only used for bottom-up traversal, and is thus a no-op for Gecko.
+ }
+
+ fn did_process_child(&self) -> isize {
+ panic!("Atomic child count not implemented in Gecko");
+ }
+
+ unsafe fn ensure_data(&self) -> AtomicRefMut<ElementData> {
+ if !self.has_data() {
+ debug!("Creating ElementData for {:?}", self);
+ let ptr = Box::into_raw(Box::new(AtomicRefCell::new(ElementData::default())));
+ self.0.mServoData.set(ptr);
+ }
+ self.mutate_data().unwrap()
+ }
+
+ unsafe fn clear_data(&self) {
+ let ptr = self.0.mServoData.get();
+ self.unset_flags(
+ ELEMENT_HAS_SNAPSHOT |
+ ELEMENT_HANDLED_SNAPSHOT |
+ structs::Element_kAllServoDescendantBits |
+ NODE_NEEDS_FRAME,
+ );
+ if !ptr.is_null() {
+ debug!("Dropping ElementData for {:?}", self);
+ let data = Box::from_raw(self.0.mServoData.get());
+ self.0.mServoData.set(ptr::null_mut());
+
+ // Perform a mutable borrow of the data in debug builds. This
+ // serves as an assertion that there are no outstanding borrows
+ // when we destroy the data.
+ debug_assert!({
+ let _ = data.borrow_mut();
+ true
+ });
+ }
+ }
+
+ #[inline]
+ fn skip_item_display_fixup(&self) -> bool {
+ debug_assert!(
+ !self.is_pseudo_element(),
+ "Just don't call me if I'm a pseudo, you should know the answer already"
+ );
+ self.is_root_of_native_anonymous_subtree()
+ }
+
+ #[inline]
+ fn may_have_animations(&self) -> bool {
+ if let Some(pseudo) = self.implemented_pseudo_element() {
+ if pseudo.animations_stored_in_parent() {
+ // FIXME(emilio): When would the parent of a ::before / ::after
+ // pseudo-element be null?
+ return self.parent_element().map_or(false, |p| {
+ p.as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ElementHasAnimations)
+ });
+ }
+ }
+ self.as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ElementHasAnimations)
+ }
+
+ /// Process various tasks that are a result of animation-only restyle.
+ fn process_post_animation(&self, tasks: PostAnimationTasks) {
+ debug_assert!(!tasks.is_empty(), "Should be involved a task");
+
+ // If display style was changed from none to other, we need to resolve
+ // the descendants in the display:none subtree. Instead of resolving
+ // those styles in animation-only restyle, we defer it to a subsequent
+ // normal restyle.
+ if tasks.intersects(PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL) {
+ debug_assert!(
+ self.implemented_pseudo_element()
+ .map_or(true, |p| !p.is_before_or_after()),
+ "display property animation shouldn't run on pseudo elements \
+ since it's only for SMIL"
+ );
+ unsafe {
+ self.note_explicit_hints(
+ RestyleHint::restyle_subtree(),
+ nsChangeHint::nsChangeHint_Empty,
+ );
+ }
+ }
+ }
+
+ /// Update various animation-related state on a given (pseudo-)element as
+ /// results of normal restyle.
+ fn update_animations(
+ &self,
+ before_change_style: Option<Arc<ComputedValues>>,
+ tasks: UpdateAnimationsTasks,
+ ) {
+ // We have to update animations even if the element has no computed
+ // style since it means the element is in a display:none subtree, we
+ // should destroy all CSS animations in display:none subtree.
+ let computed_data = self.borrow_data();
+ let computed_values = computed_data.as_ref().map(|d| d.styles.primary());
+ let before_change_values = before_change_style
+ .as_ref()
+ .map_or(ptr::null(), |x| x.as_gecko_computed_style());
+ let computed_values_opt = computed_values
+ .as_ref()
+ .map_or(ptr::null(), |x| x.as_gecko_computed_style());
+ unsafe {
+ Gecko_UpdateAnimations(
+ self.0,
+ before_change_values,
+ computed_values_opt,
+ tasks.bits(),
+ );
+ }
+ }
+
+ #[inline]
+ fn has_animations(&self, _: &SharedStyleContext) -> bool {
+ self.has_any_animation()
+ }
+
+ fn has_css_animations(&self, _: &SharedStyleContext, _: Option<PseudoElement>) -> bool {
+ self.may_have_animations() && unsafe { Gecko_ElementHasCSSAnimations(self.0) }
+ }
+
+ fn has_css_transitions(&self, _: &SharedStyleContext, _: Option<PseudoElement>) -> bool {
+ self.may_have_animations() && unsafe { Gecko_ElementHasCSSTransitions(self.0) }
+ }
+
+ // Detect if there are any changes that require us to update transitions.
+ //
+ // This is used as a more thoroughgoing check than the cheaper
+ // might_need_transitions_update check.
+ //
+ // The following logic shadows the logic used on the Gecko side
+ // (nsTransitionManager::DoUpdateTransitions) where we actually perform the
+ // update.
+ //
+ // https://drafts.csswg.org/css-transitions/#starting
+ fn needs_transitions_update(
+ &self,
+ before_change_style: &ComputedValues,
+ after_change_style: &ComputedValues,
+ ) -> bool {
+ let after_change_ui_style = after_change_style.get_ui();
+ let existing_transitions = self.css_transitions_info();
+
+ if after_change_style.get_box().clone_display().is_none() {
+ // We need to cancel existing transitions.
+ return !existing_transitions.is_empty();
+ }
+
+ let mut transitions_to_keep = PropertyDeclarationIdSet::default();
+ for transition_property in after_change_style.transition_properties() {
+ let physical_longhand = PropertyDeclarationId::Longhand(
+ transition_property
+ .longhand_id
+ .to_physical(after_change_style.writing_mode),
+ );
+ transitions_to_keep.insert(physical_longhand);
+ if self.needs_transitions_update_per_property(
+ physical_longhand,
+ after_change_ui_style
+ .transition_combined_duration_at(transition_property.index)
+ .seconds(),
+ before_change_style,
+ after_change_style,
+ &existing_transitions,
+ ) {
+ return true;
+ }
+ }
+
+ // Check if we have to cancel the running transition because this is not
+ // a matching transition-property value.
+ existing_transitions
+ .keys()
+ .any(|property| !transitions_to_keep.contains(property.as_borrowed()))
+ }
+
+ /// Whether there is an ElementData container.
+ #[inline]
+ fn has_data(&self) -> bool {
+ self.get_data().is_some()
+ }
+
+ /// Immutably borrows the ElementData.
+ fn borrow_data(&self) -> Option<AtomicRef<ElementData>> {
+ self.get_data().map(|x| x.borrow())
+ }
+
+ /// Mutably borrows the ElementData.
+ fn mutate_data(&self) -> Option<AtomicRefMut<ElementData>> {
+ self.get_data().map(|x| x.borrow_mut())
+ }
+
+ #[inline]
+ fn lang_attr(&self) -> Option<AttrValue> {
+ let ptr = unsafe { bindings::Gecko_LangValue(self.0) };
+ if ptr.is_null() {
+ None
+ } else {
+ Some(AtomString(unsafe { Atom::from_addrefed(ptr) }))
+ }
+ }
+
+ fn match_element_lang(&self, override_lang: Option<Option<AttrValue>>, value: &Lang) -> bool {
+ // Gecko supports :lang() from CSS Selectors 4, which accepts a list
+ // of language tags, and does BCP47-style range matching.
+ let override_lang_ptr = match override_lang {
+ Some(Some(ref atom)) => atom.as_ptr(),
+ _ => ptr::null_mut(),
+ };
+ value.0.iter().any(|lang| unsafe {
+ Gecko_MatchLang(
+ self.0,
+ override_lang_ptr,
+ override_lang.is_some(),
+ lang.as_slice().as_ptr(),
+ )
+ })
+ }
+
+ fn is_html_document_body_element(&self) -> bool {
+ if self.local_name() != &**local_name!("body") {
+ return false;
+ }
+
+ if !self.is_html_element() {
+ return false;
+ }
+
+ unsafe { bindings::Gecko_IsDocumentBody(self.0) }
+ }
+
+ fn synthesize_presentational_hints_for_legacy_attributes<V>(
+ &self,
+ visited_handling: VisitedHandlingMode,
+ hints: &mut V,
+ ) where
+ V: Push<ApplicableDeclarationBlock>,
+ {
+ use crate::properties::longhands::_x_lang::SpecifiedValue as SpecifiedLang;
+ use crate::properties::longhands::color::SpecifiedValue as SpecifiedColor;
+ use crate::stylesheets::layer_rule::LayerOrder;
+ use crate::values::specified::{color::Color, font::XTextScale};
+ lazy_static! {
+ static ref TABLE_COLOR_RULE: ApplicableDeclarationBlock = {
+ let global_style_data = &*GLOBAL_STYLE_DATA;
+ let pdb = PropertyDeclarationBlock::with_one(
+ PropertyDeclaration::Color(SpecifiedColor(Color::InheritFromBodyQuirk.into())),
+ Importance::Normal,
+ );
+ let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb));
+ ApplicableDeclarationBlock::from_declarations(
+ arc,
+ ServoCascadeLevel::PresHints,
+ LayerOrder::root(),
+ )
+ };
+ static ref MATHML_LANG_RULE: ApplicableDeclarationBlock = {
+ let global_style_data = &*GLOBAL_STYLE_DATA;
+ let pdb = PropertyDeclarationBlock::with_one(
+ PropertyDeclaration::XLang(SpecifiedLang(atom!("x-math"))),
+ Importance::Normal,
+ );
+ let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb));
+ ApplicableDeclarationBlock::from_declarations(
+ arc,
+ ServoCascadeLevel::PresHints,
+ LayerOrder::root(),
+ )
+ };
+ static ref SVG_TEXT_DISABLE_SCALE_RULE: ApplicableDeclarationBlock = {
+ let global_style_data = &*GLOBAL_STYLE_DATA;
+ let pdb = PropertyDeclarationBlock::with_one(
+ PropertyDeclaration::XTextScale(XTextScale::None),
+ Importance::Normal,
+ );
+ let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb));
+ ApplicableDeclarationBlock::from_declarations(
+ arc,
+ ServoCascadeLevel::PresHints,
+ LayerOrder::root(),
+ )
+ };
+ };
+
+ let ns = self.namespace_id();
+ // <th> elements get a default MozCenterOrInherit which may get overridden
+ if ns == structs::kNameSpaceID_XHTML as i32 {
+ if self.local_name().as_ptr() == atom!("table").as_ptr() &&
+ self.as_node().owner_doc().quirks_mode() == QuirksMode::Quirks
+ {
+ hints.push(TABLE_COLOR_RULE.clone());
+ }
+ }
+ if ns == structs::kNameSpaceID_SVG as i32 {
+ if self.local_name().as_ptr() == atom!("text").as_ptr() {
+ hints.push(SVG_TEXT_DISABLE_SCALE_RULE.clone());
+ }
+ }
+ let declarations =
+ unsafe { Gecko_GetHTMLPresentationAttrDeclarationBlock(self.0).as_ref() };
+ if let Some(decl) = declarations {
+ hints.push(ApplicableDeclarationBlock::from_declarations(
+ unsafe { Arc::from_raw_addrefed(decl) },
+ ServoCascadeLevel::PresHints,
+ LayerOrder::root(),
+ ));
+ }
+ let declarations = unsafe { Gecko_GetExtraContentStyleDeclarations(self.0).as_ref() };
+ if let Some(decl) = declarations {
+ hints.push(ApplicableDeclarationBlock::from_declarations(
+ unsafe { Arc::from_raw_addrefed(decl) },
+ ServoCascadeLevel::PresHints,
+ LayerOrder::root(),
+ ));
+ }
+
+ // Support for link, vlink, and alink presentation hints on <body>
+ if self.is_link() {
+ // Unvisited vs. visited styles are computed up-front based on the
+ // visited mode (not the element's actual state).
+ let declarations = match visited_handling {
+ VisitedHandlingMode::AllLinksVisitedAndUnvisited => {
+ unreachable!(
+ "We should never try to selector match with \
+ AllLinksVisitedAndUnvisited"
+ );
+ },
+ VisitedHandlingMode::AllLinksUnvisited => unsafe {
+ Gecko_GetUnvisitedLinkAttrDeclarationBlock(self.0).as_ref()
+ },
+ VisitedHandlingMode::RelevantLinkVisited => unsafe {
+ Gecko_GetVisitedLinkAttrDeclarationBlock(self.0).as_ref()
+ },
+ };
+ if let Some(decl) = declarations {
+ hints.push(ApplicableDeclarationBlock::from_declarations(
+ unsafe { Arc::from_raw_addrefed(decl) },
+ ServoCascadeLevel::PresHints,
+ LayerOrder::root(),
+ ));
+ }
+
+ let active = self
+ .state()
+ .intersects(NonTSPseudoClass::Active.state_flag());
+ if active {
+ let declarations =
+ unsafe { Gecko_GetActiveLinkAttrDeclarationBlock(self.0).as_ref() };
+ if let Some(decl) = declarations {
+ hints.push(ApplicableDeclarationBlock::from_declarations(
+ unsafe { Arc::from_raw_addrefed(decl) },
+ ServoCascadeLevel::PresHints,
+ LayerOrder::root(),
+ ));
+ }
+ }
+ }
+
+ // xml:lang has precedence over lang, which can be
+ // set by Gecko_GetHTMLPresentationAttrDeclarationBlock
+ //
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#language
+ let ptr = unsafe { bindings::Gecko_GetXMLLangValue(self.0) };
+ if !ptr.is_null() {
+ let global_style_data = &*GLOBAL_STYLE_DATA;
+
+ let pdb = PropertyDeclarationBlock::with_one(
+ PropertyDeclaration::XLang(SpecifiedLang(unsafe { Atom::from_addrefed(ptr) })),
+ Importance::Normal,
+ );
+ let arc = Arc::new(global_style_data.shared_lock.wrap(pdb));
+ hints.push(ApplicableDeclarationBlock::from_declarations(
+ arc,
+ ServoCascadeLevel::PresHints,
+ LayerOrder::root(),
+ ))
+ }
+ // MathML's default lang has precedence over both `lang` and `xml:lang`
+ if ns == structs::kNameSpaceID_MathML as i32 {
+ if self.local_name().as_ptr() == atom!("math").as_ptr() {
+ hints.push(MATHML_LANG_RULE.clone());
+ }
+ }
+ }
+
+ fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool {
+ let node_flags = selector_flags_to_node_flags(flags);
+ self.as_node().selector_flags() & node_flags == node_flags
+ }
+
+ fn relative_selector_search_direction(&self) -> Option<ElementSelectorFlags> {
+ use crate::gecko_bindings::structs::NodeSelectorFlags;
+ let flags = self.as_node().selector_flags();
+ if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionAncestorSibling.0) != 0 {
+ Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING)
+ } else if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionAncestor.0) != 0 {
+ Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR)
+ } else if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionSibling.0) != 0 {
+ Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING)
+ } else {
+ None
+ }
+ }
+}
+
+impl<'le> PartialEq for GeckoElement<'le> {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.0 as *const _ == other.0 as *const _
+ }
+}
+
+impl<'le> Eq for GeckoElement<'le> {}
+
+impl<'le> Hash for GeckoElement<'le> {
+ #[inline]
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ (self.0 as *const RawGeckoElement).hash(state);
+ }
+}
+
+impl<'le> ::selectors::Element for GeckoElement<'le> {
+ type Impl = SelectorImpl;
+
+ #[inline]
+ fn opaque(&self) -> OpaqueElement {
+ OpaqueElement::new(self.0)
+ }
+
+ #[inline]
+ fn parent_element(&self) -> Option<Self> {
+ let parent_node = self.as_node().parent_node();
+ parent_node.and_then(|n| n.as_element())
+ }
+
+ #[inline]
+ fn parent_node_is_shadow_root(&self) -> bool {
+ self.as_node()
+ .parent_node()
+ .map_or(false, |p| p.is_shadow_root())
+ }
+
+ #[inline]
+ fn containing_shadow_host(&self) -> Option<Self> {
+ let shadow = self.containing_shadow()?;
+ Some(shadow.host())
+ }
+
+ #[inline]
+ fn is_pseudo_element(&self) -> bool {
+ self.implemented_pseudo_element().is_some()
+ }
+
+ #[inline]
+ fn pseudo_element_originating_element(&self) -> Option<Self> {
+ debug_assert!(self.is_pseudo_element());
+ debug_assert!(!self.matches_user_and_content_rules());
+ let mut current = *self;
+ loop {
+ if current.is_root_of_native_anonymous_subtree() {
+ return current.traversal_parent();
+ }
+
+ current = current.traversal_parent()?;
+ }
+ }
+
+ #[inline]
+ fn assigned_slot(&self) -> Option<Self> {
+ let slot = self.extended_slots()?._base.mAssignedSlot.mRawPtr;
+
+ unsafe { Some(GeckoElement(&slot.as_ref()?._base._base._base)) }
+ }
+
+ #[inline]
+ fn prev_sibling_element(&self) -> Option<Self> {
+ let mut sibling = self.as_node().prev_sibling();
+ while let Some(sibling_node) = sibling {
+ if let Some(el) = sibling_node.as_element() {
+ return Some(el);
+ }
+ sibling = sibling_node.prev_sibling();
+ }
+ None
+ }
+
+ #[inline]
+ fn next_sibling_element(&self) -> Option<Self> {
+ let mut sibling = self.as_node().next_sibling();
+ while let Some(sibling_node) = sibling {
+ if let Some(el) = sibling_node.as_element() {
+ return Some(el);
+ }
+ sibling = sibling_node.next_sibling();
+ }
+ None
+ }
+
+ #[inline]
+ fn first_element_child(&self) -> Option<Self> {
+ let mut child = self.as_node().first_child();
+ while let Some(child_node) = child {
+ if let Some(el) = child_node.as_element() {
+ return Some(el);
+ }
+ child = child_node.next_sibling();
+ }
+ None
+ }
+
+ fn apply_selector_flags(&self, flags: ElementSelectorFlags) {
+ // Handle flags that apply to the element.
+ let self_flags = flags.for_self();
+ if !self_flags.is_empty() {
+ self.as_node()
+ .set_selector_flags(selector_flags_to_node_flags(flags))
+ }
+
+ // Handle flags that apply to the parent.
+ let parent_flags = flags.for_parent();
+ if !parent_flags.is_empty() {
+ if let Some(p) = self.as_node().parent_node() {
+ if p.is_element() || p.is_shadow_root() {
+ p.set_selector_flags(selector_flags_to_node_flags(parent_flags));
+ }
+ }
+ }
+ }
+
+ fn has_attr_in_no_namespace(&self, local_name: &LocalName) -> bool {
+ for attr in self.attrs() {
+ if attr.mName.mBits == local_name.as_ptr() as usize {
+ return true;
+ }
+ }
+ false
+ }
+
+ fn attr_matches(
+ &self,
+ ns: &NamespaceConstraint<&Namespace>,
+ local_name: &LocalName,
+ operation: &AttrSelectorOperation<&AttrValue>,
+ ) -> bool {
+ snapshot_helpers::attr_matches(self.attrs(), ns, local_name, operation)
+ }
+
+ #[inline]
+ fn is_root(&self) -> bool {
+ if self
+ .as_node()
+ .get_bool_flag(nsINode_BooleanFlag::ParentIsContent)
+ {
+ return false;
+ }
+
+ if !self.as_node().is_in_document() {
+ return false;
+ }
+
+ debug_assert!(self
+ .as_node()
+ .parent_node()
+ .map_or(false, |p| p.is_document()));
+ // XXX this should always return true at this point, shouldn't it?
+ unsafe { bindings::Gecko_IsRootElement(self.0) }
+ }
+
+ fn is_empty(&self) -> bool {
+ !self
+ .as_node()
+ .dom_children()
+ .any(|child| unsafe { Gecko_IsSignificantChild(child.0, true) })
+ }
+
+ #[inline]
+ fn has_local_name(&self, name: &WeakAtom) -> bool {
+ self.local_name() == name
+ }
+
+ #[inline]
+ fn has_namespace(&self, ns: &WeakNamespace) -> bool {
+ self.namespace() == ns
+ }
+
+ #[inline]
+ fn is_same_type(&self, other: &Self) -> bool {
+ self.local_name() == other.local_name() && self.namespace() == other.namespace()
+ }
+
+ fn match_non_ts_pseudo_class(
+ &self,
+ pseudo_class: &NonTSPseudoClass,
+ context: &mut MatchingContext<Self::Impl>,
+ ) -> bool {
+ use selectors::matching::*;
+ match *pseudo_class {
+ NonTSPseudoClass::Autofill |
+ NonTSPseudoClass::Defined |
+ NonTSPseudoClass::Focus |
+ NonTSPseudoClass::Enabled |
+ NonTSPseudoClass::Disabled |
+ NonTSPseudoClass::Checked |
+ NonTSPseudoClass::Fullscreen |
+ NonTSPseudoClass::Indeterminate |
+ NonTSPseudoClass::MozInert |
+ NonTSPseudoClass::PopoverOpen |
+ NonTSPseudoClass::PlaceholderShown |
+ NonTSPseudoClass::Target |
+ NonTSPseudoClass::Valid |
+ NonTSPseudoClass::Invalid |
+ NonTSPseudoClass::MozBroken |
+ NonTSPseudoClass::Required |
+ NonTSPseudoClass::Optional |
+ NonTSPseudoClass::ReadOnly |
+ NonTSPseudoClass::ReadWrite |
+ NonTSPseudoClass::FocusWithin |
+ NonTSPseudoClass::FocusVisible |
+ NonTSPseudoClass::MozDragOver |
+ NonTSPseudoClass::MozDevtoolsHighlighted |
+ NonTSPseudoClass::MozStyleeditorTransitioning |
+ NonTSPseudoClass::MozMathIncrementScriptLevel |
+ NonTSPseudoClass::InRange |
+ NonTSPseudoClass::OutOfRange |
+ NonTSPseudoClass::Default |
+ NonTSPseudoClass::UserValid |
+ NonTSPseudoClass::UserInvalid |
+ NonTSPseudoClass::MozMeterOptimum |
+ NonTSPseudoClass::MozMeterSubOptimum |
+ NonTSPseudoClass::MozMeterSubSubOptimum |
+ NonTSPseudoClass::MozHasDirAttr |
+ NonTSPseudoClass::MozDirAttrLTR |
+ NonTSPseudoClass::MozDirAttrRTL |
+ NonTSPseudoClass::MozDirAttrLikeAuto |
+ NonTSPseudoClass::Modal |
+ NonTSPseudoClass::MozTopmostModal |
+ NonTSPseudoClass::Active |
+ NonTSPseudoClass::Hover |
+ NonTSPseudoClass::MozAutofillPreview |
+ NonTSPseudoClass::MozRevealed |
+ NonTSPseudoClass::MozValueEmpty => self.state().intersects(pseudo_class.state_flag()),
+ // TODO: This applying only to HTML elements is weird.
+ NonTSPseudoClass::Dir(ref dir) => {
+ self.is_html_element() && self.state().intersects(dir.element_state())
+ },
+ NonTSPseudoClass::AnyLink => self.is_link(),
+ NonTSPseudoClass::Link => {
+ self.is_link() && context.visited_handling().matches_unvisited()
+ },
+ NonTSPseudoClass::CustomState(ref state) => self.has_custom_state(state),
+ NonTSPseudoClass::Visited => {
+ self.is_link() && context.visited_handling().matches_visited()
+ },
+ NonTSPseudoClass::MozFirstNode => {
+ if context.needs_selector_flags() {
+ self.apply_selector_flags(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR);
+ }
+ let mut elem = self.as_node();
+ while let Some(prev) = elem.prev_sibling() {
+ if prev.contains_non_whitespace_content() {
+ return false;
+ }
+ elem = prev;
+ }
+ true
+ },
+ NonTSPseudoClass::MozLastNode => {
+ if context.needs_selector_flags() {
+ self.apply_selector_flags(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR);
+ }
+ let mut elem = self.as_node();
+ while let Some(next) = elem.next_sibling() {
+ if next.contains_non_whitespace_content() {
+ return false;
+ }
+ elem = next;
+ }
+ true
+ },
+ NonTSPseudoClass::MozOnlyWhitespace => {
+ if context.needs_selector_flags() {
+ self.apply_selector_flags(ElementSelectorFlags::HAS_EMPTY_SELECTOR);
+ }
+ if self
+ .as_node()
+ .dom_children()
+ .any(|c| c.contains_non_whitespace_content())
+ {
+ return false;
+ }
+ true
+ },
+ NonTSPseudoClass::MozNativeAnonymous => !self.matches_user_and_content_rules(),
+ NonTSPseudoClass::MozTableBorderNonzero => unsafe {
+ bindings::Gecko_IsTableBorderNonzero(self.0)
+ },
+ NonTSPseudoClass::MozSelectListBox => unsafe {
+ bindings::Gecko_IsSelectListBox(self.0)
+ },
+ NonTSPseudoClass::MozIsHTML => self.as_node().owner_doc().is_html_document(),
+ NonTSPseudoClass::MozLWTheme |
+ NonTSPseudoClass::MozLocaleDir(..) |
+ NonTSPseudoClass::MozWindowInactive => {
+ let state_bit = pseudo_class.document_state_flag();
+ if state_bit.is_empty() {
+ debug_assert!(
+ matches!(pseudo_class, NonTSPseudoClass::MozLocaleDir(..)),
+ "Only moz-locale-dir should ever return an empty state"
+ );
+ return false;
+ }
+ if context
+ .extra_data
+ .invalidation_data
+ .document_state
+ .intersects(state_bit)
+ {
+ return !context.in_negation();
+ }
+ self.document_state().contains(state_bit)
+ },
+ NonTSPseudoClass::MozPlaceholder => false,
+ NonTSPseudoClass::Lang(ref lang_arg) => self.match_element_lang(None, lang_arg),
+ }
+ }
+
+ fn match_pseudo_element(
+ &self,
+ pseudo_element: &PseudoElement,
+ _context: &mut MatchingContext<Self::Impl>,
+ ) -> bool {
+ // TODO(emilio): I believe we could assert we are a pseudo-element and
+ // match the proper pseudo-element, given how we rulehash the stuff
+ // based on the pseudo.
+ match self.implemented_pseudo_element() {
+ Some(ref pseudo) => *pseudo == *pseudo_element,
+ None => false,
+ }
+ }
+
+ #[inline]
+ fn is_link(&self) -> bool {
+ self.state().intersects(ElementState::VISITED_OR_UNVISITED)
+ }
+
+ #[inline]
+ fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
+ if !self.has_id() {
+ return false;
+ }
+
+ let element_id = match snapshot_helpers::get_id(self.attrs()) {
+ Some(id) => id,
+ None => return false,
+ };
+
+ case_sensitivity.eq_atom(element_id, id)
+ }
+
+ #[inline]
+ fn is_part(&self, name: &AtomIdent) -> bool {
+ let attr = match self.get_part_attr() {
+ Some(c) => c,
+ None => return false,
+ };
+
+ snapshot_helpers::has_class_or_part(name, CaseSensitivity::CaseSensitive, attr)
+ }
+
+ #[inline]
+ fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> {
+ snapshot_helpers::imported_part(self.attrs(), name)
+ }
+
+ #[inline(always)]
+ fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
+ let attr = match self.get_class_attr() {
+ Some(c) => c,
+ None => return false,
+ };
+
+ snapshot_helpers::has_class_or_part(name, case_sensitivity, attr)
+ }
+
+ #[inline]
+ fn is_html_element_in_html_document(&self) -> bool {
+ self.is_html_element() && self.as_node().owner_doc().is_html_document()
+ }
+
+ #[inline]
+ fn is_html_slot_element(&self) -> bool {
+ self.is_html_element() && self.local_name().as_ptr() == local_name!("slot").as_ptr()
+ }
+
+ #[inline]
+ fn ignores_nth_child_selectors(&self) -> bool {
+ self.is_root_of_native_anonymous_subtree()
+ }
+
+ fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool {
+ each_relevant_element_hash(*self, |hash| filter.insert_hash(hash & BLOOM_HASH_MASK));
+ true
+ }
+}
diff --git a/servo/components/style/gecko_bindings/mod.rs b/servo/components/style/gecko_bindings/mod.rs
new file mode 100644
index 0000000000..f0b0adc7ec
--- /dev/null
+++ b/servo/components/style/gecko_bindings/mod.rs
@@ -0,0 +1,28 @@
+/* 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/. */
+
+//! Gecko's C++ bindings, along with some rust helpers to ease its use.
+
+// FIXME: We allow `improper_ctypes` (for now), because the lint doesn't allow
+// foreign structs to have `PhantomData`. We should remove this once the lint
+// ignores this case.
+
+#[allow(
+ dead_code,
+ improper_ctypes,
+ non_camel_case_types,
+ non_snake_case,
+ non_upper_case_globals,
+ missing_docs
+)]
+// TODO: Remove this when updating bindgen, see
+// https://github.com/rust-lang/rust-bindgen/issues/1651
+#[cfg_attr(test, allow(deref_nullptr))]
+pub mod structs {
+ include!(concat!(env!("OUT_DIR"), "/gecko/structs.rs"));
+}
+
+pub use self::structs as bindings;
+
+pub mod sugar;
diff --git a/servo/components/style/gecko_bindings/sugar/mod.rs b/servo/components/style/gecko_bindings/sugar/mod.rs
new file mode 100644
index 0000000000..00faf63ba6
--- /dev/null
+++ b/servo/components/style/gecko_bindings/sugar/mod.rs
@@ -0,0 +1,13 @@
+/* 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/. */
+
+//! Rust sugar and convenience methods for Gecko types.
+
+mod ns_com_ptr;
+mod ns_compatibility;
+mod ns_style_auto_array;
+mod ns_t_array;
+pub mod origin_flags;
+pub mod ownership;
+pub mod refptr;
diff --git a/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs
new file mode 100644
index 0000000000..1c54541bd8
--- /dev/null
+++ b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs
@@ -0,0 +1,15 @@
+/* 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/. */
+
+//! Little helpers for `nsCOMPtr`.
+
+use crate::gecko_bindings::structs::nsCOMPtr;
+
+impl<T> nsCOMPtr<T> {
+ /// Get this pointer as a raw pointer.
+ #[inline]
+ pub fn raw(&self) -> *mut T {
+ self.mRawPtr
+ }
+}
diff --git a/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs b/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs
new file mode 100644
index 0000000000..f4b81e9f79
--- /dev/null
+++ b/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs
@@ -0,0 +1,19 @@
+/* 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/. */
+
+//! Little helper for `nsCompatibility`.
+
+use crate::context::QuirksMode;
+use crate::gecko_bindings::structs::nsCompatibility;
+
+impl From<nsCompatibility> for QuirksMode {
+ #[inline]
+ fn from(mode: nsCompatibility) -> QuirksMode {
+ match mode {
+ nsCompatibility::eCompatibility_FullStandards => QuirksMode::NoQuirks,
+ nsCompatibility::eCompatibility_AlmostStandards => QuirksMode::LimitedQuirks,
+ nsCompatibility::eCompatibility_NavQuirks => QuirksMode::Quirks,
+ }
+ }
+}
diff --git a/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs
new file mode 100644
index 0000000000..b5772a6c77
--- /dev/null
+++ b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs
@@ -0,0 +1,111 @@
+/* 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/. */
+
+//! Rust helpers for Gecko's `nsStyleAutoArray`.
+
+use crate::gecko_bindings::bindings::Gecko_EnsureStyleAnimationArrayLength;
+use crate::gecko_bindings::bindings::Gecko_EnsureStyleScrollTimelineArrayLength;
+use crate::gecko_bindings::bindings::Gecko_EnsureStyleTransitionArrayLength;
+use crate::gecko_bindings::bindings::Gecko_EnsureStyleViewTimelineArrayLength;
+use crate::gecko_bindings::structs::nsStyleAutoArray;
+use crate::gecko_bindings::structs::{StyleAnimation, StyleTransition};
+use crate::gecko_bindings::structs::{StyleScrollTimeline, StyleViewTimeline};
+use std::iter::{once, Chain, IntoIterator, Once};
+use std::ops::{Index, IndexMut};
+use std::slice::{Iter, IterMut};
+
+impl<T> Index<usize> for nsStyleAutoArray<T> {
+ type Output = T;
+ fn index(&self, index: usize) -> &T {
+ match index {
+ 0 => &self.mFirstElement,
+ _ => &self.mOtherElements[index - 1],
+ }
+ }
+}
+
+impl<T> IndexMut<usize> for nsStyleAutoArray<T> {
+ fn index_mut(&mut self, index: usize) -> &mut T {
+ match index {
+ 0 => &mut self.mFirstElement,
+ _ => &mut self.mOtherElements[index - 1],
+ }
+ }
+}
+
+impl<T> nsStyleAutoArray<T> {
+ /// Mutably iterate over the array elements.
+ pub fn iter_mut(&mut self) -> Chain<Once<&mut T>, IterMut<T>> {
+ once(&mut self.mFirstElement).chain(self.mOtherElements.iter_mut())
+ }
+
+ /// Iterate over the array elements.
+ pub fn iter(&self) -> Chain<Once<&T>, Iter<T>> {
+ once(&self.mFirstElement).chain(self.mOtherElements.iter())
+ }
+
+ /// Returns the length of the array.
+ ///
+ /// Note that often structs containing autoarrays will have additional
+ /// member fields that contain the length, which must be kept in sync.
+ pub fn len(&self) -> usize {
+ 1 + self.mOtherElements.len()
+ }
+}
+
+impl nsStyleAutoArray<StyleAnimation> {
+ /// Ensures that the array has length at least the given length.
+ pub fn ensure_len(&mut self, len: usize) {
+ unsafe {
+ Gecko_EnsureStyleAnimationArrayLength(
+ self as *mut nsStyleAutoArray<StyleAnimation> as *mut _,
+ len,
+ );
+ }
+ }
+}
+
+impl nsStyleAutoArray<StyleTransition> {
+ /// Ensures that the array has length at least the given length.
+ pub fn ensure_len(&mut self, len: usize) {
+ unsafe {
+ Gecko_EnsureStyleTransitionArrayLength(
+ self as *mut nsStyleAutoArray<StyleTransition> as *mut _,
+ len,
+ );
+ }
+ }
+}
+
+impl nsStyleAutoArray<StyleViewTimeline> {
+ /// Ensures that the array has length at least the given length.
+ pub fn ensure_len(&mut self, len: usize) {
+ unsafe {
+ Gecko_EnsureStyleViewTimelineArrayLength(
+ self as *mut nsStyleAutoArray<StyleViewTimeline> as *mut _,
+ len,
+ );
+ }
+ }
+}
+
+impl nsStyleAutoArray<StyleScrollTimeline> {
+ /// Ensures that the array has length at least the given length.
+ pub fn ensure_len(&mut self, len: usize) {
+ unsafe {
+ Gecko_EnsureStyleScrollTimelineArrayLength(
+ self as *mut nsStyleAutoArray<StyleScrollTimeline> as *mut _,
+ len,
+ );
+ }
+ }
+}
+
+impl<'a, T> IntoIterator for &'a mut nsStyleAutoArray<T> {
+ type Item = &'a mut T;
+ type IntoIter = Chain<Once<&'a mut T>, IterMut<'a, T>>;
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter_mut()
+ }
+}
diff --git a/servo/components/style/gecko_bindings/sugar/ns_t_array.rs b/servo/components/style/gecko_bindings/sugar/ns_t_array.rs
new file mode 100644
index 0000000000..d10ed420dd
--- /dev/null
+++ b/servo/components/style/gecko_bindings/sugar/ns_t_array.rs
@@ -0,0 +1,144 @@
+/* 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/. */
+
+//! Rust helpers for Gecko's nsTArray.
+
+use crate::gecko_bindings::bindings;
+use crate::gecko_bindings::structs::{nsTArray, nsTArrayHeader, CopyableTArray};
+use std::mem;
+use std::ops::{Deref, DerefMut};
+use std::slice;
+
+impl<T> Deref for nsTArray<T> {
+ type Target = [T];
+
+ #[inline]
+ fn deref<'a>(&'a self) -> &'a [T] {
+ unsafe { slice::from_raw_parts(self.slice_begin(), self.header().mLength as usize) }
+ }
+}
+
+impl<T> DerefMut for nsTArray<T> {
+ fn deref_mut<'a>(&'a mut self) -> &'a mut [T] {
+ unsafe { slice::from_raw_parts_mut(self.slice_begin(), self.header().mLength as usize) }
+ }
+}
+
+impl<T> nsTArray<T> {
+ #[inline]
+ fn header<'a>(&'a self) -> &'a nsTArrayHeader {
+ debug_assert!(!self.mBuffer.is_null());
+ unsafe { mem::transmute(self.mBuffer) }
+ }
+
+ // unsafe, since header may be in shared static or something
+ unsafe fn header_mut<'a>(&'a mut self) -> &'a mut nsTArrayHeader {
+ debug_assert!(!self.mBuffer.is_null());
+
+ mem::transmute(self.mBuffer)
+ }
+
+ #[inline]
+ unsafe fn slice_begin(&self) -> *mut T {
+ debug_assert!(!self.mBuffer.is_null());
+ (self.mBuffer as *const nsTArrayHeader).offset(1) as *mut _
+ }
+
+ /// Ensures the array has enough capacity at least to hold `cap` elements.
+ ///
+ /// NOTE: This doesn't call the constructor on the values!
+ pub fn ensure_capacity(&mut self, cap: usize) {
+ if cap >= self.len() {
+ unsafe {
+ bindings::Gecko_EnsureTArrayCapacity(
+ self as *mut nsTArray<T> as *mut _,
+ cap,
+ mem::size_of::<T>(),
+ )
+ }
+ }
+ }
+
+ /// Clears the array storage without calling the destructor on the values.
+ #[inline]
+ pub unsafe fn clear(&mut self) {
+ if self.len() != 0 {
+ bindings::Gecko_ClearPODTArray(
+ self as *mut nsTArray<T> as *mut _,
+ mem::size_of::<T>(),
+ mem::align_of::<T>(),
+ );
+ }
+ }
+
+ /// Clears a POD array. This is safe since copy types are memcopyable.
+ #[inline]
+ pub fn clear_pod(&mut self)
+ where
+ T: Copy,
+ {
+ unsafe { self.clear() }
+ }
+
+ /// Resize and set the length of the array to `len`.
+ ///
+ /// unsafe because this may leave the array with uninitialized elements.
+ ///
+ /// This will not call constructors. If you need that, either manually add
+ /// bindings or run the typed `EnsureCapacity` call on the gecko side.
+ pub unsafe fn set_len(&mut self, len: u32) {
+ // this can leak
+ debug_assert!(len >= self.len() as u32);
+ if self.len() == len as usize {
+ return;
+ }
+ self.ensure_capacity(len as usize);
+ self.header_mut().mLength = len;
+ }
+
+ /// Resizes an array containing only POD elements
+ ///
+ /// unsafe because this may leave the array with uninitialized elements.
+ ///
+ /// This will not leak since it only works on POD types (and thus doesn't assert)
+ pub unsafe fn set_len_pod(&mut self, len: u32)
+ where
+ T: Copy,
+ {
+ if self.len() == len as usize {
+ return;
+ }
+ self.ensure_capacity(len as usize);
+ let header = self.header_mut();
+ header.mLength = len;
+ }
+
+ /// Collects the given iterator into this array.
+ ///
+ /// Not unsafe because we won't leave uninitialized elements in the array.
+ pub fn assign_from_iter_pod<I>(&mut self, iter: I)
+ where
+ T: Copy,
+ I: ExactSizeIterator + Iterator<Item = T>,
+ {
+ debug_assert!(iter.len() <= 0xFFFFFFFF);
+ unsafe {
+ self.set_len_pod(iter.len() as u32);
+ }
+ self.iter_mut().zip(iter).for_each(|(r, v)| *r = v);
+ }
+}
+
+impl<T> Deref for CopyableTArray<T> {
+ type Target = nsTArray<T>;
+ fn deref(&self) -> &Self::Target {
+ &self._base
+ }
+}
+
+impl<T> DerefMut for CopyableTArray<T> {
+ fn deref_mut(&mut self) -> &mut nsTArray<T> {
+ &mut self._base
+ }
+}
diff --git a/servo/components/style/gecko_bindings/sugar/origin_flags.rs b/servo/components/style/gecko_bindings/sugar/origin_flags.rs
new file mode 100644
index 0000000000..b27060405a
--- /dev/null
+++ b/servo/components/style/gecko_bindings/sugar/origin_flags.rs
@@ -0,0 +1,31 @@
+/* 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/. */
+
+//! Helper to iterate over `OriginFlags` bits.
+
+use crate::gecko_bindings::structs::OriginFlags;
+use crate::stylesheets::OriginSet;
+
+/// Checks that the values for OriginFlags are the ones we expect.
+pub fn assert_flags_match() {
+ use crate::stylesheets::origin::*;
+ debug_assert_eq!(
+ OriginFlags::UserAgent.0,
+ OriginSet::ORIGIN_USER_AGENT.bits()
+ );
+ debug_assert_eq!(OriginFlags::Author.0, OriginSet::ORIGIN_AUTHOR.bits());
+ debug_assert_eq!(OriginFlags::User.0, OriginSet::ORIGIN_USER.bits());
+}
+
+impl From<OriginFlags> for OriginSet {
+ fn from(flags: OriginFlags) -> Self {
+ Self::from_bits_retain(flags.0)
+ }
+}
+
+impl From<OriginSet> for OriginFlags {
+ fn from(set: OriginSet) -> Self {
+ OriginFlags(set.bits())
+ }
+}
diff --git a/servo/components/style/gecko_bindings/sugar/ownership.rs b/servo/components/style/gecko_bindings/sugar/ownership.rs
new file mode 100644
index 0000000000..31b512cf1e
--- /dev/null
+++ b/servo/components/style/gecko_bindings/sugar/ownership.rs
@@ -0,0 +1,61 @@
+/* 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/. */
+
+//! Helpers for different FFI pointer kinds that Gecko's FFI layer uses.
+
+use crate::gecko_bindings::structs::root::mozilla::detail::CopyablePtr;
+use servo_arc::Arc;
+use std::marker::PhantomData;
+use std::ops::{Deref, DerefMut};
+use std::ptr;
+
+/// Gecko-FFI-safe Arc (T is an ArcInner).
+///
+/// This can be null.
+///
+/// Leaks on drop. Please don't drop this.
+#[repr(C)]
+pub struct Strong<GeckoType> {
+ ptr: *const GeckoType,
+ _marker: PhantomData<GeckoType>,
+}
+
+impl<T> From<Arc<T>> for Strong<T> {
+ fn from(arc: Arc<T>) -> Self {
+ Self {
+ ptr: Arc::into_raw(arc),
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<GeckoType> Strong<GeckoType> {
+ #[inline]
+ /// Returns whether this reference is null.
+ pub fn is_null(&self) -> bool {
+ self.ptr.is_null()
+ }
+
+ #[inline]
+ /// Returns a null pointer
+ pub fn null() -> Self {
+ Self {
+ ptr: ptr::null(),
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<T> Deref for CopyablePtr<T> {
+ type Target = T;
+ fn deref(&self) -> &Self::Target {
+ &self.mPtr
+ }
+}
+
+impl<T> DerefMut for CopyablePtr<T> {
+ fn deref_mut<'a>(&'a mut self) -> &'a mut T {
+ &mut self.mPtr
+ }
+}
diff --git a/servo/components/style/gecko_bindings/sugar/refptr.rs b/servo/components/style/gecko_bindings/sugar/refptr.rs
new file mode 100644
index 0000000000..c4a0479a07
--- /dev/null
+++ b/servo/components/style/gecko_bindings/sugar/refptr.rs
@@ -0,0 +1,289 @@
+/* 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/. */
+
+//! A rust helper to ease the use of Gecko's refcounted types.
+
+use crate::gecko_bindings::{bindings, structs};
+use crate::Atom;
+use servo_arc::Arc;
+use std::fmt::Write;
+use std::marker::PhantomData;
+use std::ops::Deref;
+use std::{fmt, mem, ptr};
+
+/// Trait for all objects that have Addref() and Release
+/// methods and can be placed inside RefPtr<T>
+pub unsafe trait RefCounted {
+ /// Bump the reference count.
+ fn addref(&self);
+ /// Decrease the reference count.
+ unsafe fn release(&self);
+}
+
+/// Trait for types which can be shared across threads in RefPtr.
+pub unsafe trait ThreadSafeRefCounted: RefCounted {}
+
+/// A custom RefPtr implementation to take into account Drop semantics and
+/// a bit less-painful memory management.
+pub struct RefPtr<T: RefCounted> {
+ ptr: *mut T,
+ _marker: PhantomData<T>,
+}
+
+impl<T: RefCounted> fmt::Debug for RefPtr<T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("RefPtr { ")?;
+ self.ptr.fmt(f)?;
+ f.write_char('}')
+ }
+}
+
+impl<T: RefCounted> RefPtr<T> {
+ /// Create a new RefPtr from an already addrefed pointer obtained from FFI.
+ ///
+ /// The pointer must be valid, non-null and have been addrefed.
+ pub unsafe fn from_addrefed(ptr: *mut T) -> Self {
+ debug_assert!(!ptr.is_null());
+ RefPtr {
+ ptr,
+ _marker: PhantomData,
+ }
+ }
+
+ /// Returns whether the current pointer is null.
+ pub fn is_null(&self) -> bool {
+ self.ptr.is_null()
+ }
+
+ /// Returns a null pointer.
+ pub fn null() -> Self {
+ Self {
+ ptr: ptr::null_mut(),
+ _marker: PhantomData,
+ }
+ }
+
+ /// Create a new RefPtr from a pointer obtained from FFI.
+ ///
+ /// This method calls addref() internally
+ pub unsafe fn new(ptr: *mut T) -> Self {
+ let ret = RefPtr {
+ ptr,
+ _marker: PhantomData,
+ };
+ ret.addref();
+ ret
+ }
+
+ /// Produces an FFI-compatible RefPtr that can be stored in style structs.
+ ///
+ /// structs::RefPtr does not have a destructor, so this may leak
+ pub fn forget(self) -> structs::RefPtr<T> {
+ let ret = structs::RefPtr {
+ mRawPtr: self.ptr,
+ _phantom_0: PhantomData,
+ };
+ mem::forget(self);
+ ret
+ }
+
+ /// Returns the raw inner pointer to be fed back into FFI.
+ pub fn get(&self) -> *mut T {
+ self.ptr
+ }
+
+ /// Addref the inner data, obviously leaky on its own.
+ pub fn addref(&self) {
+ if !self.ptr.is_null() {
+ unsafe {
+ (*self.ptr).addref();
+ }
+ }
+ }
+
+ /// Release the inner data.
+ ///
+ /// Call only when the data actually needs releasing.
+ pub unsafe fn release(&self) {
+ if !self.ptr.is_null() {
+ (*self.ptr).release();
+ }
+ }
+}
+
+impl<T: RefCounted> Deref for RefPtr<T> {
+ type Target = T;
+ fn deref(&self) -> &T {
+ debug_assert!(!self.ptr.is_null());
+ unsafe { &*self.ptr }
+ }
+}
+
+impl<T: RefCounted> structs::RefPtr<T> {
+ /// Produces a Rust-side RefPtr from an FFI RefPtr, bumping the refcount.
+ ///
+ /// Must be called on a valid, non-null structs::RefPtr<T>.
+ pub unsafe fn to_safe(&self) -> RefPtr<T> {
+ let r = RefPtr {
+ ptr: self.mRawPtr,
+ _marker: PhantomData,
+ };
+ r.addref();
+ r
+ }
+ /// Produces a Rust-side RefPtr, consuming the existing one (and not bumping
+ /// the refcount).
+ pub unsafe fn into_safe(self) -> RefPtr<T> {
+ debug_assert!(!self.mRawPtr.is_null());
+ RefPtr {
+ ptr: self.mRawPtr,
+ _marker: PhantomData,
+ }
+ }
+
+ /// Replace a structs::RefPtr<T> with a different one, appropriately
+ /// addref/releasing.
+ ///
+ /// Both `self` and `other` must be valid, but can be null.
+ ///
+ /// Safe when called on an aliased pointer because the refcount in that case
+ /// needs to be at least two.
+ pub unsafe fn set(&mut self, other: &Self) {
+ self.clear();
+ if !other.mRawPtr.is_null() {
+ *self = other.to_safe().forget();
+ }
+ }
+
+ /// Clear an instance of the structs::RefPtr<T>, by releasing
+ /// it and setting its contents to null.
+ ///
+ /// `self` must be valid, but can be null.
+ pub unsafe fn clear(&mut self) {
+ if !self.mRawPtr.is_null() {
+ (*self.mRawPtr).release();
+ self.mRawPtr = ptr::null_mut();
+ }
+ }
+
+ /// Replace a `structs::RefPtr<T>` with a `RefPtr<T>`,
+ /// consuming the `RefPtr<T>`, and releasing the old
+ /// value in `self` if necessary.
+ ///
+ /// `self` must be valid, possibly null.
+ pub fn set_move(&mut self, other: RefPtr<T>) {
+ if !self.mRawPtr.is_null() {
+ unsafe {
+ (*self.mRawPtr).release();
+ }
+ }
+ *self = other.forget();
+ }
+}
+
+impl<T> structs::RefPtr<T> {
+ /// Returns a new, null refptr.
+ pub fn null() -> Self {
+ Self {
+ mRawPtr: ptr::null_mut(),
+ _phantom_0: PhantomData,
+ }
+ }
+
+ /// Create a new RefPtr from an arc.
+ pub fn from_arc(s: Arc<T>) -> Self {
+ Self {
+ mRawPtr: Arc::into_raw(s) as *mut _,
+ _phantom_0: PhantomData,
+ }
+ }
+
+ /// Sets the contents to an Arc<T>.
+ pub fn set_arc(&mut self, other: Arc<T>) {
+ unsafe {
+ if !self.mRawPtr.is_null() {
+ let _ = Arc::from_raw(self.mRawPtr);
+ }
+ self.mRawPtr = Arc::into_raw(other) as *mut _;
+ }
+ }
+}
+
+impl<T: RefCounted> Drop for RefPtr<T> {
+ fn drop(&mut self) {
+ unsafe { self.release() }
+ }
+}
+
+impl<T: RefCounted> Clone for RefPtr<T> {
+ fn clone(&self) -> Self {
+ self.addref();
+ RefPtr {
+ ptr: self.ptr,
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<T: RefCounted> PartialEq for RefPtr<T> {
+ fn eq(&self, other: &Self) -> bool {
+ self.ptr == other.ptr
+ }
+}
+
+unsafe impl<T: ThreadSafeRefCounted> Send for RefPtr<T> {}
+unsafe impl<T: ThreadSafeRefCounted> Sync for RefPtr<T> {}
+
+macro_rules! impl_refcount {
+ ($t:ty, $addref:path, $release:path) => {
+ unsafe impl RefCounted for $t {
+ #[inline]
+ fn addref(&self) {
+ unsafe { $addref(self as *const _ as *mut _) }
+ }
+
+ #[inline]
+ unsafe fn release(&self) {
+ $release(self as *const _ as *mut _)
+ }
+ }
+ };
+}
+
+// Companion of NS_DECL_THREADSAFE_FFI_REFCOUNTING.
+//
+// Gets you a free RefCounted impl implemented via FFI.
+macro_rules! impl_threadsafe_refcount {
+ ($t:ty, $addref:path, $release:path) => {
+ impl_refcount!($t, $addref, $release);
+ unsafe impl ThreadSafeRefCounted for $t {}
+ };
+}
+
+impl_threadsafe_refcount!(
+ structs::mozilla::URLExtraData,
+ bindings::Gecko_AddRefURLExtraDataArbitraryThread,
+ bindings::Gecko_ReleaseURLExtraDataArbitraryThread
+);
+impl_threadsafe_refcount!(
+ structs::nsIURI,
+ bindings::Gecko_AddRefnsIURIArbitraryThread,
+ bindings::Gecko_ReleasensIURIArbitraryThread
+);
+impl_threadsafe_refcount!(
+ structs::SheetLoadDataHolder,
+ bindings::Gecko_AddRefSheetLoadDataHolderArbitraryThread,
+ bindings::Gecko_ReleaseSheetLoadDataHolderArbitraryThread
+);
+
+#[inline]
+unsafe fn addref_atom(atom: *mut structs::nsAtom) {
+ mem::forget(Atom::from_raw(atom));
+}
+
+#[inline]
+unsafe fn release_atom(atom: *mut structs::nsAtom) {
+ let _ = Atom::from_addrefed(atom);
+}
+impl_threadsafe_refcount!(structs::nsAtom, addref_atom, release_atom);
diff --git a/servo/components/style/gecko_string_cache/mod.rs b/servo/components/style/gecko_string_cache/mod.rs
new file mode 100644
index 0000000000..79a5d46525
--- /dev/null
+++ b/servo/components/style/gecko_string_cache/mod.rs
@@ -0,0 +1,497 @@
+/* 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/. */
+
+#![allow(unsafe_code)]
+// This is needed for the constants in atom_macro.rs, because we have some
+// atoms whose names differ only by case, e.g. datetime and dateTime.
+#![allow(non_upper_case_globals)]
+
+//! A drop-in replacement for string_cache, but backed by Gecko `nsAtom`s.
+
+use crate::gecko_bindings::bindings::Gecko_AddRefAtom;
+use crate::gecko_bindings::bindings::Gecko_Atomize;
+use crate::gecko_bindings::bindings::Gecko_Atomize16;
+use crate::gecko_bindings::bindings::Gecko_ReleaseAtom;
+use crate::gecko_bindings::structs::root::mozilla::detail::gGkAtoms;
+use crate::gecko_bindings::structs::root::mozilla::detail::GkAtoms_Atoms_AtomsCount;
+use crate::gecko_bindings::structs::{nsAtom, nsDynamicAtom, nsStaticAtom};
+use nsstring::{nsAString, nsStr};
+use precomputed_hash::PrecomputedHash;
+use std::borrow::{Borrow, Cow};
+use std::char::{self, DecodeUtf16};
+use std::fmt::{self, Write};
+use std::hash::{Hash, Hasher};
+use std::iter::Cloned;
+use std::mem::{self, ManuallyDrop};
+use std::num::NonZeroUsize;
+use std::ops::Deref;
+use std::{slice, str};
+use style_traits::SpecifiedValueInfo;
+use to_shmem::{self, SharedMemoryBuilder, ToShmem};
+
+#[macro_use]
+#[allow(improper_ctypes, non_camel_case_types, missing_docs)]
+pub mod atom_macro {
+ include!(concat!(env!("OUT_DIR"), "/gecko/atom_macro.rs"));
+}
+
+#[macro_use]
+pub mod namespace;
+
+pub use self::namespace::{Namespace, WeakNamespace};
+
+/// A handle to a Gecko atom. This is a type that can represent either:
+///
+/// * A strong reference to a dynamic atom (an `nsAtom` pointer), in which case
+/// the `usize` just holds the pointer value.
+///
+/// * An index from `gGkAtoms` to the `nsStaticAtom` object (shifted to the left one bit, and with
+/// the lower bit set to `1` to differentiate it from the above), so `(index << 1 | 1)`.
+///
+#[derive(Eq, PartialEq)]
+#[repr(C)]
+pub struct Atom(NonZeroUsize);
+
+/// An atom *without* a strong reference.
+///
+/// Only usable as `&'a WeakAtom`,
+/// where `'a` is the lifetime of something that holds a strong reference to that atom.
+pub struct WeakAtom(nsAtom);
+
+/// The number of static atoms we have.
+const STATIC_ATOM_COUNT: usize = GkAtoms_Atoms_AtomsCount as usize;
+
+impl Deref for Atom {
+ type Target = WeakAtom;
+
+ #[inline]
+ fn deref(&self) -> &WeakAtom {
+ unsafe {
+ let addr = if self.is_static() {
+ // This is really hot.
+ &gGkAtoms.mAtoms.get_unchecked(self.0.get() >> 1)._base as *const nsAtom
+ } else {
+ self.0.get() as *const nsAtom
+ };
+ WeakAtom::new(addr as *const nsAtom)
+ }
+ }
+}
+
+impl PrecomputedHash for Atom {
+ #[inline]
+ fn precomputed_hash(&self) -> u32 {
+ self.get_hash()
+ }
+}
+
+impl Borrow<WeakAtom> for Atom {
+ #[inline]
+ fn borrow(&self) -> &WeakAtom {
+ self
+ }
+}
+
+impl ToShmem for Atom {
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ if !self.is_static() {
+ return Err(format!(
+ "ToShmem failed for Atom: must be a static atom: {}",
+ self
+ ));
+ }
+
+ Ok(ManuallyDrop::new(Atom(self.0)))
+ }
+}
+
+impl Eq for WeakAtom {}
+impl PartialEq for WeakAtom {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ let weak: *const WeakAtom = self;
+ let other: *const WeakAtom = other;
+ weak == other
+ }
+}
+
+impl PartialEq<Atom> for WeakAtom {
+ #[inline]
+ fn eq(&self, other: &Atom) -> bool {
+ self == &**other
+ }
+}
+
+unsafe impl Send for Atom {}
+unsafe impl Sync for Atom {}
+unsafe impl Sync for WeakAtom {}
+
+impl WeakAtom {
+ /// Construct a `WeakAtom` from a raw `nsAtom`.
+ #[inline]
+ pub unsafe fn new<'a>(atom: *const nsAtom) -> &'a mut Self {
+ &mut *(atom as *mut WeakAtom)
+ }
+
+ /// Clone this atom, bumping the refcount if the atom is not static.
+ #[inline]
+ pub fn clone(&self) -> Atom {
+ unsafe { Atom::from_raw(self.as_ptr()) }
+ }
+
+ /// Get the atom hash.
+ #[inline]
+ pub fn get_hash(&self) -> u32 {
+ self.0.mHash
+ }
+
+ /// Get the atom as a slice of utf-16 chars.
+ #[inline]
+ pub fn as_slice(&self) -> &[u16] {
+ let string = if self.is_static() {
+ let atom_ptr = self.as_ptr() as *const nsStaticAtom;
+ let string_offset = unsafe { (*atom_ptr).mStringOffset };
+ let string_offset = -(string_offset as isize);
+ let u8_ptr = atom_ptr as *const u8;
+ // It is safe to use offset() here because both addresses are within
+ // the same struct, e.g. mozilla::detail::gGkAtoms.
+ unsafe { u8_ptr.offset(string_offset) as *const u16 }
+ } else {
+ let atom_ptr = self.as_ptr() as *const nsDynamicAtom;
+ let buffer_ptr = unsafe { (*atom_ptr).mStringBuffer.mRawPtr };
+ // Dynamic atom chars are stored at the end of the string buffer.
+ unsafe { buffer_ptr.offset(1) as *const u16 }
+ };
+ unsafe { slice::from_raw_parts(string, self.len() as usize) }
+ }
+
+ // NOTE: don't expose this, since it's slow, and easy to be misused.
+ fn chars(&self) -> DecodeUtf16<Cloned<slice::Iter<u16>>> {
+ char::decode_utf16(self.as_slice().iter().cloned())
+ }
+
+ /// Execute `cb` with the string that this atom represents.
+ ///
+ /// Find alternatives to this function when possible, please, since it's
+ /// pretty slow.
+ pub fn with_str<F, Output>(&self, cb: F) -> Output
+ where
+ F: FnOnce(&str) -> Output,
+ {
+ let mut buffer = mem::MaybeUninit::<[u8; 64]>::uninit();
+ let buffer = unsafe { &mut *buffer.as_mut_ptr() };
+
+ // The total string length in utf16 is going to be less than or equal
+ // the slice length (each utf16 character is going to take at least one
+ // and at most 2 items in the utf16 slice).
+ //
+ // Each of those characters will take at most four bytes in the utf8
+ // one. Thus if the slice is less than 64 / 4 (16) we can guarantee that
+ // we'll decode it in place.
+ let owned_string;
+ let len = self.len();
+ let utf8_slice = if len <= 16 {
+ let mut total_len = 0;
+
+ for c in self.chars() {
+ let c = c.unwrap_or(char::REPLACEMENT_CHARACTER);
+ let utf8_len = c.encode_utf8(&mut buffer[total_len..]).len();
+ total_len += utf8_len;
+ }
+
+ let slice = unsafe { str::from_utf8_unchecked(&buffer[..total_len]) };
+ debug_assert_eq!(slice, String::from_utf16_lossy(self.as_slice()));
+ slice
+ } else {
+ owned_string = String::from_utf16_lossy(self.as_slice());
+ &*owned_string
+ };
+
+ cb(utf8_slice)
+ }
+
+ /// Returns whether this atom is static.
+ #[inline]
+ pub fn is_static(&self) -> bool {
+ self.0.mIsStatic() != 0
+ }
+
+ /// Returns whether this atom is ascii lowercase.
+ #[inline]
+ fn is_ascii_lowercase(&self) -> bool {
+ self.0.mIsAsciiLowercase() != 0
+ }
+
+ /// Returns the length of the atom string.
+ #[inline]
+ pub fn len(&self) -> u32 {
+ self.0.mLength()
+ }
+
+ /// Returns whether this atom is the empty string.
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Returns the atom as a mutable pointer.
+ #[inline]
+ pub fn as_ptr(&self) -> *mut nsAtom {
+ let const_ptr: *const nsAtom = &self.0;
+ const_ptr as *mut nsAtom
+ }
+
+ /// Convert this atom to ASCII lower-case
+ pub fn to_ascii_lowercase(&self) -> Atom {
+ if self.is_ascii_lowercase() {
+ return self.clone();
+ }
+
+ let slice = self.as_slice();
+ let mut buffer = mem::MaybeUninit::<[u16; 64]>::uninit();
+ let buffer = unsafe { &mut *buffer.as_mut_ptr() };
+ let mut vec;
+ let mutable_slice = if let Some(buffer_prefix) = buffer.get_mut(..slice.len()) {
+ buffer_prefix.copy_from_slice(slice);
+ buffer_prefix
+ } else {
+ vec = slice.to_vec();
+ &mut vec
+ };
+ for char16 in &mut *mutable_slice {
+ if *char16 <= 0x7F {
+ *char16 = (*char16 as u8).to_ascii_lowercase() as u16
+ }
+ }
+ Atom::from(&*mutable_slice)
+ }
+
+ /// Return whether two atoms are ASCII-case-insensitive matches
+ #[inline]
+ pub fn eq_ignore_ascii_case(&self, other: &Self) -> bool {
+ if self == other {
+ return true;
+ }
+
+ // If we know both atoms are ascii-lowercase, then we can stick with
+ // pointer equality.
+ if self.is_ascii_lowercase() && other.is_ascii_lowercase() {
+ debug_assert!(!self.eq_ignore_ascii_case_slow(other));
+ return false;
+ }
+
+ self.eq_ignore_ascii_case_slow(other)
+ }
+
+ fn eq_ignore_ascii_case_slow(&self, other: &Self) -> bool {
+ let a = self.as_slice();
+ let b = other.as_slice();
+
+ if a.len() != b.len() {
+ return false;
+ }
+
+ a.iter().zip(b).all(|(&a16, &b16)| {
+ if a16 <= 0x7F && b16 <= 0x7F {
+ (a16 as u8).eq_ignore_ascii_case(&(b16 as u8))
+ } else {
+ a16 == b16
+ }
+ })
+ }
+}
+
+impl fmt::Debug for WeakAtom {
+ fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
+ write!(w, "Gecko WeakAtom({:p}, {})", self, self)
+ }
+}
+
+impl fmt::Display for WeakAtom {
+ fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
+ for c in self.chars() {
+ w.write_char(c.unwrap_or(char::REPLACEMENT_CHARACTER))?
+ }
+ Ok(())
+ }
+}
+
+#[inline]
+unsafe fn make_handle(ptr: *const nsAtom) -> NonZeroUsize {
+ debug_assert!(!ptr.is_null());
+ if !WeakAtom::new(ptr).is_static() {
+ NonZeroUsize::new_unchecked(ptr as usize)
+ } else {
+ make_static_handle(ptr as *mut nsStaticAtom)
+ }
+}
+
+#[inline]
+unsafe fn make_static_handle(ptr: *const nsStaticAtom) -> NonZeroUsize {
+ let index = ptr.offset_from(&gGkAtoms.mAtoms[0] as *const _);
+ debug_assert!(index >= 0, "Should be a non-negative index");
+ debug_assert!(
+ (index as usize) < STATIC_ATOM_COUNT,
+ "Should be a valid static atom index"
+ );
+ NonZeroUsize::new_unchecked(((index as usize) << 1) | 1)
+}
+
+impl Atom {
+ #[inline]
+ fn is_static(&self) -> bool {
+ self.0.get() & 1 == 1
+ }
+
+ /// Execute a callback with the atom represented by `ptr`.
+ pub unsafe fn with<F, R>(ptr: *const nsAtom, callback: F) -> R
+ where
+ F: FnOnce(&Atom) -> R,
+ {
+ let atom = Atom(make_handle(ptr as *mut nsAtom));
+ let ret = callback(&atom);
+ mem::forget(atom);
+ ret
+ }
+
+ /// Creates a static atom from its index in the static atom table, without
+ /// checking.
+ #[inline]
+ pub const unsafe fn from_index_unchecked(index: u16) -> Self {
+ debug_assert!((index as usize) < STATIC_ATOM_COUNT);
+ Atom(NonZeroUsize::new_unchecked(((index as usize) << 1) | 1))
+ }
+
+ /// Creates an atom from an atom pointer.
+ #[inline(always)]
+ pub unsafe fn from_raw(ptr: *mut nsAtom) -> Self {
+ let atom = Atom(make_handle(ptr));
+ if !atom.is_static() {
+ Gecko_AddRefAtom(ptr);
+ }
+ atom
+ }
+
+ /// Creates an atom from an atom pointer that has already had AddRef
+ /// called on it. This may be a static or dynamic atom.
+ #[inline]
+ pub unsafe fn from_addrefed(ptr: *mut nsAtom) -> Self {
+ assert!(!ptr.is_null());
+ Atom(make_handle(ptr))
+ }
+
+ /// Convert this atom into an addrefed nsAtom pointer.
+ #[inline]
+ pub fn into_addrefed(self) -> *mut nsAtom {
+ let ptr = self.as_ptr();
+ mem::forget(self);
+ ptr
+ }
+}
+
+impl Hash for Atom {
+ fn hash<H>(&self, state: &mut H)
+ where
+ H: Hasher,
+ {
+ state.write_u32(self.get_hash());
+ }
+}
+
+impl Hash for WeakAtom {
+ fn hash<H>(&self, state: &mut H)
+ where
+ H: Hasher,
+ {
+ state.write_u32(self.get_hash());
+ }
+}
+
+impl Clone for Atom {
+ #[inline(always)]
+ fn clone(&self) -> Atom {
+ unsafe {
+ let atom = Atom(self.0);
+ if !atom.is_static() {
+ Gecko_AddRefAtom(atom.as_ptr());
+ }
+ atom
+ }
+ }
+}
+
+impl Drop for Atom {
+ #[inline]
+ fn drop(&mut self) {
+ if !self.is_static() {
+ unsafe {
+ Gecko_ReleaseAtom(self.as_ptr());
+ }
+ }
+ }
+}
+
+impl Default for Atom {
+ #[inline]
+ fn default() -> Self {
+ atom!("")
+ }
+}
+
+impl fmt::Debug for Atom {
+ fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
+ write!(w, "Atom(0x{:08x}, {})", self.0, self)
+ }
+}
+
+impl fmt::Display for Atom {
+ fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
+ self.deref().fmt(w)
+ }
+}
+
+impl<'a> From<&'a str> for Atom {
+ #[inline]
+ fn from(string: &str) -> Atom {
+ debug_assert!(string.len() <= u32::max_value() as usize);
+ unsafe {
+ Atom::from_addrefed(Gecko_Atomize(
+ string.as_ptr() as *const _,
+ string.len() as u32,
+ ))
+ }
+ }
+}
+
+impl<'a> From<&'a [u16]> for Atom {
+ #[inline]
+ fn from(slice: &[u16]) -> Atom {
+ Atom::from(&*nsStr::from(slice))
+ }
+}
+
+impl<'a> From<&'a nsAString> for Atom {
+ #[inline]
+ fn from(string: &nsAString) -> Atom {
+ unsafe { Atom::from_addrefed(Gecko_Atomize16(string)) }
+ }
+}
+
+impl<'a> From<Cow<'a, str>> for Atom {
+ #[inline]
+ fn from(string: Cow<'a, str>) -> Atom {
+ Atom::from(&*string)
+ }
+}
+
+impl From<String> for Atom {
+ #[inline]
+ fn from(string: String) -> Atom {
+ Atom::from(&*string)
+ }
+}
+
+malloc_size_of_is_0!(Atom);
+
+impl SpecifiedValueInfo for Atom {}
diff --git a/servo/components/style/gecko_string_cache/namespace.rs b/servo/components/style/gecko_string_cache/namespace.rs
new file mode 100644
index 0000000000..d9745b9e21
--- /dev/null
+++ b/servo/components/style/gecko_string_cache/namespace.rs
@@ -0,0 +1,105 @@
+/* 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/. */
+
+//! A type to represent a namespace.
+
+use crate::gecko_bindings::structs::nsAtom;
+use crate::string_cache::{Atom, WeakAtom};
+use precomputed_hash::PrecomputedHash;
+use std::borrow::Borrow;
+use std::fmt;
+use std::ops::Deref;
+
+/// In Gecko namespaces are just regular atoms, so this is a simple macro to
+/// forward one macro to the other.
+#[macro_export]
+macro_rules! ns {
+ () => {
+ $crate::string_cache::Namespace(atom!(""))
+ };
+ ($s:tt) => {
+ $crate::string_cache::Namespace(atom!($s))
+ };
+}
+
+/// A Gecko namespace is just a wrapped atom.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct Namespace(pub Atom);
+
+impl PrecomputedHash for Namespace {
+ #[inline]
+ fn precomputed_hash(&self) -> u32 {
+ self.0.precomputed_hash()
+ }
+}
+
+/// A Gecko WeakNamespace is a wrapped WeakAtom.
+#[derive(Deref, Hash)]
+pub struct WeakNamespace(WeakAtom);
+
+impl Deref for Namespace {
+ type Target = WeakNamespace;
+
+ #[inline]
+ fn deref(&self) -> &WeakNamespace {
+ let weak: *const WeakAtom = &*self.0;
+ unsafe { &*(weak as *const WeakNamespace) }
+ }
+}
+
+impl<'a> From<&'a str> for Namespace {
+ fn from(s: &'a str) -> Self {
+ Namespace(Atom::from(s))
+ }
+}
+
+impl fmt::Display for Namespace {
+ fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
+ self.0.fmt(w)
+ }
+}
+
+impl Borrow<WeakNamespace> for Namespace {
+ #[inline]
+ fn borrow(&self) -> &WeakNamespace {
+ self
+ }
+}
+
+impl WeakNamespace {
+ /// Trivially construct a WeakNamespace.
+ #[inline]
+ pub unsafe fn new<'a>(atom: *mut nsAtom) -> &'a Self {
+ &*(atom as *const WeakNamespace)
+ }
+
+ /// Clone this WeakNamespace to obtain a strong reference to the same
+ /// underlying namespace.
+ #[inline]
+ pub fn clone(&self) -> Namespace {
+ Namespace(self.0.clone())
+ }
+}
+
+impl Eq for WeakNamespace {}
+impl PartialEq for WeakNamespace {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ let weak: *const WeakNamespace = self;
+ let other: *const WeakNamespace = other;
+ weak == other
+ }
+}
diff --git a/servo/components/style/global_style_data.rs b/servo/components/style/global_style_data.rs
new file mode 100644
index 0000000000..38d72b2c74
--- /dev/null
+++ b/servo/components/style/global_style_data.rs
@@ -0,0 +1,212 @@
+/* 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/. */
+
+//! Global style data
+
+use crate::context::StyleSystemOptions;
+#[cfg(feature = "gecko")]
+use crate::gecko_bindings::bindings;
+use crate::parallel::STYLE_THREAD_STACK_SIZE_KB;
+use crate::shared_lock::SharedRwLock;
+use crate::thread_state;
+use gecko_profiler;
+use parking_lot::{Mutex, RwLock, RwLockReadGuard};
+use rayon;
+#[cfg(unix)]
+use std::os::unix::thread::{JoinHandleExt, RawPthread};
+#[cfg(windows)]
+use std::os::windows::{io::AsRawHandle, prelude::RawHandle};
+use std::{io, thread};
+use thin_vec::ThinVec;
+
+/// Platform-specific handle to a thread.
+#[cfg(unix)]
+pub type PlatformThreadHandle = RawPthread;
+/// Platform-specific handle to a thread.
+#[cfg(windows)]
+pub type PlatformThreadHandle = RawHandle;
+
+/// Global style data
+pub struct GlobalStyleData {
+ /// Shared RWLock for CSSOM objects
+ pub shared_lock: SharedRwLock,
+
+ /// Global style system options determined by env vars.
+ pub options: StyleSystemOptions,
+}
+
+/// Global thread pool.
+pub struct StyleThreadPool {
+ /// How many threads parallel styling can use. If not using a thread pool, this is set to `None`.
+ pub num_threads: Option<usize>,
+
+ /// The parallel styling thread pool.
+ ///
+ /// For leak-checking purposes, we want to terminate the thread-pool, which
+ /// waits for all the async jobs to complete. Thus the RwLock.
+ style_thread_pool: RwLock<Option<rayon::ThreadPool>>,
+}
+
+fn thread_name(index: usize) -> String {
+ format!("StyleThread#{}", index)
+}
+
+lazy_static! {
+ /// JoinHandles for spawned style threads. These will be joined during
+ /// StyleThreadPool::shutdown() after exiting the thread pool.
+ ///
+ /// This would be quite inefficient if rayon destroyed and re-created
+ /// threads regularly during threadpool operation in response to demand,
+ /// however rayon actually never destroys its threads until the entire
+ /// thread pool is shut-down, so the size of this list is bounded.
+ static ref STYLE_THREAD_JOIN_HANDLES: Mutex<Vec<thread::JoinHandle<()>>> =
+ Mutex::new(Vec::new());
+}
+
+fn thread_spawn(options: rayon::ThreadBuilder) -> io::Result<()> {
+ let mut b = thread::Builder::new();
+ if let Some(name) = options.name() {
+ b = b.name(name.to_owned());
+ }
+ if let Some(stack_size) = options.stack_size() {
+ b = b.stack_size(stack_size);
+ }
+ let join_handle = b.spawn(|| options.run())?;
+ STYLE_THREAD_JOIN_HANDLES.lock().push(join_handle);
+ Ok(())
+}
+
+fn thread_startup(_index: usize) {
+ thread_state::initialize_layout_worker_thread();
+ #[cfg(feature = "gecko")]
+ unsafe {
+ bindings::Gecko_SetJemallocThreadLocalArena(true);
+ let name = thread_name(_index);
+ gecko_profiler::register_thread(&name);
+ }
+}
+
+fn thread_shutdown(_: usize) {
+ #[cfg(feature = "gecko")]
+ unsafe {
+ gecko_profiler::unregister_thread();
+ bindings::Gecko_SetJemallocThreadLocalArena(false);
+ }
+}
+
+impl StyleThreadPool {
+ /// Shuts down the thread pool, waiting for all work to complete.
+ pub fn shutdown() {
+ if STYLE_THREAD_JOIN_HANDLES.lock().is_empty() {
+ return;
+ }
+ {
+ // Drop the pool.
+ let _ = STYLE_THREAD_POOL.style_thread_pool.write().take();
+ }
+
+ // Join spawned threads until all of the threads have been joined. This
+ // will usually be pretty fast, as on shutdown there should be basically
+ // no threads left running.
+ while let Some(join_handle) = STYLE_THREAD_JOIN_HANDLES.lock().pop() {
+ let _ = join_handle.join();
+ }
+ }
+
+ /// Returns a reference to the thread pool.
+ ///
+ /// We only really want to give read-only access to the pool, except
+ /// for shutdown().
+ pub fn pool(&self) -> RwLockReadGuard<Option<rayon::ThreadPool>> {
+ self.style_thread_pool.read()
+ }
+
+ /// Returns a list of the pool's platform-specific thread handles.
+ pub fn get_thread_handles(handles: &mut ThinVec<PlatformThreadHandle>) {
+ // Force the lazy initialization of STYLE_THREAD_POOL so that the threads get spawned and
+ // their join handles are added to STYLE_THREAD_JOIN_HANDLES.
+ lazy_static::initialize(&STYLE_THREAD_POOL);
+
+ for join_handle in STYLE_THREAD_JOIN_HANDLES.lock().iter() {
+ #[cfg(unix)]
+ let handle = join_handle.as_pthread_t();
+ #[cfg(windows)]
+ let handle = join_handle.as_raw_handle();
+
+ handles.push(handle);
+ }
+ }
+}
+
+#[cfg(feature = "servo")]
+fn stylo_threads_pref() -> i32 {
+ pref!(layout.threads)
+}
+
+#[cfg(feature = "gecko")]
+fn stylo_threads_pref() -> i32 {
+ static_prefs::pref!("layout.css.stylo-threads")
+}
+
+/// The performance benefit of additional threads seems to level off at around six, so we cap it
+/// there on many-core machines (see bug 1431285 comment 14).
+pub(crate) const STYLO_MAX_THREADS: usize = 6;
+
+lazy_static! {
+ /// Global thread pool
+ pub static ref STYLE_THREAD_POOL: StyleThreadPool = {
+ use std::cmp;
+ // We always set this pref on startup, before layout or script have had a chance of
+ // accessing (and thus creating) the thread-pool.
+ let threads_pref: i32 = stylo_threads_pref();
+ let num_threads = if threads_pref >= 0 {
+ threads_pref as usize
+ } else {
+ // Gecko may wish to override the default number of threads, for example on
+ // systems with heterogeneous CPUs.
+ #[cfg(feature = "gecko")]
+ let num_threads = unsafe { bindings::Gecko_GetNumStyleThreads() };
+ #[cfg(not(feature = "gecko"))]
+ let num_threads = -1;
+
+ if num_threads >= 0 {
+ num_threads as usize
+ } else {
+ use num_cpus;
+ // The default heuristic is num_virtual_cores * .75. This gives us three threads on a
+ // hyper-threaded dual core, and six threads on a hyper-threaded quad core.
+ cmp::max(num_cpus::get() * 3 / 4, 1)
+ }
+ };
+
+ let num_threads = cmp::min(num_threads, STYLO_MAX_THREADS);
+ // Since the main-thread is also part of the pool, having one thread or less doesn't make
+ // sense.
+ let (pool, num_threads) = if num_threads <= 1 {
+ (None, None)
+ } else {
+ let workers = rayon::ThreadPoolBuilder::new()
+ .spawn_handler(thread_spawn)
+ .use_current_thread()
+ .num_threads(num_threads)
+ .thread_name(thread_name)
+ .start_handler(thread_startup)
+ .exit_handler(thread_shutdown)
+ .stack_size(STYLE_THREAD_STACK_SIZE_KB * 1024)
+ .build();
+ (workers.ok(), Some(num_threads))
+ };
+
+ StyleThreadPool {
+ num_threads,
+ style_thread_pool: RwLock::new(pool),
+ }
+ };
+
+ /// Global style data
+ pub static ref GLOBAL_STYLE_DATA: GlobalStyleData = GlobalStyleData {
+ shared_lock: SharedRwLock::new_leaked(),
+ options: StyleSystemOptions::default(),
+ };
+}
diff --git a/servo/components/style/invalidation/element/document_state.rs b/servo/components/style/invalidation/element/document_state.rs
new file mode 100644
index 0000000000..0b846510d8
--- /dev/null
+++ b/servo/components/style/invalidation/element/document_state.rs
@@ -0,0 +1,154 @@
+/* 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/. */
+
+//! An invalidation processor for style changes due to document state changes.
+
+use crate::dom::TElement;
+use crate::invalidation::element::invalidation_map::Dependency;
+use crate::invalidation::element::invalidator::{
+ DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap,
+};
+use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor};
+use crate::invalidation::element::state_and_attributes;
+use crate::stylist::CascadeData;
+use dom::DocumentState;
+use selectors::matching::{
+ MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, QuirksMode,
+ SelectorCaches, VisitedHandlingMode,
+};
+
+/// A struct holding the members necessary to invalidate document state
+/// selectors.
+#[derive(Debug)]
+pub struct InvalidationMatchingData {
+ /// The document state that has changed, which makes it always match.
+ pub document_state: DocumentState,
+}
+
+impl Default for InvalidationMatchingData {
+ #[inline(always)]
+ fn default() -> Self {
+ Self {
+ document_state: DocumentState::empty(),
+ }
+ }
+}
+
+/// An invalidation processor for style changes due to state and attribute
+/// changes.
+pub struct DocumentStateInvalidationProcessor<'a, 'b, E: TElement, I> {
+ rules: I,
+ matching_context: MatchingContext<'a, E::Impl>,
+ traversal_map: SiblingTraversalMap<E>,
+ document_states_changed: DocumentState,
+ _marker: std::marker::PhantomData<&'b ()>,
+}
+
+impl<'a, 'b, E: TElement, I> DocumentStateInvalidationProcessor<'a, 'b, E, I> {
+ /// Creates a new DocumentStateInvalidationProcessor.
+ #[inline]
+ pub fn new(
+ rules: I,
+ document_states_changed: DocumentState,
+ selector_caches: &'a mut SelectorCaches,
+ quirks_mode: QuirksMode,
+ ) -> Self {
+ let mut matching_context = MatchingContext::<'a, E::Impl>::new_for_visited(
+ MatchingMode::Normal,
+ None,
+ selector_caches,
+ VisitedHandlingMode::AllLinksVisitedAndUnvisited,
+ quirks_mode,
+ NeedsSelectorFlags::No,
+ MatchingForInvalidation::No,
+ );
+
+ matching_context.extra_data.invalidation_data.document_state = document_states_changed;
+
+ Self {
+ rules,
+ document_states_changed,
+ matching_context,
+ traversal_map: SiblingTraversalMap::default(),
+ _marker: std::marker::PhantomData,
+ }
+ }
+}
+
+impl<'a, 'b, E, I> InvalidationProcessor<'b, 'a, E>
+ for DocumentStateInvalidationProcessor<'a, 'b, E, I>
+where
+ E: TElement,
+ I: Iterator<Item = &'b CascadeData>,
+{
+ fn check_outer_dependency(&mut self, _: &Dependency, _: E) -> bool {
+ debug_assert!(
+ false,
+ "how, we should only have parent-less dependencies here!"
+ );
+ true
+ }
+
+ fn collect_invalidations(
+ &mut self,
+ _element: E,
+ self_invalidations: &mut InvalidationVector<'b>,
+ _descendant_invalidations: &mut DescendantInvalidationLists<'b>,
+ _sibling_invalidations: &mut InvalidationVector<'b>,
+ ) -> bool {
+ for cascade_data in &mut self.rules {
+ let map = cascade_data.invalidation_map();
+ for dependency in &map.document_state_selectors {
+ if !dependency.state.intersects(self.document_states_changed) {
+ continue;
+ }
+
+ // We pass `None` as a scope, as document state selectors aren't
+ // affected by the current scope.
+ //
+ // FIXME(emilio): We should really pass the relevant host for
+ // self.rules, so that we invalidate correctly if the selector
+ // happens to have something like :host(:-moz-window-inactive)
+ // for example.
+ self_invalidations.push(Invalidation::new(
+ &dependency.dependency,
+ /* scope = */ None,
+ ));
+ }
+ }
+
+ false
+ }
+
+ fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> {
+ &mut self.matching_context
+ }
+
+ fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> {
+ &self.traversal_map
+ }
+
+ fn recursion_limit_exceeded(&mut self, _: E) {
+ unreachable!("We don't run document state invalidation with stack limits")
+ }
+
+ fn should_process_descendants(&mut self, element: E) -> bool {
+ match element.borrow_data() {
+ Some(d) => state_and_attributes::should_process_descendants(&d),
+ None => false,
+ }
+ }
+
+ fn invalidated_descendants(&mut self, element: E, child: E) {
+ state_and_attributes::invalidated_descendants(element, child)
+ }
+
+ fn invalidated_self(&mut self, element: E) {
+ state_and_attributes::invalidated_self(element);
+ }
+
+ fn invalidated_sibling(&mut self, sibling: E, of: E) {
+ state_and_attributes::invalidated_sibling(sibling, of);
+ }
+}
diff --git a/servo/components/style/invalidation/element/element_wrapper.rs b/servo/components/style/invalidation/element/element_wrapper.rs
new file mode 100644
index 0000000000..e17afd7774
--- /dev/null
+++ b/servo/components/style/invalidation/element/element_wrapper.rs
@@ -0,0 +1,388 @@
+/* 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/. */
+
+//! A wrapper over an element and a snapshot, that allows us to selector-match
+//! against a past state of the element.
+
+use crate::dom::TElement;
+use crate::selector_parser::{AttrValue, NonTSPseudoClass, PseudoElement, SelectorImpl};
+use crate::selector_parser::{Snapshot, SnapshotMap};
+use crate::values::AtomIdent;
+use crate::{CaseSensitivityExt, LocalName, Namespace, WeakAtom};
+use dom::ElementState;
+use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
+use selectors::bloom::BloomFilter;
+use selectors::matching::{ElementSelectorFlags, MatchingContext};
+use selectors::{Element, OpaqueElement};
+use std::cell::Cell;
+use std::fmt;
+
+/// In order to compute restyle hints, we perform a selector match against a
+/// list of partial selectors whose rightmost simple selector may be sensitive
+/// to the thing being changed. We do this matching twice, once for the element
+/// as it exists now and once for the element as it existed at the time of the
+/// last restyle. If the results of the selector match differ, that means that
+/// the given partial selector is sensitive to the change, and we compute a
+/// restyle hint based on its combinator.
+///
+/// In order to run selector matching against the old element state, we generate
+/// a wrapper for the element which claims to have the old state. This is the
+/// ElementWrapper logic below.
+///
+/// Gecko does this differently for element states, and passes a mask called
+/// mStateMask, which indicates the states that need to be ignored during
+/// selector matching. This saves an ElementWrapper allocation and an additional
+/// selector match call at the expense of additional complexity inside the
+/// selector matching logic. This only works for boolean states though, so we
+/// still need to take the ElementWrapper approach for attribute-dependent
+/// style. So we do it the same both ways for now to reduce complexity, but it's
+/// worth measuring the performance impact (if any) of the mStateMask approach.
+pub trait ElementSnapshot: Sized {
+ /// The state of the snapshot, if any.
+ fn state(&self) -> Option<ElementState>;
+
+ /// If this snapshot contains attribute information.
+ fn has_attrs(&self) -> bool;
+
+ /// Gets the attribute information of the snapshot as a string.
+ ///
+ /// Only for debugging purposes.
+ fn debug_list_attributes(&self) -> String {
+ String::new()
+ }
+
+ /// The ID attribute per this snapshot. Should only be called if
+ /// `has_attrs()` returns true.
+ fn id_attr(&self) -> Option<&WeakAtom>;
+
+ /// Whether this snapshot contains the class `name`. Should only be called
+ /// if `has_attrs()` returns true.
+ fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool;
+
+ /// Whether this snapshot represents the part named `name`. Should only be
+ /// called if `has_attrs()` returns true.
+ fn is_part(&self, name: &AtomIdent) -> bool;
+
+ /// See Element::imported_part.
+ fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent>;
+
+ /// A callback that should be called for each class of the snapshot. Should
+ /// only be called if `has_attrs()` returns true.
+ fn each_class<F>(&self, _: F)
+ where
+ F: FnMut(&AtomIdent);
+
+ /// The `xml:lang=""` or `lang=""` attribute value per this snapshot.
+ fn lang_attr(&self) -> Option<AttrValue>;
+}
+
+/// A simple wrapper over an element and a snapshot, that allows us to
+/// selector-match against a past state of the element.
+#[derive(Clone)]
+pub struct ElementWrapper<'a, E>
+where
+ E: TElement,
+{
+ element: E,
+ cached_snapshot: Cell<Option<&'a Snapshot>>,
+ snapshot_map: &'a SnapshotMap,
+}
+
+impl<'a, E> ElementWrapper<'a, E>
+where
+ E: TElement,
+{
+ /// Trivially constructs an `ElementWrapper`.
+ pub fn new(el: E, snapshot_map: &'a SnapshotMap) -> Self {
+ ElementWrapper {
+ element: el,
+ cached_snapshot: Cell::new(None),
+ snapshot_map: snapshot_map,
+ }
+ }
+
+ /// Gets the snapshot associated with this element, if any.
+ pub fn snapshot(&self) -> Option<&'a Snapshot> {
+ if !self.element.has_snapshot() {
+ return None;
+ }
+
+ if let Some(s) = self.cached_snapshot.get() {
+ return Some(s);
+ }
+
+ let snapshot = self.snapshot_map.get(&self.element);
+ debug_assert!(snapshot.is_some(), "has_snapshot lied!");
+
+ self.cached_snapshot.set(snapshot);
+
+ snapshot
+ }
+
+ /// Returns the states that have changed since the element was snapshotted.
+ pub fn state_changes(&self) -> ElementState {
+ let snapshot = match self.snapshot() {
+ Some(s) => s,
+ None => return ElementState::empty(),
+ };
+
+ match snapshot.state() {
+ Some(state) => state ^ self.element.state(),
+ None => ElementState::empty(),
+ }
+ }
+
+ /// Returns the value of the `xml:lang=""` (or, if appropriate, `lang=""`)
+ /// attribute from this element's snapshot or the closest ancestor
+ /// element snapshot with the attribute specified.
+ fn get_lang(&self) -> Option<AttrValue> {
+ let mut current = self.clone();
+ loop {
+ let lang = match self.snapshot() {
+ Some(snapshot) if snapshot.has_attrs() => snapshot.lang_attr(),
+ _ => current.element.lang_attr(),
+ };
+ if lang.is_some() {
+ return lang;
+ }
+ current = current.parent_element()?;
+ }
+ }
+}
+
+impl<'a, E> fmt::Debug for ElementWrapper<'a, E>
+where
+ E: TElement,
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ // Ignore other fields for now, can change later if needed.
+ self.element.fmt(f)
+ }
+}
+
+impl<'a, E> Element for ElementWrapper<'a, E>
+where
+ E: TElement,
+{
+ type Impl = SelectorImpl;
+
+ fn match_non_ts_pseudo_class(
+ &self,
+ pseudo_class: &NonTSPseudoClass,
+ context: &mut MatchingContext<Self::Impl>,
+ ) -> bool {
+ // Some pseudo-classes need special handling to evaluate them against
+ // the snapshot.
+ match *pseudo_class {
+ // For :link and :visited, we don't actually want to test the
+ // element state directly.
+ //
+ // Instead, we use the `visited_handling` to determine if they
+ // match.
+ NonTSPseudoClass::Link => {
+ return self.is_link() && context.visited_handling().matches_unvisited();
+ },
+ NonTSPseudoClass::Visited => {
+ return self.is_link() && context.visited_handling().matches_visited();
+ },
+
+ #[cfg(feature = "gecko")]
+ NonTSPseudoClass::MozTableBorderNonzero => {
+ if let Some(snapshot) = self.snapshot() {
+ if snapshot.has_other_pseudo_class_state() {
+ return snapshot.mIsTableBorderNonzero();
+ }
+ }
+ },
+
+ #[cfg(feature = "gecko")]
+ NonTSPseudoClass::MozSelectListBox => {
+ if let Some(snapshot) = self.snapshot() {
+ if snapshot.has_other_pseudo_class_state() {
+ return snapshot.mIsSelectListBox();
+ }
+ }
+ },
+
+ // :lang() needs to match using the closest ancestor xml:lang="" or
+ // lang="" attribtue from snapshots.
+ NonTSPseudoClass::Lang(ref lang_arg) => {
+ return self
+ .element
+ .match_element_lang(Some(self.get_lang()), lang_arg);
+ },
+
+ _ => {},
+ }
+
+ let flag = pseudo_class.state_flag();
+ if flag.is_empty() {
+ return self
+ .element
+ .match_non_ts_pseudo_class(pseudo_class, context);
+ }
+ match self.snapshot().and_then(|s| s.state()) {
+ Some(snapshot_state) => snapshot_state.intersects(flag),
+ None => self
+ .element
+ .match_non_ts_pseudo_class(pseudo_class, context),
+ }
+ }
+
+ fn apply_selector_flags(&self, _flags: ElementSelectorFlags) {
+ debug_assert!(false, "Shouldn't need selector flags for invalidation");
+ }
+
+ fn match_pseudo_element(
+ &self,
+ pseudo_element: &PseudoElement,
+ context: &mut MatchingContext<Self::Impl>,
+ ) -> bool {
+ self.element.match_pseudo_element(pseudo_element, context)
+ }
+
+ fn is_link(&self) -> bool {
+ match self.snapshot().and_then(|s| s.state()) {
+ Some(state) => state.intersects(ElementState::VISITED_OR_UNVISITED),
+ None => self.element.is_link(),
+ }
+ }
+
+ fn opaque(&self) -> OpaqueElement {
+ self.element.opaque()
+ }
+
+ fn parent_element(&self) -> Option<Self> {
+ let parent = self.element.parent_element()?;
+ Some(Self::new(parent, self.snapshot_map))
+ }
+
+ fn parent_node_is_shadow_root(&self) -> bool {
+ self.element.parent_node_is_shadow_root()
+ }
+
+ fn containing_shadow_host(&self) -> Option<Self> {
+ let host = self.element.containing_shadow_host()?;
+ Some(Self::new(host, self.snapshot_map))
+ }
+
+ fn prev_sibling_element(&self) -> Option<Self> {
+ let sibling = self.element.prev_sibling_element()?;
+ Some(Self::new(sibling, self.snapshot_map))
+ }
+
+ fn next_sibling_element(&self) -> Option<Self> {
+ let sibling = self.element.next_sibling_element()?;
+ Some(Self::new(sibling, self.snapshot_map))
+ }
+
+ fn first_element_child(&self) -> Option<Self> {
+ let child = self.element.first_element_child()?;
+ Some(Self::new(child, self.snapshot_map))
+ }
+
+ #[inline]
+ fn is_html_element_in_html_document(&self) -> bool {
+ self.element.is_html_element_in_html_document()
+ }
+
+ #[inline]
+ fn is_html_slot_element(&self) -> bool {
+ self.element.is_html_slot_element()
+ }
+
+ #[inline]
+ fn has_local_name(
+ &self,
+ local_name: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedLocalName,
+ ) -> bool {
+ self.element.has_local_name(local_name)
+ }
+
+ #[inline]
+ fn has_namespace(
+ &self,
+ ns: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedNamespaceUrl,
+ ) -> bool {
+ self.element.has_namespace(ns)
+ }
+
+ #[inline]
+ fn is_same_type(&self, other: &Self) -> bool {
+ self.element.is_same_type(&other.element)
+ }
+
+ fn attr_matches(
+ &self,
+ ns: &NamespaceConstraint<&Namespace>,
+ local_name: &LocalName,
+ operation: &AttrSelectorOperation<&AttrValue>,
+ ) -> bool {
+ match self.snapshot() {
+ Some(snapshot) if snapshot.has_attrs() => {
+ snapshot.attr_matches(ns, local_name, operation)
+ },
+ _ => self.element.attr_matches(ns, local_name, operation),
+ }
+ }
+
+ fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
+ match self.snapshot() {
+ Some(snapshot) if snapshot.has_attrs() => snapshot
+ .id_attr()
+ .map_or(false, |atom| case_sensitivity.eq_atom(&atom, id)),
+ _ => self.element.has_id(id, case_sensitivity),
+ }
+ }
+
+ fn is_part(&self, name: &AtomIdent) -> bool {
+ match self.snapshot() {
+ Some(snapshot) if snapshot.has_attrs() => snapshot.is_part(name),
+ _ => self.element.is_part(name),
+ }
+ }
+
+ fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> {
+ match self.snapshot() {
+ Some(snapshot) if snapshot.has_attrs() => snapshot.imported_part(name),
+ _ => self.element.imported_part(name),
+ }
+ }
+
+ fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
+ match self.snapshot() {
+ Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name, case_sensitivity),
+ _ => self.element.has_class(name, case_sensitivity),
+ }
+ }
+
+ fn is_empty(&self) -> bool {
+ self.element.is_empty()
+ }
+
+ fn is_root(&self) -> bool {
+ self.element.is_root()
+ }
+
+ fn is_pseudo_element(&self) -> bool {
+ self.element.is_pseudo_element()
+ }
+
+ fn pseudo_element_originating_element(&self) -> Option<Self> {
+ self.element
+ .pseudo_element_originating_element()
+ .map(|e| ElementWrapper::new(e, self.snapshot_map))
+ }
+
+ fn assigned_slot(&self) -> Option<Self> {
+ self.element
+ .assigned_slot()
+ .map(|e| ElementWrapper::new(e, self.snapshot_map))
+ }
+
+ fn add_element_unique_hashes(&self, _filter: &mut BloomFilter) -> bool {
+ // Should not be relevant in the context of checking past elements in invalidation.
+ false
+ }
+}
diff --git a/servo/components/style/invalidation/element/invalidation_map.rs b/servo/components/style/invalidation/element/invalidation_map.rs
new file mode 100644
index 0000000000..cb03862740
--- /dev/null
+++ b/servo/components/style/invalidation/element/invalidation_map.rs
@@ -0,0 +1,1425 @@
+/* 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/. */
+
+//! Code for invalidations due to state or attribute changes.
+
+use crate::context::QuirksMode;
+use crate::selector_map::{
+ MaybeCaseInsensitiveHashMap, PrecomputedHashMap, SelectorMap, SelectorMapEntry,
+};
+use crate::selector_parser::{NonTSPseudoClass, SelectorImpl};
+use crate::AllocErr;
+use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded};
+use dom::{DocumentState, ElementState};
+use selectors::attr::NamespaceConstraint;
+use selectors::parser::{
+ Combinator, Component, RelativeSelector, RelativeSelectorCombinatorCount,
+ RelativeSelectorMatchHint,
+};
+use selectors::parser::{Selector, SelectorIter};
+use selectors::visitor::{SelectorListKind, SelectorVisitor};
+use servo_arc::Arc;
+use smallvec::SmallVec;
+
+/// Mapping between (partial) CompoundSelectors (and the combinator to their
+/// right) and the states and attributes they depend on.
+///
+/// In general, for all selectors in all applicable stylesheets of the form:
+///
+/// |a _ b _ c _ d _ e|
+///
+/// Where:
+/// * |b| and |d| are simple selectors that depend on state (like :hover) or
+/// attributes (like [attr...], .foo, or #foo).
+/// * |a|, |c|, and |e| are arbitrary simple selectors that do not depend on
+/// state or attributes.
+///
+/// We generate a Dependency for both |a _ b:X _| and |a _ b:X _ c _ d:Y _|,
+/// even though those selectors may not appear on their own in any stylesheet.
+/// This allows us to quickly scan through the dependency sites of all style
+/// rules and determine the maximum effect that a given state or attribute
+/// change may have on the style of elements in the document.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct Dependency {
+ /// The dependency selector.
+ #[ignore_malloc_size_of = "CssRules have primary refs, we measure there"]
+ pub selector: Selector<SelectorImpl>,
+
+ /// The offset into the selector that we should match on.
+ pub selector_offset: usize,
+
+ /// The parent dependency for an ancestor selector. For example, consider
+ /// the following:
+ ///
+ /// .foo .bar:where(.baz span) .qux
+ /// ^ ^ ^
+ /// A B C
+ ///
+ /// We'd generate:
+ ///
+ /// * One dependency for .qux (offset: 0, parent: None)
+ /// * One dependency for .baz pointing to B with parent being a
+ /// dependency pointing to C.
+ /// * One dependency from .bar pointing to C (parent: None)
+ /// * One dependency from .foo pointing to A (parent: None)
+ ///
+ #[ignore_malloc_size_of = "Arc"]
+ pub parent: Option<Arc<Dependency>>,
+
+ /// What kind of relative selector invalidation this generates.
+ /// None if this dependency is not within a relative selector.
+ relative_kind: Option<RelativeDependencyInvalidationKind>,
+}
+
+impl SelectorMapEntry for Dependency {
+ fn selector(&self) -> SelectorIter<SelectorImpl> {
+ self.selector.iter_from(self.selector_offset)
+ }
+}
+
+/// The kind of elements down the tree this dependency may affect.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)]
+pub enum NormalDependencyInvalidationKind {
+ /// This dependency may affect the element that changed itself.
+ Element,
+ /// This dependency affects the style of the element itself, and also the
+ /// style of its descendants.
+ ///
+ /// TODO(emilio): Each time this feels more of a hack for eager pseudos...
+ ElementAndDescendants,
+ /// This dependency may affect descendants down the tree.
+ Descendants,
+ /// This dependency may affect siblings to the right of the element that
+ /// changed.
+ Siblings,
+ /// This dependency may affect slotted elements of the element that changed.
+ SlottedElements,
+ /// This dependency may affect parts of the element that changed.
+ Parts,
+}
+
+/// The kind of elements up the tree this relative selector dependency may
+/// affect. Because this travels upwards, it's not viable for parallel subtree
+/// traversal, and is handled separately.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)]
+pub enum RelativeDependencyInvalidationKind {
+ /// This dependency may affect relative selector anchors for ancestors.
+ Ancestors,
+ /// This dependency may affect a relative selector anchor for the parent.
+ Parent,
+ /// This dependency may affect a relative selector anchor for the previous sibling.
+ PrevSibling,
+ /// This dependency may affect relative selector anchors for ancestors' previous siblings.
+ AncestorPrevSibling,
+ /// This dependency may affect relative selector anchors for earlier siblings.
+ EarlierSibling,
+ /// This dependency may affect relative selector anchors for ancestors' earlier siblings.
+ AncestorEarlierSibling,
+}
+
+/// Invalidation kind merging normal and relative dependencies.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)]
+pub enum DependencyInvalidationKind {
+ /// This dependency is a normal dependency.
+ Normal(NormalDependencyInvalidationKind),
+ /// This dependency is a relative dependency.
+ Relative(RelativeDependencyInvalidationKind),
+}
+
+impl Dependency {
+ /// Creates a dummy dependency to invalidate the whole selector.
+ ///
+ /// This is necessary because document state invalidation wants to
+ /// invalidate all elements in the document.
+ ///
+ /// The offset is such as that Invalidation::new(self) returns a zero
+ /// offset. That is, it points to a virtual "combinator" outside of the
+ /// selector, so calling combinator() on such a dependency will panic.
+ pub fn for_full_selector_invalidation(selector: Selector<SelectorImpl>) -> Self {
+ Self {
+ selector_offset: selector.len() + 1,
+ selector,
+ parent: None,
+ relative_kind: None,
+ }
+ }
+
+ /// Returns the combinator to the right of the partial selector this
+ /// dependency represents.
+ ///
+ /// TODO(emilio): Consider storing inline if it helps cache locality?
+ fn combinator(&self) -> Option<Combinator> {
+ if self.selector_offset == 0 {
+ return None;
+ }
+
+ Some(
+ self.selector
+ .combinator_at_match_order(self.selector_offset - 1),
+ )
+ }
+
+ /// The kind of normal invalidation that this would generate. The dependency
+ /// in question must be a normal dependency.
+ pub fn normal_invalidation_kind(&self) -> NormalDependencyInvalidationKind {
+ debug_assert!(
+ self.relative_kind.is_none(),
+ "Querying normal invalidation kind on relative dependency."
+ );
+ match self.combinator() {
+ None => NormalDependencyInvalidationKind::Element,
+ Some(Combinator::Child) | Some(Combinator::Descendant) => {
+ NormalDependencyInvalidationKind::Descendants
+ },
+ Some(Combinator::LaterSibling) | Some(Combinator::NextSibling) => {
+ NormalDependencyInvalidationKind::Siblings
+ },
+ // TODO(emilio): We could look at the selector itself to see if it's
+ // an eager pseudo, and return only Descendants here if not.
+ Some(Combinator::PseudoElement) => {
+ NormalDependencyInvalidationKind::ElementAndDescendants
+ },
+ Some(Combinator::SlotAssignment) => NormalDependencyInvalidationKind::SlottedElements,
+ Some(Combinator::Part) => NormalDependencyInvalidationKind::Parts,
+ }
+ }
+
+ /// The kind of invalidation that this would generate.
+ pub fn invalidation_kind(&self) -> DependencyInvalidationKind {
+ if let Some(kind) = self.relative_kind {
+ return DependencyInvalidationKind::Relative(kind);
+ }
+ DependencyInvalidationKind::Normal(self.normal_invalidation_kind())
+ }
+
+ /// Is the combinator to the right of this dependency's compound selector
+ /// the next sibling combinator? This matters for insertion/removal in between
+ /// two elements connected through next sibling, e.g. `.foo:has(> .a + .b)`
+ /// where an element gets inserted between `.a` and `.b`.
+ pub fn right_combinator_is_next_sibling(&self) -> bool {
+ if self.selector_offset == 0 {
+ return false;
+ }
+ matches!(
+ self.selector
+ .combinator_at_match_order(self.selector_offset - 1),
+ Combinator::NextSibling
+ )
+ }
+
+ /// Is this dependency's compound selector a single compound in `:has`
+ /// with the next sibling relative combinator i.e. `:has(> .foo)`?
+ /// This matters for insertion between an anchor and an element
+ /// connected through next sibling, e.g. `.a:has(> .b)`.
+ pub fn dependency_is_relative_with_single_next_sibling(&self) -> bool {
+ match self.invalidation_kind() {
+ DependencyInvalidationKind::Normal(_) => false,
+ DependencyInvalidationKind::Relative(kind) => {
+ kind == RelativeDependencyInvalidationKind::PrevSibling
+ },
+ }
+ }
+}
+
+/// The same, but for state selectors, which can track more exactly what state
+/// do they track.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct StateDependency {
+ /// The other dependency fields.
+ pub dep: Dependency,
+ /// The state this dependency is affected by.
+ pub state: ElementState,
+}
+
+impl SelectorMapEntry for StateDependency {
+ fn selector(&self) -> SelectorIter<SelectorImpl> {
+ self.dep.selector()
+ }
+}
+
+/// The same, but for document state selectors.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct DocumentStateDependency {
+ /// We track `Dependency` even though we don't need to track an offset,
+ /// since when it changes it changes for the whole document anyway.
+ #[cfg_attr(
+ feature = "gecko",
+ ignore_malloc_size_of = "CssRules have primary refs, we measure there"
+ )]
+ #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")]
+ pub dependency: Dependency,
+ /// The state this dependency is affected by.
+ pub state: DocumentState,
+}
+
+/// Dependency mapping for classes or IDs.
+pub type IdOrClassDependencyMap = MaybeCaseInsensitiveHashMap<Atom, SmallVec<[Dependency; 1]>>;
+/// Dependency mapping for non-tree-strctural pseudo-class states.
+pub type StateDependencyMap = SelectorMap<StateDependency>;
+/// Dependency mapping for local names.
+pub type LocalNameDependencyMap = PrecomputedHashMap<LocalName, SmallVec<[Dependency; 1]>>;
+
+/// A map where we store invalidations.
+///
+/// This is slightly different to a SelectorMap, in the sense of that the same
+/// selector may appear multiple times.
+///
+/// In particular, we want to lookup as few things as possible to get the fewer
+/// selectors the better, so this looks up by id, class, or looks at the list of
+/// state/other attribute affecting selectors.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct InvalidationMap {
+ /// A map from a given class name to all the selectors with that class
+ /// selector.
+ pub class_to_selector: IdOrClassDependencyMap,
+ /// A map from a given id to all the selectors with that ID in the
+ /// stylesheets currently applying to the document.
+ pub id_to_selector: IdOrClassDependencyMap,
+ /// A map of all the state dependencies.
+ pub state_affecting_selectors: StateDependencyMap,
+ /// A list of document state dependencies in the rules we represent.
+ pub document_state_selectors: Vec<DocumentStateDependency>,
+ /// A map of other attribute affecting selectors.
+ pub other_attribute_affecting_selectors: LocalNameDependencyMap,
+}
+
+/// Tree-structural pseudoclasses that we care about for (Relative selector) invalidation.
+/// Specifically, we need to store information on ones that don't generate the inner selector.
+#[derive(Clone, Copy, Debug, MallocSizeOf)]
+pub struct TSStateForInvalidation(u8);
+
+bitflags! {
+ impl TSStateForInvalidation : u8 {
+ /// :empty
+ const EMPTY = 1 << 0;
+ /// :nth etc, without of.
+ const NTH = 1 << 1;
+ /// "Simple" edge child selectors, like :first-child, :last-child, etc.
+ /// Excludes :*-of-type.
+ const NTH_EDGE = 1 << 2;
+ }
+}
+
+/// Dependency for tree-structural pseudo-classes.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct TSStateDependency {
+ /// The other dependency fields.
+ pub dep: Dependency,
+ /// The state this dependency is affected by.
+ pub state: TSStateForInvalidation,
+}
+
+impl SelectorMapEntry for TSStateDependency {
+ fn selector(&self) -> SelectorIter<SelectorImpl> {
+ self.dep.selector()
+ }
+}
+
+/// Dependency mapping for tree-structural pseudo-class states.
+pub type TSStateDependencyMap = SelectorMap<TSStateDependency>;
+/// Dependency mapping for * selectors.
+pub type AnyDependencyMap = SmallVec<[Dependency; 1]>;
+
+/// A map to store all relative selector invalidations.
+/// This keeps a lot more data than the usual map, because any change can generate
+/// upward traversals that need to be handled separately.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct RelativeSelectorInvalidationMap {
+ /// Portion common to the normal invalidation map, except that this is for relative selectors and their inner selectors.
+ pub map: InvalidationMap,
+ /// A map for a given tree-structural pseudo-class to all the relative selector dependencies with that type.
+ pub ts_state_to_selector: TSStateDependencyMap,
+ /// A map from a given type name to all the relative selector dependencies with that type.
+ pub type_to_selector: LocalNameDependencyMap,
+ /// All relative selector dependencies that specify `*`.
+ pub any_to_selector: AnyDependencyMap,
+ /// Flag indicating if any relative selector is used.
+ pub used: bool,
+ /// Flag indicating if invalidating a relative selector requires ancestor traversal.
+ pub needs_ancestors_traversal: bool,
+}
+
+impl RelativeSelectorInvalidationMap {
+ /// Creates an empty `InvalidationMap`.
+ pub fn new() -> Self {
+ Self {
+ map: InvalidationMap::new(),
+ ts_state_to_selector: TSStateDependencyMap::default(),
+ type_to_selector: LocalNameDependencyMap::default(),
+ any_to_selector: SmallVec::default(),
+ used: false,
+ needs_ancestors_traversal: false,
+ }
+ }
+
+ /// Returns the number of dependencies stored in the invalidation map.
+ pub fn len(&self) -> usize {
+ self.map.len()
+ }
+
+ /// Clears this map, leaving it empty.
+ pub fn clear(&mut self) {
+ self.map.clear();
+ self.ts_state_to_selector.clear();
+ self.type_to_selector.clear();
+ self.any_to_selector.clear();
+ }
+
+ /// Shrink the capacity of hash maps if needed.
+ pub fn shrink_if_needed(&mut self) {
+ self.map.shrink_if_needed();
+ self.ts_state_to_selector.shrink_if_needed();
+ self.type_to_selector.shrink_if_needed();
+ }
+}
+
+impl InvalidationMap {
+ /// Creates an empty `InvalidationMap`.
+ pub fn new() -> Self {
+ Self {
+ class_to_selector: IdOrClassDependencyMap::new(),
+ id_to_selector: IdOrClassDependencyMap::new(),
+ state_affecting_selectors: StateDependencyMap::new(),
+ document_state_selectors: Vec::new(),
+ other_attribute_affecting_selectors: LocalNameDependencyMap::default(),
+ }
+ }
+
+ /// Returns the number of dependencies stored in the invalidation map.
+ pub fn len(&self) -> usize {
+ self.state_affecting_selectors.len() +
+ self.document_state_selectors.len() +
+ self.other_attribute_affecting_selectors
+ .iter()
+ .fold(0, |accum, (_, ref v)| accum + v.len()) +
+ self.id_to_selector
+ .iter()
+ .fold(0, |accum, (_, ref v)| accum + v.len()) +
+ self.class_to_selector
+ .iter()
+ .fold(0, |accum, (_, ref v)| accum + v.len())
+ }
+
+ /// Clears this map, leaving it empty.
+ pub fn clear(&mut self) {
+ self.class_to_selector.clear();
+ self.id_to_selector.clear();
+ self.state_affecting_selectors.clear();
+ self.document_state_selectors.clear();
+ self.other_attribute_affecting_selectors.clear();
+ }
+
+ /// Shrink the capacity of hash maps if needed.
+ pub fn shrink_if_needed(&mut self) {
+ self.class_to_selector.shrink_if_needed();
+ self.id_to_selector.shrink_if_needed();
+ self.state_affecting_selectors.shrink_if_needed();
+ self.other_attribute_affecting_selectors.shrink_if_needed();
+ }
+}
+
+/// Adds a selector to the given `InvalidationMap`. Returns Err(..) to signify OOM.
+pub fn note_selector_for_invalidation(
+ selector: &Selector<SelectorImpl>,
+ quirks_mode: QuirksMode,
+ map: &mut InvalidationMap,
+ relative_selector_invalidation_map: &mut RelativeSelectorInvalidationMap,
+) -> Result<(), AllocErr> {
+ debug!("note_selector_for_invalidation({:?})", selector);
+
+ let mut document_state = DocumentState::empty();
+ {
+ let mut parent_stack = ParentSelectors::new();
+ let mut alloc_error = None;
+ let mut collector = SelectorDependencyCollector {
+ map,
+ relative_selector_invalidation_map,
+ document_state: &mut document_state,
+ selector,
+ parent_selectors: &mut parent_stack,
+ quirks_mode,
+ compound_state: PerCompoundState::new(0),
+ alloc_error: &mut alloc_error,
+ };
+
+ let visit_result = collector.visit_whole_selector();
+ debug_assert_eq!(!visit_result, alloc_error.is_some());
+ if let Some(alloc_error) = alloc_error {
+ return Err(alloc_error);
+ }
+ }
+
+ if !document_state.is_empty() {
+ let dep = DocumentStateDependency {
+ state: document_state,
+ dependency: Dependency::for_full_selector_invalidation(selector.clone()),
+ };
+ map.document_state_selectors.try_reserve(1)?;
+ map.document_state_selectors.push(dep);
+ }
+ Ok(())
+}
+struct PerCompoundState {
+ /// The offset at which our compound starts.
+ offset: usize,
+
+ /// The state this compound selector is affected by.
+ element_state: ElementState,
+}
+
+impl PerCompoundState {
+ fn new(offset: usize) -> Self {
+ Self {
+ offset,
+ element_state: ElementState::empty(),
+ }
+ }
+}
+
+struct ParentDependencyEntry {
+ selector: Selector<SelectorImpl>,
+ offset: usize,
+ cached_dependency: Option<Arc<Dependency>>,
+}
+
+trait Collector {
+ fn dependency(&mut self) -> Dependency;
+ fn id_map(&mut self) -> &mut IdOrClassDependencyMap;
+ fn class_map(&mut self) -> &mut IdOrClassDependencyMap;
+ fn state_map(&mut self) -> &mut StateDependencyMap;
+ fn attribute_map(&mut self) -> &mut LocalNameDependencyMap;
+ fn update_states(&mut self, element_state: ElementState, document_state: DocumentState);
+
+ // In normal invalidations, type-based dependencies don't need to be explicitly tracked;
+ // elements don't change their types, and mutations cause invalidations to go descendant
+ // (Where they are about to be styled anyway), and/or later-sibling direction (Where they
+ // siblings after inserted/removed elements get restyled anyway).
+ // However, for relative selectors, a DOM mutation can affect and arbitrary ancestor and/or
+ // earlier siblings, so we need to keep track of them.
+ fn type_map(&mut self) -> &mut LocalNameDependencyMap {
+ unreachable!();
+ }
+
+ // Tree-structural pseudo-selectors generally invalidates in a well-defined way, which are
+ // handled by RestyleManager. However, for relative selectors, as with type invalidations,
+ // the direction of invalidation becomes arbitrary, so we need to keep track of them.
+ fn ts_state_map(&mut self) -> &mut TSStateDependencyMap {
+ unreachable!();
+ }
+
+ // Same story as type invalidation maps.
+ fn any_vec(&mut self) -> &mut AnyDependencyMap {
+ unreachable!();
+ }
+}
+
+fn on_attribute<C: Collector>(
+ local_name: &LocalName,
+ local_name_lower: &LocalName,
+ collector: &mut C,
+) -> Result<(), AllocErr> {
+ add_attr_dependency(local_name.clone(), collector)?;
+
+ if local_name != local_name_lower {
+ add_attr_dependency(local_name_lower.clone(), collector)?;
+ }
+ Ok(())
+}
+
+fn on_id_or_class<C: Collector>(
+ s: &Component<SelectorImpl>,
+ quirks_mode: QuirksMode,
+ collector: &mut C,
+) -> Result<(), AllocErr> {
+ let dependency = collector.dependency();
+ let (atom, map) = match *s {
+ Component::ID(ref atom) => (atom, collector.id_map()),
+ Component::Class(ref atom) => (atom, collector.class_map()),
+ _ => unreachable!(),
+ };
+ let entry = map.try_entry(atom.0.clone(), quirks_mode)?;
+ let vec = entry.or_insert_with(SmallVec::new);
+ vec.try_reserve(1)?;
+ vec.push(dependency);
+ Ok(())
+}
+
+fn add_attr_dependency<C: Collector>(name: LocalName, collector: &mut C) -> Result<(), AllocErr> {
+ let dependency = collector.dependency();
+ let map = collector.attribute_map();
+ add_local_name(name, dependency, map)
+}
+
+fn add_local_name(
+ name: LocalName,
+ dependency: Dependency,
+ map: &mut LocalNameDependencyMap,
+) -> Result<(), AllocErr> {
+ map.try_reserve(1)?;
+ let vec = map.entry(name).or_default();
+ vec.try_reserve(1)?;
+ vec.push(dependency);
+ Ok(())
+}
+
+fn on_pseudo_class<C: Collector>(pc: &NonTSPseudoClass, collector: &mut C) -> Result<(), AllocErr> {
+ collector.update_states(pc.state_flag(), pc.document_state_flag());
+
+ let attr_name = match *pc {
+ #[cfg(feature = "gecko")]
+ NonTSPseudoClass::MozTableBorderNonzero => local_name!("border"),
+ #[cfg(feature = "gecko")]
+ NonTSPseudoClass::MozSelectListBox => {
+ // This depends on two attributes.
+ add_attr_dependency(local_name!("multiple"), collector)?;
+ return add_attr_dependency(local_name!("size"), collector);
+ },
+ NonTSPseudoClass::Lang(..) => local_name!("lang"),
+ _ => return Ok(()),
+ };
+
+ add_attr_dependency(attr_name, collector)
+}
+
+fn add_pseudo_class_dependency<C: Collector>(
+ element_state: ElementState,
+ quirks_mode: QuirksMode,
+ collector: &mut C,
+) -> Result<(), AllocErr> {
+ if element_state.is_empty() {
+ return Ok(());
+ }
+ let dependency = collector.dependency();
+ collector.state_map().insert(
+ StateDependency {
+ dep: dependency,
+ state: element_state,
+ },
+ quirks_mode,
+ )
+}
+
+type ParentSelectors = SmallVec<[ParentDependencyEntry; 5]>;
+
+/// A struct that collects invalidations for a given compound selector.
+struct SelectorDependencyCollector<'a> {
+ map: &'a mut InvalidationMap,
+ relative_selector_invalidation_map: &'a mut RelativeSelectorInvalidationMap,
+
+ /// The document this _complex_ selector is affected by.
+ ///
+ /// We don't need to track state per compound selector, since it's global
+ /// state and it changes for everything.
+ document_state: &'a mut DocumentState,
+
+ /// The current selector and offset we're iterating.
+ selector: &'a Selector<SelectorImpl>,
+
+ /// The stack of parent selectors that we have, and at which offset of the
+ /// sequence.
+ ///
+ /// This starts empty. It grows when we find nested :is and :where selector
+ /// lists. The dependency field is cached and reference counted.
+ parent_selectors: &'a mut ParentSelectors,
+
+ /// The quirks mode of the document where we're inserting dependencies.
+ quirks_mode: QuirksMode,
+
+ /// State relevant to a given compound selector.
+ compound_state: PerCompoundState,
+
+ /// The allocation error, if we OOM.
+ alloc_error: &'a mut Option<AllocErr>,
+}
+
+fn parent_dependency(
+ parent_selectors: &mut ParentSelectors,
+ outer_parent: Option<&Arc<Dependency>>,
+) -> Option<Arc<Dependency>> {
+ if parent_selectors.is_empty() {
+ return outer_parent.cloned();
+ }
+
+ fn dependencies_from(
+ entries: &mut [ParentDependencyEntry],
+ outer_parent: &Option<&Arc<Dependency>>,
+ ) -> Option<Arc<Dependency>> {
+ if entries.is_empty() {
+ return None;
+ }
+
+ let last_index = entries.len() - 1;
+ let (previous, last) = entries.split_at_mut(last_index);
+ let last = &mut last[0];
+ let selector = &last.selector;
+ let selector_offset = last.offset;
+ Some(
+ last.cached_dependency
+ .get_or_insert_with(|| {
+ Arc::new(Dependency {
+ selector: selector.clone(),
+ selector_offset,
+ parent: dependencies_from(previous, outer_parent),
+ relative_kind: None,
+ })
+ })
+ .clone(),
+ )
+ }
+
+ dependencies_from(parent_selectors, &outer_parent)
+}
+
+impl<'a> Collector for SelectorDependencyCollector<'a> {
+ fn dependency(&mut self) -> Dependency {
+ let parent = parent_dependency(self.parent_selectors, None);
+ Dependency {
+ selector: self.selector.clone(),
+ selector_offset: self.compound_state.offset,
+ parent,
+ relative_kind: None,
+ }
+ }
+
+ fn id_map(&mut self) -> &mut IdOrClassDependencyMap {
+ &mut self.map.id_to_selector
+ }
+
+ fn class_map(&mut self) -> &mut IdOrClassDependencyMap {
+ &mut self.map.class_to_selector
+ }
+
+ fn state_map(&mut self) -> &mut StateDependencyMap {
+ &mut self.map.state_affecting_selectors
+ }
+
+ fn attribute_map(&mut self) -> &mut LocalNameDependencyMap {
+ &mut self.map.other_attribute_affecting_selectors
+ }
+
+ fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) {
+ self.compound_state.element_state |= element_state;
+ *self.document_state |= document_state;
+ }
+}
+
+impl<'a> SelectorDependencyCollector<'a> {
+ fn visit_whole_selector(&mut self) -> bool {
+ let iter = self.selector.iter();
+ self.visit_whole_selector_from(iter, 0)
+ }
+
+ fn visit_whole_selector_from(
+ &mut self,
+ mut iter: SelectorIter<SelectorImpl>,
+ mut index: usize,
+ ) -> bool {
+ loop {
+ // Reset the compound state.
+ self.compound_state = PerCompoundState::new(index);
+
+ // Visit all the simple selectors in this sequence.
+ for ss in &mut iter {
+ if !ss.visit(self) {
+ return false;
+ }
+ index += 1; // Account for the simple selector.
+ }
+
+ if let Err(err) = add_pseudo_class_dependency(
+ self.compound_state.element_state,
+ self.quirks_mode,
+ self,
+ ) {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+
+ let combinator = iter.next_sequence();
+ if combinator.is_none() {
+ return true;
+ }
+ index += 1; // account for the combinator
+ }
+ }
+}
+
+impl<'a> SelectorVisitor for SelectorDependencyCollector<'a> {
+ type Impl = SelectorImpl;
+
+ fn visit_selector_list(
+ &mut self,
+ _list_kind: SelectorListKind,
+ list: &[Selector<SelectorImpl>],
+ ) -> bool {
+ for selector in list {
+ // Here we cheat a bit: We can visit the rightmost compound with
+ // the "outer" visitor, and it'd be fine. This reduces the amount of
+ // state and attribute invalidations, and we need to check the outer
+ // selector to the left anyway to avoid over-invalidation, so it
+ // avoids matching it twice uselessly.
+ let mut iter = selector.iter();
+ let mut index = 0;
+
+ for ss in &mut iter {
+ if !ss.visit(self) {
+ return false;
+ }
+ index += 1;
+ }
+
+ let combinator = iter.next_sequence();
+ if combinator.is_none() {
+ continue;
+ }
+
+ index += 1; // account for the combinator.
+
+ self.parent_selectors.push(ParentDependencyEntry {
+ selector: self.selector.clone(),
+ offset: self.compound_state.offset,
+ cached_dependency: None,
+ });
+ let mut nested = SelectorDependencyCollector {
+ map: &mut *self.map,
+ relative_selector_invalidation_map: &mut *self.relative_selector_invalidation_map,
+ document_state: &mut *self.document_state,
+ selector,
+ parent_selectors: &mut *self.parent_selectors,
+ quirks_mode: self.quirks_mode,
+ compound_state: PerCompoundState::new(index),
+ alloc_error: &mut *self.alloc_error,
+ };
+ if !nested.visit_whole_selector_from(iter, index) {
+ return false;
+ }
+ self.parent_selectors.pop();
+ }
+ true
+ }
+
+ fn visit_relative_selector_list(
+ &mut self,
+ list: &[selectors::parser::RelativeSelector<Self::Impl>],
+ ) -> bool {
+ self.relative_selector_invalidation_map.used = true;
+ for relative_selector in list {
+ // We can't cheat here like we do with other selector lists - the rightmost
+ // compound of a relative selector is not the subject of the invalidation.
+ self.parent_selectors.push(ParentDependencyEntry {
+ selector: self.selector.clone(),
+ offset: self.compound_state.offset,
+ cached_dependency: None,
+ });
+ let mut nested = RelativeSelectorDependencyCollector {
+ map: &mut *self.relative_selector_invalidation_map,
+ document_state: &mut *self.document_state,
+ selector: &relative_selector,
+ combinator_count: RelativeSelectorCombinatorCount::new(relative_selector),
+ parent_selectors: &mut *self.parent_selectors,
+ quirks_mode: self.quirks_mode,
+ compound_state: RelativeSelectorPerCompoundState::new(0),
+ alloc_error: &mut *self.alloc_error,
+ };
+ if !nested.visit_whole_selector() {
+ return false;
+ }
+ self.parent_selectors.pop();
+ }
+ true
+ }
+
+ fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool {
+ match *s {
+ Component::ID(..) | Component::Class(..) => {
+ if let Err(err) = on_id_or_class(s, self.quirks_mode, self) {
+ *self.alloc_error = Some(err.into());
+ return false;
+ }
+ true
+ },
+ Component::NonTSPseudoClass(ref pc) => {
+ if let Err(err) = on_pseudo_class(pc, self) {
+ *self.alloc_error = Some(err.into());
+ return false;
+ }
+ true
+ },
+ _ => true,
+ }
+ }
+
+ fn visit_attribute_selector(
+ &mut self,
+ _: &NamespaceConstraint<&Namespace>,
+ local_name: &LocalName,
+ local_name_lower: &LocalName,
+ ) -> bool {
+ if let Err(err) = on_attribute(local_name, local_name_lower, self) {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+ true
+ }
+}
+
+struct RelativeSelectorPerCompoundState {
+ state: PerCompoundState,
+ ts_state: TSStateForInvalidation,
+ added_entry: bool,
+}
+
+impl RelativeSelectorPerCompoundState {
+ fn new(offset: usize) -> Self {
+ Self {
+ state: PerCompoundState::new(offset),
+ ts_state: TSStateForInvalidation::empty(),
+ added_entry: false,
+ }
+ }
+}
+
+/// A struct that collects invalidations for a given compound selector.
+struct RelativeSelectorDependencyCollector<'a> {
+ map: &'a mut RelativeSelectorInvalidationMap,
+
+ /// The document this _complex_ selector is affected by.
+ ///
+ /// We don't need to track state per compound selector, since it's global
+ /// state and it changes for everything.
+ document_state: &'a mut DocumentState,
+
+ /// The current inner relative selector and offset we're iterating.
+ selector: &'a RelativeSelector<SelectorImpl>,
+ /// Running combinator for this inner relative selector.
+ combinator_count: RelativeSelectorCombinatorCount,
+
+ /// The stack of parent selectors that we have, and at which offset of the
+ /// sequence.
+ ///
+ /// This starts empty. It grows when we find nested :is and :where selector
+ /// lists. The dependency field is cached and reference counted.
+ parent_selectors: &'a mut ParentSelectors,
+
+ /// The quirks mode of the document where we're inserting dependencies.
+ quirks_mode: QuirksMode,
+
+ /// State relevant to a given compound selector.
+ compound_state: RelativeSelectorPerCompoundState,
+
+ /// The allocation error, if we OOM.
+ alloc_error: &'a mut Option<AllocErr>,
+}
+
+fn add_non_unique_info<C: Collector>(
+ selector: &Selector<SelectorImpl>,
+ offset: usize,
+ collector: &mut C,
+) -> Result<(), AllocErr> {
+ // Go through this compound again.
+ for ss in selector.iter_from(offset) {
+ match ss {
+ Component::LocalName(ref name) => {
+ let dependency = collector.dependency();
+ add_local_name(name.name.clone(), dependency, &mut collector.type_map())?;
+ if name.name != name.lower_name {
+ let dependency = collector.dependency();
+ add_local_name(
+ name.lower_name.clone(),
+ dependency,
+ &mut collector.type_map(),
+ )?;
+ }
+ return Ok(());
+ },
+ _ => (),
+ };
+ }
+ // Ouch. Add one for *.
+ collector.any_vec().try_reserve(1)?;
+ let dependency = collector.dependency();
+ collector.any_vec().push(dependency);
+ Ok(())
+}
+
+fn add_ts_pseudo_class_dependency<C: Collector>(
+ state: TSStateForInvalidation,
+ quirks_mode: QuirksMode,
+ collector: &mut C,
+) -> Result<(), AllocErr> {
+ if state.is_empty() {
+ return Ok(());
+ }
+ let dependency = collector.dependency();
+ collector.ts_state_map().insert(
+ TSStateDependency {
+ dep: dependency,
+ state,
+ },
+ quirks_mode,
+ )
+}
+
+impl<'a> RelativeSelectorDependencyCollector<'a> {
+ fn visit_whole_selector(&mut self) -> bool {
+ let mut iter = self.selector.selector.iter_skip_relative_selector_anchor();
+ let mut index = 0;
+
+ self.map.needs_ancestors_traversal |= match self.selector.match_hint {
+ RelativeSelectorMatchHint::InNextSiblingSubtree |
+ RelativeSelectorMatchHint::InSiblingSubtree |
+ RelativeSelectorMatchHint::InSubtree => true,
+ _ => false,
+ };
+ loop {
+ // Reset the compound state.
+ self.compound_state = RelativeSelectorPerCompoundState::new(index);
+
+ // Visit all the simple selectors in this sequence.
+ for ss in &mut iter {
+ if !ss.visit(self) {
+ return false;
+ }
+ index += 1; // Account for the simple selector.
+ }
+
+ if let Err(err) = add_pseudo_class_dependency(
+ self.compound_state.state.element_state,
+ self.quirks_mode,
+ self,
+ ) {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+ if let Err(err) =
+ add_ts_pseudo_class_dependency(self.compound_state.ts_state, self.quirks_mode, self)
+ {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+
+ if !self.compound_state.added_entry {
+ // Not great - we didn't add any uniquely identifiable information.
+ if let Err(err) = add_non_unique_info(
+ &self.selector.selector,
+ self.compound_state.state.offset,
+ self,
+ ) {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+ }
+
+ let combinator = iter.next_sequence();
+ if let Some(c) = combinator {
+ match c {
+ Combinator::Child | Combinator::Descendant => {
+ self.combinator_count.child_or_descendants -= 1
+ },
+ Combinator::NextSibling | Combinator::LaterSibling => {
+ self.combinator_count.adjacent_or_next_siblings -= 1
+ },
+ Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => (),
+ }
+ } else {
+ return true;
+ }
+ index += 1; // account for the combinator
+ }
+ }
+}
+
+impl<'a> Collector for RelativeSelectorDependencyCollector<'a> {
+ fn dependency(&mut self) -> Dependency {
+ let parent = parent_dependency(self.parent_selectors, None);
+ Dependency {
+ selector: self.selector.selector.clone(),
+ selector_offset: self.compound_state.state.offset,
+ relative_kind: Some(match self.combinator_count.get_match_hint() {
+ RelativeSelectorMatchHint::InChild => RelativeDependencyInvalidationKind::Parent,
+ RelativeSelectorMatchHint::InSubtree => {
+ RelativeDependencyInvalidationKind::Ancestors
+ },
+ RelativeSelectorMatchHint::InNextSibling => {
+ RelativeDependencyInvalidationKind::PrevSibling
+ },
+ RelativeSelectorMatchHint::InSibling => {
+ RelativeDependencyInvalidationKind::EarlierSibling
+ },
+ RelativeSelectorMatchHint::InNextSiblingSubtree => {
+ RelativeDependencyInvalidationKind::AncestorPrevSibling
+ },
+ RelativeSelectorMatchHint::InSiblingSubtree => {
+ RelativeDependencyInvalidationKind::AncestorEarlierSibling
+ },
+ }),
+ parent,
+ }
+ }
+
+ fn id_map(&mut self) -> &mut IdOrClassDependencyMap {
+ &mut self.map.map.id_to_selector
+ }
+
+ fn class_map(&mut self) -> &mut IdOrClassDependencyMap {
+ &mut self.map.map.class_to_selector
+ }
+
+ fn state_map(&mut self) -> &mut StateDependencyMap {
+ &mut self.map.map.state_affecting_selectors
+ }
+
+ fn attribute_map(&mut self) -> &mut LocalNameDependencyMap {
+ &mut self.map.map.other_attribute_affecting_selectors
+ }
+
+ fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) {
+ self.compound_state.state.element_state |= element_state;
+ *self.document_state |= document_state;
+ }
+
+ fn type_map(&mut self) -> &mut LocalNameDependencyMap {
+ &mut self.map.type_to_selector
+ }
+
+ fn ts_state_map(&mut self) -> &mut TSStateDependencyMap {
+ &mut self.map.ts_state_to_selector
+ }
+
+ fn any_vec(&mut self) -> &mut AnyDependencyMap {
+ &mut self.map.any_to_selector
+ }
+}
+
+impl<'a> SelectorVisitor for RelativeSelectorDependencyCollector<'a> {
+ type Impl = SelectorImpl;
+
+ fn visit_selector_list(
+ &mut self,
+ _list_kind: SelectorListKind,
+ list: &[Selector<SelectorImpl>],
+ ) -> bool {
+ let mut parent_stack = ParentSelectors::new();
+ let parent_dependency = Arc::new(self.dependency());
+ for selector in list {
+ // Subjects inside relative selectors aren't really subjects.
+ // This simplifies compound state tracking as well (Additional
+ // states we track for relative selector's inner selectors should
+ // not leak out of the relevant selector).
+ let mut nested = RelativeSelectorInnerDependencyCollector {
+ map: &mut *self.map,
+ parent_dependency: &parent_dependency,
+ document_state: &mut *self.document_state,
+ selector,
+ parent_selectors: &mut parent_stack,
+ quirks_mode: self.quirks_mode,
+ compound_state: RelativeSelectorPerCompoundState::new(0),
+ alloc_error: &mut *self.alloc_error,
+ };
+ if !nested.visit_whole_selector() {
+ return false;
+ }
+ }
+ true
+ }
+
+ fn visit_relative_selector_list(
+ &mut self,
+ _list: &[selectors::parser::RelativeSelector<Self::Impl>],
+ ) -> bool {
+ // Ignore nested relative selectors. These can happen as a result of nesting.
+ true
+ }
+
+ fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool {
+ match *s {
+ Component::ID(..) | Component::Class(..) => {
+ self.compound_state.added_entry = true;
+ if let Err(err) = on_id_or_class(s, self.quirks_mode, self) {
+ *self.alloc_error = Some(err.into());
+ return false;
+ }
+ true
+ },
+ Component::NonTSPseudoClass(ref pc) => {
+ if !pc
+ .state_flag()
+ .intersects(ElementState::VISITED_OR_UNVISITED)
+ {
+ // Visited/Unvisited styling doesn't take the usual state invalidation path.
+ self.compound_state.added_entry = true;
+ }
+ if let Err(err) = on_pseudo_class(pc, self) {
+ *self.alloc_error = Some(err.into());
+ return false;
+ }
+ true
+ },
+ Component::Empty => {
+ self.compound_state
+ .ts_state
+ .insert(TSStateForInvalidation::EMPTY);
+ true
+ },
+ Component::Nth(data) => {
+ let kind = if data.is_simple_edge() {
+ TSStateForInvalidation::NTH_EDGE
+ } else {
+ TSStateForInvalidation::NTH
+ };
+ self.compound_state
+ .ts_state
+ .insert(kind);
+ true
+ },
+ Component::RelativeSelectorAnchor => unreachable!("Should not visit this far"),
+ _ => true,
+ }
+ }
+
+ fn visit_attribute_selector(
+ &mut self,
+ _: &NamespaceConstraint<&Namespace>,
+ local_name: &LocalName,
+ local_name_lower: &LocalName,
+ ) -> bool {
+ self.compound_state.added_entry = true;
+ if let Err(err) = on_attribute(local_name, local_name_lower, self) {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+ true
+ }
+}
+
+/// A struct that collects invalidations from a complex selector inside a relative selector.
+/// TODO(dshin): All of this duplication is not great Perhaps should be merged to the normal
+/// one, if possible? See bug 1855690.
+struct RelativeSelectorInnerDependencyCollector<'a, 'b> {
+ map: &'a mut RelativeSelectorInvalidationMap,
+
+ /// The document this _complex_ selector is affected by.
+ ///
+ /// We don't need to track state per compound selector, since it's global
+ /// state and it changes for everything.
+ document_state: &'a mut DocumentState,
+
+ /// Parent relative selector dependency.
+ parent_dependency: &'b Arc<Dependency>,
+
+ /// The current inner relative selector and offset we're iterating.
+ selector: &'a Selector<SelectorImpl>,
+
+ /// The stack of parent selectors that we have, and at which offset of the
+ /// sequence.
+ ///
+ /// This starts empty. It grows when we find nested :is and :where selector
+ /// lists. The dependency field is cached and reference counted.
+ parent_selectors: &'a mut ParentSelectors,
+
+ /// The quirks mode of the document where we're inserting dependencies.
+ quirks_mode: QuirksMode,
+
+ /// State relevant to a given compound selector.
+ compound_state: RelativeSelectorPerCompoundState,
+
+ /// The allocation error, if we OOM.
+ alloc_error: &'a mut Option<AllocErr>,
+}
+
+impl<'a, 'b> Collector for RelativeSelectorInnerDependencyCollector<'a, 'b> {
+ fn dependency(&mut self) -> Dependency {
+ let parent = parent_dependency(self.parent_selectors, Some(self.parent_dependency));
+ Dependency {
+ selector: self.selector.clone(),
+ selector_offset: self.compound_state.state.offset,
+ parent,
+ relative_kind: None,
+ }
+ }
+
+ fn id_map(&mut self) -> &mut IdOrClassDependencyMap {
+ &mut self.map.map.id_to_selector
+ }
+
+ fn class_map(&mut self) -> &mut IdOrClassDependencyMap {
+ &mut self.map.map.class_to_selector
+ }
+
+ fn state_map(&mut self) -> &mut StateDependencyMap {
+ &mut self.map.map.state_affecting_selectors
+ }
+
+ fn attribute_map(&mut self) -> &mut LocalNameDependencyMap {
+ &mut self.map.map.other_attribute_affecting_selectors
+ }
+
+ fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) {
+ self.compound_state.state.element_state |= element_state;
+ *self.document_state |= document_state;
+ }
+
+ fn type_map(&mut self) -> &mut LocalNameDependencyMap {
+ &mut self.map.type_to_selector
+ }
+
+ fn ts_state_map(&mut self) -> &mut TSStateDependencyMap {
+ &mut self.map.ts_state_to_selector
+ }
+
+ fn any_vec(&mut self) -> &mut AnyDependencyMap {
+ &mut self.map.any_to_selector
+ }
+}
+
+impl<'a, 'b> RelativeSelectorInnerDependencyCollector<'a, 'b> {
+ fn visit_whole_selector(&mut self) -> bool {
+ let mut iter = self.selector.iter();
+ let mut index = 0;
+ loop {
+ // Reset the compound state.
+ self.compound_state = RelativeSelectorPerCompoundState::new(index);
+
+ // Visit all the simple selectors in this sequence.
+ for ss in &mut iter {
+ if !ss.visit(self) {
+ return false;
+ }
+ index += 1; // Account for the simple selector.
+ }
+
+ if let Err(err) = add_pseudo_class_dependency(
+ self.compound_state.state.element_state,
+ self.quirks_mode,
+ self,
+ ) {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+
+ if let Err(err) =
+ add_ts_pseudo_class_dependency(self.compound_state.ts_state, self.quirks_mode, self)
+ {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+
+ if !self.compound_state.added_entry {
+ // Not great - we didn't add any uniquely identifiable information.
+ if let Err(err) =
+ add_non_unique_info(&self.selector, self.compound_state.state.offset, self)
+ {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+ }
+
+ let combinator = iter.next_sequence();
+ if combinator.is_none() {
+ return true;
+ }
+ index += 1; // account for the combinator
+ }
+ }
+}
+
+impl<'a, 'b> SelectorVisitor for RelativeSelectorInnerDependencyCollector<'a, 'b> {
+ type Impl = SelectorImpl;
+
+ fn visit_selector_list(
+ &mut self,
+ _list_kind: SelectorListKind,
+ list: &[Selector<SelectorImpl>],
+ ) -> bool {
+ let parent_dependency = Arc::new(self.dependency());
+ for selector in list {
+ // Subjects inside relative selectors aren't really subjects.
+ // This simplifies compound state tracking as well (Additional
+ // states we track for relative selector's inner selectors should
+ // not leak out of the relevant selector).
+ let mut nested = RelativeSelectorInnerDependencyCollector {
+ map: &mut *self.map,
+ parent_dependency: &parent_dependency,
+ document_state: &mut *self.document_state,
+ selector,
+ parent_selectors: &mut *self.parent_selectors,
+ quirks_mode: self.quirks_mode,
+ compound_state: RelativeSelectorPerCompoundState::new(0),
+ alloc_error: &mut *self.alloc_error,
+ };
+ if !nested.visit_whole_selector() {
+ return false;
+ }
+ }
+ true
+ }
+
+ fn visit_relative_selector_list(
+ &mut self,
+ _list: &[selectors::parser::RelativeSelector<Self::Impl>],
+ ) -> bool {
+ // Ignore nested relative selectors. These can happen as a result of nesting.
+ true
+ }
+
+ fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool {
+ match *s {
+ Component::ID(..) | Component::Class(..) => {
+ self.compound_state.added_entry = true;
+ if let Err(err) = on_id_or_class(s, self.quirks_mode, self) {
+ *self.alloc_error = Some(err.into());
+ return false;
+ }
+ true
+ },
+ Component::NonTSPseudoClass(ref pc) => {
+ if !pc
+ .state_flag()
+ .intersects(ElementState::VISITED_OR_UNVISITED)
+ {
+ // Visited/Unvisited styling doesn't take the usual state invalidation path.
+ self.compound_state.added_entry = true;
+ }
+ if let Err(err) = on_pseudo_class(pc, self) {
+ *self.alloc_error = Some(err.into());
+ return false;
+ }
+ true
+ },
+ Component::Empty => {
+ self.compound_state
+ .ts_state
+ .insert(TSStateForInvalidation::EMPTY);
+ true
+ },
+ Component::Nth(data) => {
+ let kind = if data.is_simple_edge() {
+ TSStateForInvalidation::NTH_EDGE
+ } else {
+ TSStateForInvalidation::NTH
+ };
+ self.compound_state
+ .ts_state
+ .insert(kind);
+ true
+ },
+ Component::RelativeSelectorAnchor => unreachable!("Should not visit this far"),
+ _ => true,
+ }
+ }
+
+ fn visit_attribute_selector(
+ &mut self,
+ _: &NamespaceConstraint<&Namespace>,
+ local_name: &LocalName,
+ local_name_lower: &LocalName,
+ ) -> bool {
+ self.compound_state.added_entry = true;
+ if let Err(err) = on_attribute(local_name, local_name_lower, self) {
+ *self.alloc_error = Some(err);
+ return false;
+ }
+ true
+ }
+}
diff --git a/servo/components/style/invalidation/element/invalidator.rs b/servo/components/style/invalidation/element/invalidator.rs
new file mode 100644
index 0000000000..5dee1f5dcf
--- /dev/null
+++ b/servo/components/style/invalidation/element/invalidator.rs
@@ -0,0 +1,1130 @@
+/* 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/. */
+
+//! The struct that takes care of encapsulating all the logic on where and how
+//! element styles need to be invalidated.
+
+use crate::context::StackLimitChecker;
+use crate::dom::{TElement, TNode, TShadowRoot};
+use crate::invalidation::element::invalidation_map::{
+ Dependency, DependencyInvalidationKind, NormalDependencyInvalidationKind,
+ RelativeDependencyInvalidationKind,
+};
+use selectors::matching::matches_compound_selector_from;
+use selectors::matching::{CompoundSelectorMatchingResult, MatchingContext};
+use selectors::parser::{Combinator, Component};
+use selectors::OpaqueElement;
+use smallvec::SmallVec;
+use std::fmt;
+use std::fmt::Write;
+
+struct SiblingInfo<E>
+where
+ E: TElement,
+{
+ affected: E,
+ prev_sibling: Option<E>,
+ next_sibling: Option<E>,
+}
+
+/// Traversal mapping for elements under consideration. It acts like a snapshot map,
+/// though this only "maps" one element at most.
+/// For general invalidations, this has no effect, especially since when
+/// DOM mutates, the mutation's effect should not escape the subtree being mutated.
+/// This is not the case for relative selectors, unfortunately, so we may end up
+/// traversing a portion of the DOM tree that mutated. In case the mutation is removal,
+/// its sibling relation is severed by the time the invalidation happens. This structure
+/// recovers that relation. Note - it assumes that there is only one element under this
+/// effect.
+pub struct SiblingTraversalMap<E>
+where
+ E: TElement,
+{
+ info: Option<SiblingInfo<E>>,
+}
+
+impl<E> Default for SiblingTraversalMap<E>
+where
+ E: TElement,
+{
+ fn default() -> Self {
+ Self { info: None }
+ }
+}
+
+impl<E> SiblingTraversalMap<E>
+where
+ E: TElement,
+{
+ /// Create a new traversal map with the affected element.
+ pub fn new(affected: E, prev_sibling: Option<E>, next_sibling: Option<E>) -> Self {
+ Self {
+ info: Some(SiblingInfo {
+ affected,
+ prev_sibling,
+ next_sibling,
+ }),
+ }
+ }
+
+ /// Get the element's previous sibling element.
+ pub fn next_sibling_for(&self, element: &E) -> Option<E> {
+ if let Some(ref info) = self.info {
+ if *element == info.affected {
+ return info.next_sibling;
+ }
+ }
+ element.next_sibling_element()
+ }
+
+ /// Get the element's previous sibling element.
+ pub fn prev_sibling_for(&self, element: &E) -> Option<E> {
+ if let Some(ref info) = self.info {
+ if *element == info.affected {
+ return info.prev_sibling;
+ }
+ }
+ element.prev_sibling_element()
+ }
+}
+
+/// A trait to abstract the collection of invalidations for a given pass.
+pub trait InvalidationProcessor<'a, 'b, E>
+where
+ E: TElement,
+{
+ /// Whether an invalidation that contains only a pseudo-element selector
+ /// like ::before or ::after triggers invalidation of the element that would
+ /// originate it.
+ fn invalidates_on_pseudo_element(&self) -> bool {
+ false
+ }
+
+ /// Whether the invalidation processor only cares about light-tree
+ /// descendants of a given element, that is, doesn't invalidate
+ /// pseudo-elements, NAC, shadow dom...
+ fn light_tree_only(&self) -> bool {
+ false
+ }
+
+ /// When a dependency from a :where or :is selector matches, it may still be
+ /// the case that we don't need to invalidate the full style. Consider the
+ /// case of:
+ ///
+ /// div .foo:where(.bar *, .baz) .qux
+ ///
+ /// We can get to the `*` part after a .bar class change, but you only need
+ /// to restyle the element if it also matches .foo.
+ ///
+ /// Similarly, you only need to restyle .baz if the whole result of matching
+ /// the selector changes.
+ ///
+ /// This function is called to check the result of matching the "outer"
+ /// dependency that we generate for the parent of the `:where` selector,
+ /// that is, in the case above it should match
+ /// `div .foo:where(.bar *, .baz)`.
+ ///
+ /// Returning true unconditionally here is over-optimistic and may
+ /// over-invalidate.
+ fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool;
+
+ /// The matching context that should be used to process invalidations.
+ fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl>;
+
+ /// The traversal map that should be used to process invalidations.
+ fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E>;
+
+ /// Collect invalidations for a given element's descendants and siblings.
+ ///
+ /// Returns whether the element itself was invalidated.
+ fn collect_invalidations(
+ &mut self,
+ element: E,
+ self_invalidations: &mut InvalidationVector<'a>,
+ descendant_invalidations: &mut DescendantInvalidationLists<'a>,
+ sibling_invalidations: &mut InvalidationVector<'a>,
+ ) -> bool;
+
+ /// Returns whether the invalidation process should process the descendants
+ /// of the given element.
+ fn should_process_descendants(&mut self, element: E) -> bool;
+
+ /// Executes an arbitrary action when the recursion limit is exceded (if
+ /// any).
+ fn recursion_limit_exceeded(&mut self, element: E);
+
+ /// Executes an action when `Self` is invalidated.
+ fn invalidated_self(&mut self, element: E);
+
+ /// Executes an action when `sibling` is invalidated as a sibling of
+ /// `of`.
+ fn invalidated_sibling(&mut self, sibling: E, of: E);
+
+ /// Executes an action when any descendant of `Self` is invalidated.
+ fn invalidated_descendants(&mut self, element: E, child: E);
+
+ /// Executes an action when an element in a relative selector is reached.
+ /// Lets the dependency to be borrowed for further processing out of the
+ /// invalidation traversal.
+ fn found_relative_selector_invalidation(
+ &mut self,
+ _element: E,
+ _kind: RelativeDependencyInvalidationKind,
+ _relative_dependency: &'a Dependency,
+ ) {
+ debug_assert!(false, "Reached relative selector dependency");
+ }
+}
+
+/// Different invalidation lists for descendants.
+#[derive(Debug, Default)]
+pub struct DescendantInvalidationLists<'a> {
+ /// Invalidations for normal DOM children and pseudo-elements.
+ ///
+ /// TODO(emilio): Having a list of invalidations just for pseudo-elements
+ /// may save some work here and there.
+ pub dom_descendants: InvalidationVector<'a>,
+ /// Invalidations for slotted children of an element.
+ pub slotted_descendants: InvalidationVector<'a>,
+ /// Invalidations for ::part()s of an element.
+ pub parts: InvalidationVector<'a>,
+}
+
+impl<'a> DescendantInvalidationLists<'a> {
+ fn is_empty(&self) -> bool {
+ self.dom_descendants.is_empty() &&
+ self.slotted_descendants.is_empty() &&
+ self.parts.is_empty()
+ }
+}
+
+/// The struct that takes care of encapsulating all the logic on where and how
+/// element styles need to be invalidated.
+pub struct TreeStyleInvalidator<'a, 'b, 'c, E, P: 'a>
+where
+ 'b: 'a,
+ E: TElement,
+ P: InvalidationProcessor<'b, 'c, E>,
+{
+ element: E,
+ stack_limit_checker: Option<&'a StackLimitChecker>,
+ processor: &'a mut P,
+ _marker: std::marker::PhantomData<(&'b (), &'c ())>,
+}
+
+/// A vector of invalidations, optimized for small invalidation sets.
+pub type InvalidationVector<'a> = SmallVec<[Invalidation<'a>; 10]>;
+
+/// The kind of descendant invalidation we're processing.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum DescendantInvalidationKind {
+ /// A DOM descendant invalidation.
+ Dom,
+ /// A ::slotted() descendant invalidation.
+ Slotted,
+ /// A ::part() descendant invalidation.
+ Part,
+}
+
+/// The kind of invalidation we're processing.
+///
+/// We can use this to avoid pushing invalidations of the same kind to our
+/// descendants or siblings.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum InvalidationKind {
+ Descendant(DescendantInvalidationKind),
+ Sibling,
+}
+
+/// An `Invalidation` is a complex selector that describes which elements,
+/// relative to a current element we are processing, must be restyled.
+#[derive(Clone)]
+pub struct Invalidation<'a> {
+ /// The dependency that generated this invalidation.
+ ///
+ /// Note that the offset inside the dependency is not really useful after
+ /// construction.
+ dependency: &'a Dependency,
+ /// The right shadow host from where the rule came from, if any.
+ ///
+ /// This is needed to ensure that we match the selector with the right
+ /// state, as whether some selectors like :host and ::part() match depends
+ /// on it.
+ scope: Option<OpaqueElement>,
+ /// The offset of the selector pointing to a compound selector.
+ ///
+ /// This order is a "parse order" offset, that is, zero is the leftmost part
+ /// of the selector written as parsed / serialized.
+ ///
+ /// It is initialized from the offset from `dependency`.
+ offset: usize,
+ /// Whether the invalidation was already matched by any previous sibling or
+ /// ancestor.
+ ///
+ /// If this is the case, we can avoid pushing invalidations generated by
+ /// this one if the generated invalidation is effective for all the siblings
+ /// or descendants after us.
+ matched_by_any_previous: bool,
+}
+
+impl<'a> Invalidation<'a> {
+ /// Create a new invalidation for matching a dependency.
+ pub fn new(dependency: &'a Dependency, scope: Option<OpaqueElement>) -> Self {
+ debug_assert!(
+ dependency.selector_offset == dependency.selector.len() + 1 ||
+ dependency.normal_invalidation_kind() !=
+ NormalDependencyInvalidationKind::Element,
+ "No point to this, if the dependency matched the element we should just invalidate it"
+ );
+ Self {
+ dependency,
+ scope,
+ // + 1 to go past the combinator.
+ offset: dependency.selector.len() + 1 - dependency.selector_offset,
+ matched_by_any_previous: false,
+ }
+ }
+
+ /// Whether this invalidation is effective for the next sibling or
+ /// descendant after us.
+ fn effective_for_next(&self) -> bool {
+ if self.offset == 0 {
+ return true;
+ }
+
+ // TODO(emilio): For pseudo-elements this should be mostly false, except
+ // for the weird pseudos in <input type="number">.
+ //
+ // We should be able to do better here!
+ match self
+ .dependency
+ .selector
+ .combinator_at_parse_order(self.offset - 1)
+ {
+ Combinator::Descendant | Combinator::LaterSibling | Combinator::PseudoElement => true,
+ Combinator::Part |
+ Combinator::SlotAssignment |
+ Combinator::NextSibling |
+ Combinator::Child => false,
+ }
+ }
+
+ fn kind(&self) -> InvalidationKind {
+ if self.offset == 0 {
+ return InvalidationKind::Descendant(DescendantInvalidationKind::Dom);
+ }
+
+ match self
+ .dependency
+ .selector
+ .combinator_at_parse_order(self.offset - 1)
+ {
+ Combinator::Child | Combinator::Descendant | Combinator::PseudoElement => {
+ InvalidationKind::Descendant(DescendantInvalidationKind::Dom)
+ },
+ Combinator::Part => InvalidationKind::Descendant(DescendantInvalidationKind::Part),
+ Combinator::SlotAssignment => {
+ InvalidationKind::Descendant(DescendantInvalidationKind::Slotted)
+ },
+ Combinator::NextSibling | Combinator::LaterSibling => InvalidationKind::Sibling,
+ }
+ }
+}
+
+impl<'a> fmt::Debug for Invalidation<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use cssparser::ToCss;
+
+ f.write_str("Invalidation(")?;
+ for component in self
+ .dependency
+ .selector
+ .iter_raw_parse_order_from(self.offset)
+ {
+ if matches!(*component, Component::Combinator(..)) {
+ break;
+ }
+ component.to_css(f)?;
+ }
+ f.write_char(')')
+ }
+}
+
+/// The result of processing a single invalidation for a given element.
+struct SingleInvalidationResult {
+ /// Whether the element itself was invalidated.
+ invalidated_self: bool,
+ /// Whether the invalidation matched, either invalidating the element or
+ /// generating another invalidation.
+ matched: bool,
+}
+
+/// The result of a whole invalidation process for a given element.
+pub struct InvalidationResult {
+ /// Whether the element itself was invalidated.
+ invalidated_self: bool,
+ /// Whether the element's descendants were invalidated.
+ invalidated_descendants: bool,
+ /// Whether the element's siblings were invalidated.
+ invalidated_siblings: bool,
+}
+
+impl InvalidationResult {
+ /// Create an emtpy result.
+ pub fn empty() -> Self {
+ Self {
+ invalidated_self: false,
+ invalidated_descendants: false,
+ invalidated_siblings: false,
+ }
+ }
+
+ /// Whether the invalidation has invalidate the element itself.
+ pub fn has_invalidated_self(&self) -> bool {
+ self.invalidated_self
+ }
+
+ /// Whether the invalidation has invalidate desendants.
+ pub fn has_invalidated_descendants(&self) -> bool {
+ self.invalidated_descendants
+ }
+
+ /// Whether the invalidation has invalidate siblings.
+ pub fn has_invalidated_siblings(&self) -> bool {
+ self.invalidated_siblings
+ }
+}
+
+impl<'a, 'b, 'c, E, P: 'a> TreeStyleInvalidator<'a, 'b, 'c, E, P>
+where
+ 'b: 'a,
+ E: TElement,
+ P: InvalidationProcessor<'b, 'c, E>,
+{
+ /// Trivially constructs a new `TreeStyleInvalidator`.
+ pub fn new(
+ element: E,
+ stack_limit_checker: Option<&'a StackLimitChecker>,
+ processor: &'a mut P,
+ ) -> Self {
+ Self {
+ element,
+ stack_limit_checker,
+ processor,
+ _marker: std::marker::PhantomData,
+ }
+ }
+
+ /// Perform the invalidation pass.
+ pub fn invalidate(mut self) -> InvalidationResult {
+ debug!("StyleTreeInvalidator::invalidate({:?})", self.element);
+
+ let mut self_invalidations = InvalidationVector::new();
+ let mut descendant_invalidations = DescendantInvalidationLists::default();
+ let mut sibling_invalidations = InvalidationVector::new();
+
+ let mut invalidated_self = self.processor.collect_invalidations(
+ self.element,
+ &mut self_invalidations,
+ &mut descendant_invalidations,
+ &mut sibling_invalidations,
+ );
+
+ debug!("Collected invalidations (self: {}): ", invalidated_self);
+ debug!(
+ " > self: {}, {:?}",
+ self_invalidations.len(),
+ self_invalidations
+ );
+ debug!(" > descendants: {:?}", descendant_invalidations);
+ debug!(
+ " > siblings: {}, {:?}",
+ sibling_invalidations.len(),
+ sibling_invalidations
+ );
+
+ let invalidated_self_from_collection = invalidated_self;
+
+ invalidated_self |= self.process_descendant_invalidations(
+ &self_invalidations,
+ &mut descendant_invalidations,
+ &mut sibling_invalidations,
+ DescendantInvalidationKind::Dom,
+ );
+
+ if invalidated_self && !invalidated_self_from_collection {
+ self.processor.invalidated_self(self.element);
+ }
+
+ let invalidated_descendants = self.invalidate_descendants(&descendant_invalidations);
+ let invalidated_siblings = self.invalidate_siblings(&mut sibling_invalidations);
+
+ InvalidationResult {
+ invalidated_self,
+ invalidated_descendants,
+ invalidated_siblings,
+ }
+ }
+
+ /// Go through later DOM siblings, invalidating style as needed using the
+ /// `sibling_invalidations` list.
+ ///
+ /// Returns whether any sibling's style or any sibling descendant's style
+ /// was invalidated.
+ fn invalidate_siblings(&mut self, sibling_invalidations: &mut InvalidationVector<'b>) -> bool {
+ if sibling_invalidations.is_empty() {
+ return false;
+ }
+
+ let mut current = self
+ .processor
+ .sibling_traversal_map()
+ .next_sibling_for(&self.element);
+ let mut any_invalidated = false;
+
+ while let Some(sibling) = current {
+ let mut sibling_invalidator =
+ TreeStyleInvalidator::new(sibling, self.stack_limit_checker, self.processor);
+
+ let mut invalidations_for_descendants = DescendantInvalidationLists::default();
+ let invalidated_sibling = sibling_invalidator.process_sibling_invalidations(
+ &mut invalidations_for_descendants,
+ sibling_invalidations,
+ );
+
+ if invalidated_sibling {
+ sibling_invalidator
+ .processor
+ .invalidated_sibling(sibling, self.element);
+ }
+
+ any_invalidated |= invalidated_sibling;
+
+ any_invalidated |=
+ sibling_invalidator.invalidate_descendants(&invalidations_for_descendants);
+
+ if sibling_invalidations.is_empty() {
+ break;
+ }
+
+ current = self
+ .processor
+ .sibling_traversal_map()
+ .next_sibling_for(&sibling);
+ }
+
+ any_invalidated
+ }
+
+ fn invalidate_pseudo_element_or_nac(
+ &mut self,
+ child: E,
+ invalidations: &[Invalidation<'b>],
+ ) -> bool {
+ let mut sibling_invalidations = InvalidationVector::new();
+
+ let result = self.invalidate_child(
+ child,
+ invalidations,
+ &mut sibling_invalidations,
+ DescendantInvalidationKind::Dom,
+ );
+
+ // Roots of NAC subtrees can indeed generate sibling invalidations, but
+ // they can be just ignored, since they have no siblings.
+ //
+ // Note that we can end up testing selectors that wouldn't end up
+ // matching due to this being NAC, like those coming from document
+ // rules, but we overinvalidate instead of checking this.
+
+ result
+ }
+
+ /// Invalidate a child and recurse down invalidating its descendants if
+ /// needed.
+ fn invalidate_child(
+ &mut self,
+ child: E,
+ invalidations: &[Invalidation<'b>],
+ sibling_invalidations: &mut InvalidationVector<'b>,
+ descendant_invalidation_kind: DescendantInvalidationKind,
+ ) -> bool {
+ let mut invalidations_for_descendants = DescendantInvalidationLists::default();
+
+ let mut invalidated_child = false;
+ let invalidated_descendants = {
+ let mut child_invalidator =
+ TreeStyleInvalidator::new(child, self.stack_limit_checker, self.processor);
+
+ invalidated_child |= child_invalidator.process_sibling_invalidations(
+ &mut invalidations_for_descendants,
+ sibling_invalidations,
+ );
+
+ invalidated_child |= child_invalidator.process_descendant_invalidations(
+ invalidations,
+ &mut invalidations_for_descendants,
+ sibling_invalidations,
+ descendant_invalidation_kind,
+ );
+
+ if invalidated_child {
+ child_invalidator.processor.invalidated_self(child);
+ }
+
+ child_invalidator.invalidate_descendants(&invalidations_for_descendants)
+ };
+
+ // The child may not be a flattened tree child of the current element,
+ // but may be arbitrarily deep.
+ //
+ // Since we keep the traversal flags in terms of the flattened tree,
+ // we need to propagate it as appropriate.
+ if invalidated_child || invalidated_descendants {
+ self.processor.invalidated_descendants(self.element, child);
+ }
+
+ invalidated_child || invalidated_descendants
+ }
+
+ fn invalidate_nac(&mut self, invalidations: &[Invalidation<'b>]) -> bool {
+ let mut any_nac_root = false;
+
+ let element = self.element;
+ element.each_anonymous_content_child(|nac| {
+ any_nac_root |= self.invalidate_pseudo_element_or_nac(nac, invalidations);
+ });
+
+ any_nac_root
+ }
+
+ // NB: It's important that this operates on DOM children, which is what
+ // selector-matching operates on.
+ fn invalidate_dom_descendants_of(
+ &mut self,
+ parent: E::ConcreteNode,
+ invalidations: &[Invalidation<'b>],
+ ) -> bool {
+ let mut any_descendant = false;
+
+ let mut sibling_invalidations = InvalidationVector::new();
+ for child in parent.dom_children() {
+ let child = match child.as_element() {
+ Some(e) => e,
+ None => continue,
+ };
+
+ any_descendant |= self.invalidate_child(
+ child,
+ invalidations,
+ &mut sibling_invalidations,
+ DescendantInvalidationKind::Dom,
+ );
+ }
+
+ any_descendant
+ }
+
+ fn invalidate_parts_in_shadow_tree(
+ &mut self,
+ shadow: <E::ConcreteNode as TNode>::ConcreteShadowRoot,
+ invalidations: &[Invalidation<'b>],
+ ) -> bool {
+ debug_assert!(!invalidations.is_empty());
+
+ let mut any = false;
+ let mut sibling_invalidations = InvalidationVector::new();
+
+ for node in shadow.as_node().dom_descendants() {
+ let element = match node.as_element() {
+ Some(e) => e,
+ None => continue,
+ };
+
+ if element.has_part_attr() {
+ any |= self.invalidate_child(
+ element,
+ invalidations,
+ &mut sibling_invalidations,
+ DescendantInvalidationKind::Part,
+ );
+ debug_assert!(
+ sibling_invalidations.is_empty(),
+ "::part() shouldn't have sibling combinators to the right, \
+ this makes no sense! {:?}",
+ sibling_invalidations
+ );
+ }
+
+ if let Some(shadow) = element.shadow_root() {
+ if element.exports_any_part() {
+ any |= self.invalidate_parts_in_shadow_tree(shadow, invalidations)
+ }
+ }
+ }
+
+ any
+ }
+
+ fn invalidate_parts(&mut self, invalidations: &[Invalidation<'b>]) -> bool {
+ if invalidations.is_empty() {
+ return false;
+ }
+
+ let shadow = match self.element.shadow_root() {
+ Some(s) => s,
+ None => return false,
+ };
+
+ self.invalidate_parts_in_shadow_tree(shadow, invalidations)
+ }
+
+ fn invalidate_slotted_elements(&mut self, invalidations: &[Invalidation<'b>]) -> bool {
+ if invalidations.is_empty() {
+ return false;
+ }
+
+ let slot = self.element;
+ self.invalidate_slotted_elements_in_slot(slot, invalidations)
+ }
+
+ fn invalidate_slotted_elements_in_slot(
+ &mut self,
+ slot: E,
+ invalidations: &[Invalidation<'b>],
+ ) -> bool {
+ let mut any = false;
+
+ let mut sibling_invalidations = InvalidationVector::new();
+ for node in slot.slotted_nodes() {
+ let element = match node.as_element() {
+ Some(e) => e,
+ None => continue,
+ };
+
+ if element.is_html_slot_element() {
+ any |= self.invalidate_slotted_elements_in_slot(element, invalidations);
+ } else {
+ any |= self.invalidate_child(
+ element,
+ invalidations,
+ &mut sibling_invalidations,
+ DescendantInvalidationKind::Slotted,
+ );
+ }
+
+ debug_assert!(
+ sibling_invalidations.is_empty(),
+ "::slotted() shouldn't have sibling combinators to the right, \
+ this makes no sense! {:?}",
+ sibling_invalidations
+ );
+ }
+
+ any
+ }
+
+ fn invalidate_non_slotted_descendants(&mut self, invalidations: &[Invalidation<'b>]) -> bool {
+ if invalidations.is_empty() {
+ return false;
+ }
+
+ if self.processor.light_tree_only() {
+ let node = self.element.as_node();
+ return self.invalidate_dom_descendants_of(node, invalidations);
+ }
+
+ let mut any_descendant = false;
+
+ // NOTE(emilio): This is only needed for Shadow DOM to invalidate
+ // correctly on :host(..) changes. Instead of doing this, we could add
+ // a third kind of invalidation list that walks shadow root children,
+ // but it's not clear it's worth it.
+ //
+ // Also, it's needed as of right now for document state invalidation,
+ // where we rely on iterating every element that ends up in the composed
+ // doc, but we could fix that invalidating per subtree.
+ if let Some(root) = self.element.shadow_root() {
+ any_descendant |= self.invalidate_dom_descendants_of(root.as_node(), invalidations);
+ }
+
+ if let Some(marker) = self.element.marker_pseudo_element() {
+ any_descendant |= self.invalidate_pseudo_element_or_nac(marker, invalidations);
+ }
+
+ if let Some(before) = self.element.before_pseudo_element() {
+ any_descendant |= self.invalidate_pseudo_element_or_nac(before, invalidations);
+ }
+
+ let node = self.element.as_node();
+ any_descendant |= self.invalidate_dom_descendants_of(node, invalidations);
+
+ if let Some(after) = self.element.after_pseudo_element() {
+ any_descendant |= self.invalidate_pseudo_element_or_nac(after, invalidations);
+ }
+
+ any_descendant |= self.invalidate_nac(invalidations);
+
+ any_descendant
+ }
+
+ /// Given the descendant invalidation lists, go through the current
+ /// element's descendants, and invalidate style on them.
+ fn invalidate_descendants(&mut self, invalidations: &DescendantInvalidationLists<'b>) -> bool {
+ if invalidations.is_empty() {
+ return false;
+ }
+
+ debug!(
+ "StyleTreeInvalidator::invalidate_descendants({:?})",
+ self.element
+ );
+ debug!(" > {:?}", invalidations);
+
+ let should_process = self.processor.should_process_descendants(self.element);
+
+ if !should_process {
+ return false;
+ }
+
+ if let Some(checker) = self.stack_limit_checker {
+ if checker.limit_exceeded() {
+ self.processor.recursion_limit_exceeded(self.element);
+ return true;
+ }
+ }
+
+ let mut any_descendant = false;
+
+ any_descendant |= self.invalidate_non_slotted_descendants(&invalidations.dom_descendants);
+ any_descendant |= self.invalidate_slotted_elements(&invalidations.slotted_descendants);
+ any_descendant |= self.invalidate_parts(&invalidations.parts);
+
+ any_descendant
+ }
+
+ /// Process the given sibling invalidations coming from our previous
+ /// sibling.
+ ///
+ /// The sibling invalidations are somewhat special because they can be
+ /// modified on the fly. New invalidations may be added and removed.
+ ///
+ /// In particular, all descendants get the same set of invalidations from
+ /// the parent, but the invalidations from a given sibling depend on the
+ /// ones we got from the previous one.
+ ///
+ /// Returns whether invalidated the current element's style.
+ fn process_sibling_invalidations(
+ &mut self,
+ descendant_invalidations: &mut DescendantInvalidationLists<'b>,
+ sibling_invalidations: &mut InvalidationVector<'b>,
+ ) -> bool {
+ let mut i = 0;
+ let mut new_sibling_invalidations = InvalidationVector::new();
+ let mut invalidated_self = false;
+
+ while i < sibling_invalidations.len() {
+ let result = self.process_invalidation(
+ &sibling_invalidations[i],
+ descendant_invalidations,
+ &mut new_sibling_invalidations,
+ InvalidationKind::Sibling,
+ );
+
+ invalidated_self |= result.invalidated_self;
+ sibling_invalidations[i].matched_by_any_previous |= result.matched;
+ if sibling_invalidations[i].effective_for_next() {
+ i += 1;
+ } else {
+ sibling_invalidations.remove(i);
+ }
+ }
+
+ sibling_invalidations.extend(new_sibling_invalidations.drain(..));
+ invalidated_self
+ }
+
+ /// Process a given invalidation list coming from our parent,
+ /// adding to `descendant_invalidations` and `sibling_invalidations` as
+ /// needed.
+ ///
+ /// Returns whether our style was invalidated as a result.
+ fn process_descendant_invalidations(
+ &mut self,
+ invalidations: &[Invalidation<'b>],
+ descendant_invalidations: &mut DescendantInvalidationLists<'b>,
+ sibling_invalidations: &mut InvalidationVector<'b>,
+ descendant_invalidation_kind: DescendantInvalidationKind,
+ ) -> bool {
+ let mut invalidated = false;
+
+ for invalidation in invalidations {
+ let result = self.process_invalidation(
+ invalidation,
+ descendant_invalidations,
+ sibling_invalidations,
+ InvalidationKind::Descendant(descendant_invalidation_kind),
+ );
+
+ invalidated |= result.invalidated_self;
+ if invalidation.effective_for_next() {
+ let mut invalidation = invalidation.clone();
+ invalidation.matched_by_any_previous |= result.matched;
+ debug_assert_eq!(
+ descendant_invalidation_kind,
+ DescendantInvalidationKind::Dom,
+ "Slotted or part invalidations don't propagate."
+ );
+ descendant_invalidations.dom_descendants.push(invalidation);
+ }
+ }
+
+ invalidated
+ }
+
+ /// Processes a given invalidation, potentially invalidating the style of
+ /// the current element.
+ ///
+ /// Returns whether invalidated the style of the element, and whether the
+ /// invalidation should be effective to subsequent siblings or descendants
+ /// down in the tree.
+ fn process_invalidation(
+ &mut self,
+ invalidation: &Invalidation<'b>,
+ descendant_invalidations: &mut DescendantInvalidationLists<'b>,
+ sibling_invalidations: &mut InvalidationVector<'b>,
+ invalidation_kind: InvalidationKind,
+ ) -> SingleInvalidationResult {
+ debug!(
+ "TreeStyleInvalidator::process_invalidation({:?}, {:?}, {:?})",
+ self.element, invalidation, invalidation_kind
+ );
+
+ let matching_result = {
+ let context = self.processor.matching_context();
+ context.current_host = invalidation.scope;
+
+ matches_compound_selector_from(
+ &invalidation.dependency.selector,
+ invalidation.offset,
+ context,
+ &self.element,
+ )
+ };
+
+ let next_invalidation = match matching_result {
+ CompoundSelectorMatchingResult::NotMatched => {
+ return SingleInvalidationResult {
+ invalidated_self: false,
+ matched: false,
+ }
+ },
+ CompoundSelectorMatchingResult::FullyMatched => {
+ debug!(" > Invalidation matched completely");
+ // We matched completely. If we're an inner selector now we need
+ // to go outside our selector and carry on invalidating.
+ let mut cur_dependency = invalidation.dependency;
+ loop {
+ cur_dependency = match cur_dependency.parent {
+ None => {
+ return SingleInvalidationResult {
+ invalidated_self: true,
+ matched: true,
+ }
+ },
+ Some(ref p) => {
+ let invalidation_kind = p.invalidation_kind();
+ match invalidation_kind {
+ DependencyInvalidationKind::Normal(_) => &**p,
+ DependencyInvalidationKind::Relative(kind) => {
+ self.processor.found_relative_selector_invalidation(
+ self.element,
+ kind,
+ &**p,
+ );
+ return SingleInvalidationResult {
+ invalidated_self: false,
+ matched: true,
+ };
+ },
+ }
+ },
+ };
+
+ debug!(" > Checking outer dependency {:?}", cur_dependency);
+
+ // The inner selector changed, now check if the full
+ // previous part of the selector did, before keeping
+ // checking for descendants.
+ if !self
+ .processor
+ .check_outer_dependency(cur_dependency, self.element)
+ {
+ return SingleInvalidationResult {
+ invalidated_self: false,
+ matched: false,
+ };
+ }
+
+ if cur_dependency.normal_invalidation_kind() ==
+ NormalDependencyInvalidationKind::Element
+ {
+ continue;
+ }
+
+ debug!(" > Generating invalidation");
+ break Invalidation::new(cur_dependency, invalidation.scope);
+ }
+ },
+ CompoundSelectorMatchingResult::Matched {
+ next_combinator_offset,
+ } => Invalidation {
+ dependency: invalidation.dependency,
+ scope: invalidation.scope,
+ offset: next_combinator_offset + 1,
+ matched_by_any_previous: false,
+ },
+ };
+
+ debug_assert_ne!(
+ next_invalidation.offset, 0,
+ "Rightmost selectors shouldn't generate more invalidations",
+ );
+
+ let mut invalidated_self = false;
+ let next_combinator = next_invalidation
+ .dependency
+ .selector
+ .combinator_at_parse_order(next_invalidation.offset - 1);
+
+ if matches!(next_combinator, Combinator::PseudoElement) &&
+ self.processor.invalidates_on_pseudo_element()
+ {
+ // We need to invalidate the element whenever pseudos change, for
+ // two reasons:
+ //
+ // * Eager pseudo styles are stored as part of the originating
+ // element's computed style.
+ //
+ // * Lazy pseudo-styles might be cached on the originating
+ // element's pseudo-style cache.
+ //
+ // This could be more fine-grained (perhaps with a RESTYLE_PSEUDOS
+ // hint?).
+ //
+ // Note that we'll also restyle the pseudo-element because it would
+ // match this invalidation.
+ //
+ // FIXME: For non-element-backed pseudos this is still not quite
+ // correct. For example for ::selection even though we invalidate
+ // the style properly there's nothing that triggers a repaint
+ // necessarily. Though this matches old Gecko behavior, and the
+ // ::selection implementation needs to change significantly anyway
+ // to implement https://github.com/w3c/csswg-drafts/issues/2474 for
+ // example.
+ invalidated_self = true;
+ }
+
+ debug!(
+ " > Invalidation matched, next: {:?}, ({:?})",
+ next_invalidation, next_combinator
+ );
+
+ let next_invalidation_kind = next_invalidation.kind();
+
+ // We can skip pushing under some circumstances, and we should
+ // because otherwise the invalidation list could grow
+ // exponentially.
+ //
+ // * First of all, both invalidations need to be of the same
+ // kind. This is because of how we propagate them going to
+ // the right of the tree for sibling invalidations and going
+ // down the tree for children invalidations. A sibling
+ // invalidation that ends up generating a children
+ // invalidation ends up (correctly) in five different lists,
+ // not in the same list five different times.
+ //
+ // * Then, the invalidation needs to be matched by a previous
+ // ancestor/sibling, in order to know that this invalidation
+ // has been generated already.
+ //
+ // * Finally, the new invalidation needs to be
+ // `effective_for_next()`, in order for us to know that it is
+ // still in the list, since we remove the dependencies that
+ // aren't from the lists for our children / siblings.
+ //
+ // To go through an example, let's imagine we are processing a
+ // dom subtree like:
+ //
+ // <div><address><div><div/></div></address></div>
+ //
+ // And an invalidation list with a single invalidation like:
+ //
+ // [div div div]
+ //
+ // When we process the invalidation list for the outer div, we
+ // match it, and generate a `div div` invalidation, so for the
+ // <address> child we have:
+ //
+ // [div div div, div div]
+ //
+ // With the first of them marked as `matched`.
+ //
+ // When we process the <address> child, we don't match any of
+ // them, so both invalidations go untouched to our children.
+ //
+ // When we process the second <div>, we match _both_
+ // invalidations.
+ //
+ // However, when matching the first, we can tell it's been
+ // matched, and not push the corresponding `div div`
+ // invalidation, since we know it's necessarily already on the
+ // list.
+ //
+ // Thus, without skipping the push, we'll arrive to the
+ // innermost <div> with:
+ //
+ // [div div div, div div, div div, div]
+ //
+ // While skipping it, we won't arrive here with duplicating
+ // dependencies:
+ //
+ // [div div div, div div, div]
+ //
+ let can_skip_pushing = next_invalidation_kind == invalidation_kind &&
+ invalidation.matched_by_any_previous &&
+ next_invalidation.effective_for_next();
+
+ if can_skip_pushing {
+ debug!(
+ " > Can avoid push, since the invalidation had \
+ already been matched before"
+ );
+ } else {
+ match next_invalidation_kind {
+ InvalidationKind::Descendant(DescendantInvalidationKind::Dom) => {
+ descendant_invalidations
+ .dom_descendants
+ .push(next_invalidation);
+ },
+ InvalidationKind::Descendant(DescendantInvalidationKind::Part) => {
+ descendant_invalidations.parts.push(next_invalidation);
+ },
+ InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) => {
+ descendant_invalidations
+ .slotted_descendants
+ .push(next_invalidation);
+ },
+ InvalidationKind::Sibling => {
+ sibling_invalidations.push(next_invalidation);
+ },
+ }
+ }
+
+ SingleInvalidationResult {
+ invalidated_self,
+ matched: true,
+ }
+ }
+}
diff --git a/servo/components/style/invalidation/element/mod.rs b/servo/components/style/invalidation/element/mod.rs
new file mode 100644
index 0000000000..0ddb9f1863
--- /dev/null
+++ b/servo/components/style/invalidation/element/mod.rs
@@ -0,0 +1,13 @@
+/* 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/. */
+
+//! Invalidation of element styles due to attribute or style changes.
+
+pub mod document_state;
+pub mod element_wrapper;
+pub mod invalidation_map;
+pub mod invalidator;
+pub mod relative_selector;
+pub mod restyle_hints;
+pub mod state_and_attributes;
diff --git a/servo/components/style/invalidation/element/relative_selector.rs b/servo/components/style/invalidation/element/relative_selector.rs
new file mode 100644
index 0000000000..ccb48e349f
--- /dev/null
+++ b/servo/components/style/invalidation/element/relative_selector.rs
@@ -0,0 +1,1164 @@
+/* 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/. */
+
+//! Invalidation of element styles relative selectors.
+
+use crate::data::ElementData;
+use crate::dom::{TElement, TNode};
+use crate::gecko_bindings::structs::ServoElementSnapshotTable;
+use crate::invalidation::element::element_wrapper::ElementWrapper;
+use crate::invalidation::element::invalidation_map::{
+ Dependency, DependencyInvalidationKind, NormalDependencyInvalidationKind,
+ RelativeDependencyInvalidationKind, RelativeSelectorInvalidationMap,
+};
+use crate::invalidation::element::invalidator::{
+ DescendantInvalidationLists, Invalidation, InvalidationProcessor, InvalidationResult,
+ InvalidationVector, SiblingTraversalMap, TreeStyleInvalidator,
+};
+use crate::invalidation::element::restyle_hints::RestyleHint;
+use crate::invalidation::element::state_and_attributes::{
+ check_dependency, dependency_may_be_relevant, invalidated_descendants, invalidated_self,
+ invalidated_sibling, push_invalidation, should_process_descendants,
+};
+use crate::stylist::{CascadeData, Stylist};
+use dom::ElementState;
+use fxhash::FxHashMap;
+use selectors::matching::{
+ matches_compound_selector_from, matches_selector, CompoundSelectorMatchingResult,
+ ElementSelectorFlags, MatchingContext, MatchingForInvalidation, MatchingMode,
+ NeedsSelectorFlags, QuirksMode, SelectorCaches, VisitedHandlingMode,
+};
+use selectors::parser::{Combinator, SelectorKey};
+use selectors::OpaqueElement;
+use smallvec::SmallVec;
+use std::ops::DerefMut;
+
+/// Kind of DOM mutation this relative selector invalidation is being carried out in.
+#[derive(Clone, Copy)]
+pub enum DomMutationOperation {
+ /// Insertion operation, can cause side effect, but presumed already happened.
+ Insert,
+ /// Append operation, cannot cause side effect.
+ Append,
+ /// Removal operation, can cause side effect, but presumed already happened. Sibling relationships are destroyed.
+ Remove,
+ /// Invalidating for side effect of a DOM operation, for the previous sibling.
+ SideEffectPrevSibling,
+ /// Invalidating for side effect of a DOM operation, for the next sibling.
+ SideEffectNextSibling,
+}
+
+impl DomMutationOperation {
+ fn accept<E: TElement>(&self, d: &Dependency, e: E) -> bool {
+ match self {
+ Self::Insert | Self::Append | Self::Remove => {
+ e.relative_selector_search_direction().is_some()
+ },
+ // `:has(+ .a + .b)` with `.anchor + .a + .remove + .b` - `.a` would be present
+ // in the search path.
+ Self::SideEffectPrevSibling => {
+ e.relative_selector_search_direction().is_some() &&
+ d.right_combinator_is_next_sibling()
+ },
+ // If an element is being removed and would cause next-sibling match to happen,
+ // e.g. `:has(+ .a)` with `.anchor + .remove + .a`, `.a` isn't yet searched
+ // for relative selector matching.
+ Self::SideEffectNextSibling => d.dependency_is_relative_with_single_next_sibling(),
+ }
+ }
+
+ fn is_side_effect(&self) -> bool {
+ match self {
+ Self::Insert | Self::Append | Self::Remove => false,
+ Self::SideEffectPrevSibling | Self::SideEffectNextSibling => true,
+ }
+ }
+}
+
+/// Context required to try and optimize away relative dependencies.
+struct OptimizationContext<'a, E: TElement> {
+ sibling_traversal_map: &'a SiblingTraversalMap<E>,
+ quirks_mode: QuirksMode,
+ operation: DomMutationOperation,
+}
+
+impl<'a, E: TElement> OptimizationContext<'a, E> {
+ fn can_be_ignored(
+ &self,
+ is_subtree: bool,
+ element: E,
+ host: Option<OpaqueElement>,
+ dependency: &Dependency,
+ ) -> bool {
+ if is_subtree {
+ // Subtree elements don't have unaffected sibling to look at.
+ return false;
+ }
+ debug_assert!(
+ matches!(
+ dependency.invalidation_kind(),
+ DependencyInvalidationKind::Relative(..)
+ ),
+ "Non-relative selector being evaluated for optimization"
+ );
+ // This optimization predecates on the fact that there may be a sibling that can readily
+ // "take over" this element.
+ let sibling = match self.sibling_traversal_map.prev_sibling_for(&element) {
+ None => {
+ if matches!(self.operation, DomMutationOperation::Append) {
+ return false;
+ }
+ match self.sibling_traversal_map.next_sibling_for(&element) {
+ Some(s) => s,
+ None => return false,
+ }
+ },
+ Some(s) => s,
+ };
+ {
+ // Run through the affected compund.
+ let mut iter = dependency.selector.iter_from(dependency.selector_offset);
+ while let Some(c) = iter.next() {
+ if c.has_indexed_selector_in_subject() {
+ // We do not calculate indices during invalidation as they're wasteful - as a side effect,
+ // such selectors always return true, breaking this optimization. Note that we only check
+ // this compound only because the check to skip compares against this element's sibling.
+ // i.e. Given `:has(:nth-child(2) .foo)`, we'd try to find `.foo`'s sibling, which
+ // shares `:nth-child` up the selector.
+ return false;
+ }
+ }
+ }
+ let is_rightmost = dependency.selector_offset == 0;
+ if !is_rightmost {
+ let combinator = dependency
+ .selector
+ .combinator_at_match_order(dependency.selector_offset - 1);
+ if combinator.is_ancestor() {
+ // We can safely ignore these, since we're about to traverse the
+ // rest of the affected tree anyway to find the rightmost invalidated element.
+ return true;
+ }
+ if combinator.is_sibling() && matches!(self.operation, DomMutationOperation::Append) {
+ // If we're in the subtree, same argument applies as ancestor combinator case.
+ // If we're at the top of the DOM tree being mutated, we can ignore it if the
+ // operation is append - we know we'll cover all the later siblings and their descendants.
+ return true;
+ }
+ }
+ let mut caches = SelectorCaches::default();
+ let mut matching_context = MatchingContext::new(
+ MatchingMode::Normal,
+ None,
+ &mut caches,
+ self.quirks_mode,
+ NeedsSelectorFlags::No,
+ MatchingForInvalidation::Yes,
+ );
+ matching_context.current_host = host;
+ let sibling_matches = matches_selector(
+ &dependency.selector,
+ dependency.selector_offset,
+ None,
+ &sibling,
+ &mut matching_context,
+ );
+ if sibling_matches {
+ // Remember that at this point, we know that the combinator to the right of this
+ // compound is a sibling combinator. Effectively, we've found a standin for the
+ // element we're mutating.
+ // e.g. Given `:has(... .a ~ .b ...)`, we're the mutating element matching `... .a`,
+ // if we find a sibling that matches the `... .a`, it can stand in for us.
+ debug_assert!(dependency.parent.is_some(), "No relative selector outer dependency?");
+ return dependency.parent.as_ref().map_or(false, |par| {
+ // ... However, if the standin sibling can be the anchor, we can't skip it, since
+ // that sibling should be invlidated to become the anchor.
+ !matches_selector(
+ &par.selector,
+ par.selector_offset,
+ None,
+ &sibling,
+ &mut matching_context
+ )
+ });
+ }
+ // Ok, there's no standin element - but would this element have matched the upstream
+ // selector anyway? If we don't, either the match exists somewhere far from us
+ // (In which case our mutation doesn't really matter), or it doesn't exist at all,
+ // so we can just skip the invalidation.
+ let (combinator, prev_offset) = {
+ let mut iter = dependency.selector.iter_from(dependency.selector_offset);
+ let mut o = dependency.selector_offset;
+ while iter.next().is_some() {
+ o += 1;
+ }
+ let combinator = iter.next_sequence();
+ o += 1;
+ debug_assert!(
+ combinator.is_some(),
+ "Should at least see a relative combinator"
+ );
+ (combinator.unwrap(), o)
+ };
+ if combinator.is_sibling() {
+ if prev_offset >= dependency.selector.len() - 1 {
+ // Hit the relative combinator - we don't have enough information to
+ // see if there's going to be a downstream match.
+ return false;
+ }
+ if matches!(self.operation, DomMutationOperation::Remove) {
+ // This is sad :( The sibling relation of a removed element is lost, and we don't
+ // propagate sibling traversal map to selector matching context, so we need to do
+ // manual matching here. TODO(dshin): Worth changing selector matching for this?
+
+ // Try matching this compound, then...
+ // Note: We'll not hit the leftmost sequence (Since we would have returned early
+ // if we'd hit the relative selector anchor).
+ if matches!(
+ matches_compound_selector_from(
+ &dependency.selector,
+ dependency.selector.len() - prev_offset + 1,
+ &mut matching_context,
+ &element
+ ),
+ CompoundSelectorMatchingResult::NotMatched
+ ) {
+ return true;
+ }
+
+ // ... Match the rest of the selector, manually traversing.
+ let mut prev_sibling = self.sibling_traversal_map.prev_sibling_for(&element);
+ while let Some(sib) = prev_sibling {
+ if matches_selector(
+ &dependency.selector,
+ prev_offset,
+ None,
+ &sib,
+ &mut matching_context,
+ ) {
+ return false;
+ }
+ if matches!(combinator, Combinator::NextSibling) {
+ break;
+ }
+ prev_sibling = self.sibling_traversal_map.prev_sibling_for(&sib);
+ }
+ return true;
+ }
+ }
+ !matches_selector(
+ &dependency.selector,
+ dependency.selector_offset,
+ None,
+ &element,
+ &mut matching_context,
+ )
+ }
+}
+
+/// Overall invalidator for handling relative selector invalidations.
+pub struct RelativeSelectorInvalidator<'a, 'b, E>
+where
+ E: TElement + 'a,
+{
+ /// Element triggering the invalidation.
+ pub element: E,
+ /// Quirks mode of the current invalidation.
+ pub quirks_mode: QuirksMode,
+ /// Snapshot containing changes to invalidate against.
+ /// Can be None if it's a DOM mutation.
+ pub snapshot_table: Option<&'b ServoElementSnapshotTable>,
+ /// Callback to trigger when the subject element is invalidated.
+ pub invalidated: fn(E, &InvalidationResult),
+ /// The traversal map that should be used to process invalidations.
+ pub sibling_traversal_map: SiblingTraversalMap<E>,
+ /// Marker for 'a lifetime.
+ pub _marker: ::std::marker::PhantomData<&'a ()>,
+}
+
+struct RelativeSelectorInvalidation<'a> {
+ host: Option<OpaqueElement>,
+ kind: RelativeDependencyInvalidationKind,
+ dependency: &'a Dependency,
+}
+
+type ElementDependencies<'a> = SmallVec<[(Option<OpaqueElement>, &'a Dependency); 1]>;
+type Dependencies<'a, E> = SmallVec<[(E, ElementDependencies<'a>); 1]>;
+type AlreadyInvalidated<'a, E> = SmallVec<[(E, Option<OpaqueElement>, &'a Dependency); 2]>;
+
+/// Interface for collecting relative selector dependencies.
+pub struct RelativeSelectorDependencyCollector<'a, E>
+where
+ E: TElement,
+{
+ /// Dependencies that need to run through the normal invalidation that may generate
+ /// a relative selector invalidation.
+ dependencies: FxHashMap<E, ElementDependencies<'a>>,
+ /// Dependencies that created an invalidation right away.
+ invalidations: AlreadyInvalidated<'a, E>,
+ /// The top element in the subtree being invalidated.
+ top: E,
+ /// Optional context that will be used to try and skip invalidations
+ /// by running selector matches.
+ optimization_context: Option<OptimizationContext<'a, E>>,
+}
+
+type Invalidations<'a> = SmallVec<[RelativeSelectorInvalidation<'a>; 1]>;
+type InnerInvalidations<'a, E> = SmallVec<[(E, RelativeSelectorInvalidation<'a>); 1]>;
+
+struct ToInvalidate<'a, E: TElement + 'a> {
+ /// Dependencies to run through normal invalidator.
+ dependencies: Dependencies<'a, E>,
+ /// Dependencies already invalidated.
+ invalidations: Invalidations<'a>,
+}
+
+impl<'a, E: TElement + 'a> Default for ToInvalidate<'a, E> {
+ fn default() -> Self {
+ Self {
+ dependencies: Dependencies::default(),
+ invalidations: Invalidations::default(),
+ }
+ }
+}
+
+fn dependency_selectors_match(a: &Dependency, b: &Dependency) -> bool {
+ if a.invalidation_kind() != b.invalidation_kind() {
+ return false;
+ }
+ if SelectorKey::new(&a.selector) != SelectorKey::new(&b.selector) {
+ return false;
+ }
+ let mut a_parent = a.parent.as_ref();
+ let mut b_parent = b.parent.as_ref();
+ while let (Some(a_p), Some(b_p)) = (a_parent, b_parent) {
+ if SelectorKey::new(&a_p.selector) != SelectorKey::new(&b_p.selector) {
+ return false;
+ }
+ a_parent = a_p.parent.as_ref();
+ b_parent = b_p.parent.as_ref();
+ }
+ a_parent.is_none() && b_parent.is_none()
+}
+
+impl<'a, E> RelativeSelectorDependencyCollector<'a, E>
+where
+ E: TElement,
+{
+ fn new(top: E, optimization_context: Option<OptimizationContext<'a, E>>) -> Self {
+ Self {
+ dependencies: FxHashMap::default(),
+ invalidations: AlreadyInvalidated::default(),
+ top,
+ optimization_context,
+ }
+ }
+
+ fn insert_invalidation(
+ &mut self,
+ element: E,
+ dependency: &'a Dependency,
+ host: Option<OpaqueElement>,
+ ) {
+ match self
+ .invalidations
+ .iter_mut()
+ .find(|(_, _, d)| dependency_selectors_match(dependency, d))
+ {
+ Some((e, h, d)) => {
+ // Just keep one.
+ if d.selector_offset > dependency.selector_offset {
+ (*e, *h, *d) = (element, host, dependency);
+ }
+ },
+ None => {
+ self.invalidations.push((element, host, dependency));
+ },
+ }
+ }
+
+ /// Add this dependency, if it is unique (i.e. Different outer dependency or same outer dependency
+ /// but requires a different invalidation traversal).
+ pub fn add_dependency(
+ &mut self,
+ dependency: &'a Dependency,
+ element: E,
+ host: Option<OpaqueElement>,
+ ) {
+ match dependency.invalidation_kind() {
+ DependencyInvalidationKind::Normal(..) => {
+ self.dependencies
+ .entry(element)
+ .and_modify(|v| v.push((host, dependency)))
+ .or_default()
+ .push((host, dependency));
+ },
+ DependencyInvalidationKind::Relative(kind) => {
+ debug_assert!(
+ dependency.parent.is_some(),
+ "Orphaned inner relative selector?"
+ );
+ if element != self.top &&
+ matches!(
+ kind,
+ RelativeDependencyInvalidationKind::Parent |
+ RelativeDependencyInvalidationKind::PrevSibling |
+ RelativeDependencyInvalidationKind::EarlierSibling
+ )
+ {
+ return;
+ }
+ self.insert_invalidation(element, dependency, host);
+ },
+ };
+ }
+
+ /// Get the dependencies in a list format.
+ fn get(self) -> ToInvalidate<'a, E> {
+ let mut result = ToInvalidate::default();
+ for (element, host, dependency) in self.invalidations {
+ match dependency.invalidation_kind() {
+ DependencyInvalidationKind::Normal(_) => {
+ unreachable!("Inner selector in invalidation?")
+ },
+ DependencyInvalidationKind::Relative(kind) => {
+ if let Some(context) = self.optimization_context.as_ref() {
+ if context.can_be_ignored(element != self.top, element, host, dependency) {
+ continue;
+ }
+ }
+ let dependency = dependency.parent.as_ref().unwrap();
+ result.invalidations.push(RelativeSelectorInvalidation {
+ kind,
+ host,
+ dependency,
+ });
+ // We move the invalidation up to the top of the subtree to avoid unnecessary traveral, but
+ // this means that we need to take ancestor-earlier sibling invalidations into account, as
+ // they'd look into earlier siblings of the top of the subtree as well.
+ if element != self.top &&
+ matches!(
+ kind,
+ RelativeDependencyInvalidationKind::AncestorEarlierSibling |
+ RelativeDependencyInvalidationKind::AncestorPrevSibling
+ )
+ {
+ result.invalidations.push(RelativeSelectorInvalidation {
+ kind: if matches!(
+ kind,
+ RelativeDependencyInvalidationKind::AncestorPrevSibling
+ ) {
+ RelativeDependencyInvalidationKind::PrevSibling
+ } else {
+ RelativeDependencyInvalidationKind::EarlierSibling
+ },
+ host,
+ dependency,
+ });
+ }
+ },
+ };
+ }
+ for (key, element_dependencies) in self.dependencies {
+ // At least for now, we don't try to optimize away dependencies emitted from nested selectors.
+ result.dependencies.push((key, element_dependencies));
+ }
+ result
+ }
+
+ fn collect_all_dependencies_for_element(
+ &mut self,
+ element: E,
+ scope: Option<OpaqueElement>,
+ quirks_mode: QuirksMode,
+ map: &'a RelativeSelectorInvalidationMap,
+ operation: DomMutationOperation,
+ ) {
+ element
+ .id()
+ .map(|v| match map.map.id_to_selector.get(v, quirks_mode) {
+ Some(v) => {
+ for dependency in v {
+ if !operation.accept(dependency, element) {
+ continue;
+ }
+ self.add_dependency(dependency, element, scope);
+ }
+ },
+ None => (),
+ });
+ element.each_class(|v| match map.map.class_to_selector.get(v, quirks_mode) {
+ Some(v) => {
+ for dependency in v {
+ if !operation.accept(dependency, element) {
+ continue;
+ }
+ self.add_dependency(dependency, element, scope);
+ }
+ },
+ None => (),
+ });
+ element.each_attr_name(
+ |v| match map.map.other_attribute_affecting_selectors.get(v) {
+ Some(v) => {
+ for dependency in v {
+ if !operation.accept(dependency, element) {
+ continue;
+ }
+ self.add_dependency(dependency, element, scope);
+ }
+ },
+ None => (),
+ },
+ );
+ let state = element.state();
+ map.map.state_affecting_selectors.lookup_with_additional(
+ element,
+ quirks_mode,
+ None,
+ &[],
+ ElementState::empty(),
+ |dependency| {
+ if !dependency.state.intersects(state) {
+ return true;
+ }
+ if !operation.accept(&dependency.dep, element) {
+ return true;
+ }
+ self.add_dependency(&dependency.dep, element, scope);
+ true
+ },
+ );
+
+ if let Some(v) = map.type_to_selector.get(element.local_name()) {
+ for dependency in v {
+ if !operation.accept(dependency, element) {
+ continue;
+ }
+ self.add_dependency(dependency, element, scope);
+ }
+ }
+
+ for dependency in &map.any_to_selector {
+ if !operation.accept(dependency, element) {
+ continue;
+ }
+ self.add_dependency(dependency, element, scope);
+ }
+ }
+
+ fn is_empty(&self) -> bool {
+ self.invalidations.is_empty() && self.dependencies.is_empty()
+ }
+}
+
+impl<'a, 'b, E> RelativeSelectorInvalidator<'a, 'b, E>
+where
+ E: TElement + 'a,
+{
+ /// Gather relative selector dependencies for the given element, and invalidate as necessary.
+ #[inline(never)]
+ pub fn invalidate_relative_selectors_for_this<F>(
+ self,
+ stylist: &'a Stylist,
+ mut gather_dependencies: F,
+ ) where
+ F: FnMut(
+ &E,
+ Option<OpaqueElement>,
+ &'a CascadeData,
+ QuirksMode,
+ &mut RelativeSelectorDependencyCollector<'a, E>,
+ ),
+ {
+ let mut collector = RelativeSelectorDependencyCollector::new(self.element, None);
+ stylist.for_each_cascade_data_with_scope(self.element, |data, scope| {
+ let map = data.relative_selector_invalidation_map();
+ if !map.used {
+ return;
+ }
+ gather_dependencies(
+ &self.element,
+ scope.map(|e| e.opaque()),
+ data,
+ self.quirks_mode,
+ &mut collector,
+ );
+ });
+ if collector.is_empty() {
+ return;
+ }
+ self.invalidate_from_dependencies(collector.get());
+ }
+
+ /// Gather relative selector dependencies for the given element (And its subtree) that mutated, and invalidate as necessary.
+ #[inline(never)]
+ pub fn invalidate_relative_selectors_for_dom_mutation(
+ self,
+ subtree: bool,
+ stylist: &'a Stylist,
+ inherited_search_path: ElementSelectorFlags,
+ operation: DomMutationOperation,
+ ) {
+ let mut collector = RelativeSelectorDependencyCollector::new(
+ self.element,
+ if operation.is_side_effect() {
+ None
+ } else {
+ Some(OptimizationContext {
+ sibling_traversal_map: &self.sibling_traversal_map,
+ quirks_mode: self.quirks_mode,
+ operation,
+ })
+ },
+ );
+ let mut traverse_subtree = false;
+ self.element.apply_selector_flags(inherited_search_path);
+ stylist.for_each_cascade_data_with_scope(self.element, |data, scope| {
+ let map = data.relative_selector_invalidation_map();
+ if !map.used {
+ return;
+ }
+ traverse_subtree |= map.needs_ancestors_traversal;
+ collector.collect_all_dependencies_for_element(
+ self.element,
+ scope.map(|e| e.opaque()),
+ self.quirks_mode,
+ map,
+ operation,
+ );
+ });
+
+ if subtree && traverse_subtree {
+ for node in self.element.as_node().dom_descendants() {
+ let descendant = match node.as_element() {
+ Some(e) => e,
+ None => continue,
+ };
+ descendant.apply_selector_flags(inherited_search_path);
+ stylist.for_each_cascade_data_with_scope(descendant, |data, scope| {
+ let map = data.relative_selector_invalidation_map();
+ if !map.used {
+ return;
+ }
+ collector.collect_all_dependencies_for_element(
+ descendant,
+ scope.map(|e| e.opaque()),
+ self.quirks_mode,
+ map,
+ operation,
+ );
+ });
+ }
+ }
+ if collector.is_empty() {
+ return;
+ }
+ self.invalidate_from_dependencies(collector.get());
+ }
+
+ /// Carry out complete invalidation triggered by a relative selector invalidation.
+ fn invalidate_from_dependencies(&self, to_invalidate: ToInvalidate<'a, E>) {
+ for (element, dependencies) in to_invalidate.dependencies {
+ let mut selector_caches = SelectorCaches::default();
+ let mut processor = RelativeSelectorInnerInvalidationProcessor::new(
+ self.quirks_mode,
+ self.snapshot_table,
+ &dependencies,
+ &mut selector_caches,
+ &self.sibling_traversal_map,
+ );
+ TreeStyleInvalidator::new(element, None, &mut processor).invalidate();
+ for (element, invalidation) in processor.take_invalidations() {
+ self.invalidate_upwards(element, &invalidation);
+ }
+ }
+ for invalidation in to_invalidate.invalidations {
+ self.invalidate_upwards(self.element, &invalidation);
+ }
+ }
+
+ fn invalidate_upwards(&self, element: E, invalidation: &RelativeSelectorInvalidation<'a>) {
+ // This contains the main reason for why relative selector invalidation is handled
+ // separately - It travels ancestor and/or earlier sibling direction.
+ match invalidation.kind {
+ RelativeDependencyInvalidationKind::Parent => {
+ element.parent_element().map(|e| {
+ if !Self::in_search_direction(
+ &e,
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR,
+ ) {
+ return;
+ }
+ self.handle_anchor(e, invalidation.dependency, invalidation.host);
+ });
+ },
+ RelativeDependencyInvalidationKind::Ancestors => {
+ let mut parent = element.parent_element();
+ while let Some(par) = parent {
+ if !Self::in_search_direction(
+ &par,
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR,
+ ) {
+ return;
+ }
+ self.handle_anchor(par, invalidation.dependency, invalidation.host);
+ parent = par.parent_element();
+ }
+ },
+ RelativeDependencyInvalidationKind::PrevSibling => {
+ self.sibling_traversal_map
+ .prev_sibling_for(&element)
+ .map(|e| {
+ if !Self::in_search_direction(
+ &e,
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING,
+ ) {
+ return;
+ }
+ self.handle_anchor(e, invalidation.dependency, invalidation.host);
+ });
+ },
+ RelativeDependencyInvalidationKind::AncestorPrevSibling => {
+ let mut parent = element.parent_element();
+ while let Some(par) = parent {
+ if !Self::in_search_direction(
+ &par,
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR,
+ ) {
+ return;
+ }
+ par.prev_sibling_element().map(|e| {
+ if !Self::in_search_direction(
+ &e,
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING,
+ ) {
+ return;
+ }
+ self.handle_anchor(e, invalidation.dependency, invalidation.host);
+ });
+ parent = par.parent_element();
+ }
+ },
+ RelativeDependencyInvalidationKind::EarlierSibling => {
+ let mut sibling = self.sibling_traversal_map.prev_sibling_for(&element);
+ while let Some(sib) = sibling {
+ if !Self::in_search_direction(
+ &sib,
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING,
+ ) {
+ return;
+ }
+ self.handle_anchor(sib, invalidation.dependency, invalidation.host);
+ sibling = sib.prev_sibling_element();
+ }
+ },
+ RelativeDependencyInvalidationKind::AncestorEarlierSibling => {
+ let mut parent = element.parent_element();
+ while let Some(par) = parent {
+ if !Self::in_search_direction(
+ &par,
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR,
+ ) {
+ return;
+ }
+ let mut sibling = par.prev_sibling_element();
+ while let Some(sib) = sibling {
+ if !Self::in_search_direction(
+ &sib,
+ ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING,
+ ) {
+ return;
+ }
+ self.handle_anchor(sib, invalidation.dependency, invalidation.host);
+ sibling = sib.prev_sibling_element();
+ }
+ parent = par.parent_element();
+ }
+ },
+ }
+ }
+
+ /// Is this element in the direction of the given relative selector search path?
+ fn in_search_direction(element: &E, desired: ElementSelectorFlags) -> bool {
+ if let Some(direction) = element.relative_selector_search_direction() {
+ direction.intersects(desired)
+ } else {
+ false
+ }
+ }
+
+ /// Handle a potential relative selector anchor.
+ fn handle_anchor(
+ &self,
+ element: E,
+ outer_dependency: &Dependency,
+ host: Option<OpaqueElement>,
+ ) {
+ let is_rightmost = Self::is_subject(outer_dependency);
+ if (is_rightmost &&
+ !element.has_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR)) ||
+ (!is_rightmost &&
+ !element.has_selector_flags(
+ ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT,
+ ))
+ {
+ // If it was never a relative selector anchor, don't bother.
+ return;
+ }
+ let mut selector_caches = SelectorCaches::default();
+ let matching_context = MatchingContext::<'_, E::Impl>::new_for_visited(
+ MatchingMode::Normal,
+ None,
+ &mut selector_caches,
+ VisitedHandlingMode::AllLinksVisitedAndUnvisited,
+ self.quirks_mode,
+ NeedsSelectorFlags::No,
+ MatchingForInvalidation::Yes,
+ );
+ let mut data = match element.mutate_data() {
+ Some(data) => data,
+ None => return,
+ };
+ let mut processor = RelativeSelectorOuterInvalidationProcessor {
+ element,
+ host,
+ data: data.deref_mut(),
+ dependency: &*outer_dependency,
+ matching_context,
+ traversal_map: &self.sibling_traversal_map,
+ };
+ let result = TreeStyleInvalidator::new(element, None, &mut processor).invalidate();
+ (self.invalidated)(element, &result);
+ }
+
+ /// Does this relative selector dependency have its relative selector in the subject position?
+ fn is_subject(outer_dependency: &Dependency) -> bool {
+ debug_assert!(
+ matches!(
+ outer_dependency.invalidation_kind(),
+ DependencyInvalidationKind::Normal(_)
+ ),
+ "Outer selector of relative selector is relative?"
+ );
+
+ if let Some(p) = outer_dependency.parent.as_ref() {
+ if !Self::is_subject(p.as_ref()) {
+ // Not subject in outer selector.
+ return false;
+ }
+ }
+ outer_dependency.selector.is_rightmost(outer_dependency.selector_offset)
+ }
+}
+
+/// Blindly invalidate everything outside of a relative selector.
+/// Consider `:is(.a :has(.b) .c ~ .d) ~ .e .f`, where .b gets deleted.
+/// Since the tree mutated, we cannot rely on snapshots.
+pub struct RelativeSelectorOuterInvalidationProcessor<'a, 'b, E: TElement> {
+ /// Element being invalidated.
+ pub element: E,
+ /// The current shadow host, if any.
+ pub host: Option<OpaqueElement>,
+ /// Data for the element being invalidated.
+ pub data: &'a mut ElementData,
+ /// Dependency to be processed.
+ pub dependency: &'b Dependency,
+ /// Matching context to use for invalidation.
+ pub matching_context: MatchingContext<'a, E::Impl>,
+ /// Traversal map for this invalidation.
+ pub traversal_map: &'a SiblingTraversalMap<E>,
+}
+
+impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'b, 'a, E>
+ for RelativeSelectorOuterInvalidationProcessor<'a, 'b, E>
+where
+ E: TElement,
+{
+ fn invalidates_on_pseudo_element(&self) -> bool {
+ true
+ }
+
+ fn check_outer_dependency(&mut self, _dependency: &Dependency, _element: E) -> bool {
+ // At this point, we know a relative selector invalidated, and are ignoring them.
+ true
+ }
+
+ fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> {
+ &mut self.matching_context
+ }
+
+ fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> {
+ self.traversal_map
+ }
+
+ fn collect_invalidations(
+ &mut self,
+ element: E,
+ _self_invalidations: &mut InvalidationVector<'b>,
+ descendant_invalidations: &mut DescendantInvalidationLists<'b>,
+ sibling_invalidations: &mut InvalidationVector<'b>,
+ ) -> bool {
+ debug_assert_eq!(element, self.element);
+ debug_assert!(
+ self.matching_context.matching_for_invalidation(),
+ "Not matching for invalidation?"
+ );
+
+ // Ok, this element can potentially an anchor to the given dependency.
+ // Before we do the potentially-costly ancestor/earlier sibling traversal,
+ // See if it can actuall be an anchor by trying to match the "rest" of the selector
+ // outside and to the left of `:has` in question.
+ // e.g. Element under consideration can only be the anchor to `:has` in
+ // `.foo .bar ~ .baz:has()`, iff it matches `.foo .bar ~ .baz`.
+ let invalidated_self = {
+ let mut d = self.dependency;
+ loop {
+ debug_assert!(
+ matches!(
+ d.invalidation_kind(),
+ DependencyInvalidationKind::Normal(_)
+ ),
+ "Unexpected outer relative dependency"
+ );
+ if !dependency_may_be_relevant(d, &element, false) {
+ break false;
+ }
+ if !matches_selector(
+ &d.selector,
+ d.selector_offset,
+ None,
+ &element,
+ self.matching_context(),
+ ) {
+ break false;
+ }
+ let invalidation_kind = d.normal_invalidation_kind();
+ if matches!(invalidation_kind, NormalDependencyInvalidationKind::Element) {
+ if let Some(ref parent) = d.parent {
+ d = parent;
+ continue;
+ }
+ break true;
+ }
+ debug_assert_ne!(d.selector_offset, 0);
+ debug_assert_ne!(d.selector_offset, d.selector.len());
+ let invalidation = Invalidation::new(&d, self.host);
+ break push_invalidation(
+ invalidation,
+ invalidation_kind,
+ descendant_invalidations,
+ sibling_invalidations
+ );
+ }
+ };
+
+ if invalidated_self {
+ self.data.hint.insert(RestyleHint::RESTYLE_SELF);
+ }
+ invalidated_self
+ }
+
+ fn should_process_descendants(&mut self, element: E) -> bool {
+ if element == self.element {
+ return should_process_descendants(&self.data);
+ }
+
+ match element.borrow_data() {
+ Some(d) => should_process_descendants(&d),
+ None => return false,
+ }
+ }
+
+ fn recursion_limit_exceeded(&mut self, _element: E) {
+ unreachable!("Unexpected recursion limit");
+ }
+
+ fn invalidated_descendants(&mut self, element: E, child: E) {
+ invalidated_descendants(element, child)
+ }
+
+ fn invalidated_self(&mut self, element: E) {
+ debug_assert_ne!(element, self.element);
+ invalidated_self(element);
+ }
+
+ fn invalidated_sibling(&mut self, element: E, of: E) {
+ debug_assert_ne!(element, self.element);
+ invalidated_sibling(element, of);
+ }
+}
+
+/// Invalidation for the selector(s) inside a relative selector.
+pub struct RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E>
+where
+ E: TElement + 'a,
+{
+ /// Matching context to be used.
+ matching_context: MatchingContext<'b, E::Impl>,
+ /// Table of snapshots.
+ snapshot_table: Option<&'c ServoElementSnapshotTable>,
+ /// Incoming dependencies to be processed.
+ dependencies: &'c ElementDependencies<'a>,
+ /// Generated invalidations.
+ invalidations: InnerInvalidations<'a, E>,
+ /// Traversal map for this invalidation.
+ traversal_map: &'b SiblingTraversalMap<E>,
+}
+
+impl<'a, 'b, 'c, E> RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E>
+where
+ E: TElement + 'a,
+{
+ fn new(
+ quirks_mode: QuirksMode,
+ snapshot_table: Option<&'c ServoElementSnapshotTable>,
+ dependencies: &'c ElementDependencies<'a>,
+ selector_caches: &'b mut SelectorCaches,
+ traversal_map: &'b SiblingTraversalMap<E>,
+ ) -> Self {
+ let matching_context = MatchingContext::new_for_visited(
+ MatchingMode::Normal,
+ None,
+ selector_caches,
+ VisitedHandlingMode::AllLinksVisitedAndUnvisited,
+ quirks_mode,
+ NeedsSelectorFlags::No,
+ MatchingForInvalidation::Yes,
+ );
+ Self {
+ matching_context,
+ snapshot_table,
+ dependencies,
+ invalidations: InnerInvalidations::default(),
+ traversal_map,
+ }
+ }
+
+ fn note_dependency(
+ &mut self,
+ element: E,
+ scope: Option<OpaqueElement>,
+ dependency: &'a Dependency,
+ descendant_invalidations: &mut DescendantInvalidationLists<'a>,
+ sibling_invalidations: &mut InvalidationVector<'a>,
+ ) {
+ match dependency.invalidation_kind() {
+ DependencyInvalidationKind::Normal(_) => (),
+ DependencyInvalidationKind::Relative(kind) => {
+ self.found_relative_selector_invalidation(element, kind, dependency);
+ return;
+ },
+ }
+ if matches!(
+ dependency.normal_invalidation_kind(),
+ NormalDependencyInvalidationKind::Element
+ ) {
+ // Ok, keep heading outwards.
+ debug_assert!(
+ dependency.parent.is_some(),
+ "Orphaned inner selector dependency?"
+ );
+ if let Some(parent) = dependency.parent.as_ref() {
+ self.note_dependency(
+ element,
+ scope,
+ parent,
+ descendant_invalidations,
+ sibling_invalidations,
+ );
+ }
+ return;
+ }
+ let invalidation = Invalidation::new(&dependency, scope);
+ match dependency.normal_invalidation_kind() {
+ NormalDependencyInvalidationKind::Descendants => {
+ // Descendant invalidations are simplified due to pseudo-elements not being available within the relative selector.
+ descendant_invalidations.dom_descendants.push(invalidation)
+ },
+ NormalDependencyInvalidationKind::Siblings => sibling_invalidations.push(invalidation),
+ _ => unreachable!(),
+ }
+ }
+
+ /// Take the generated invalidations.
+ fn take_invalidations(self) -> InnerInvalidations<'a, E> {
+ self.invalidations
+ }
+}
+
+impl<'a, 'b, 'c, E> InvalidationProcessor<'a, 'b, E>
+ for RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E>
+where
+ E: TElement + 'a,
+{
+ fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool {
+ if let Some(snapshot_table) = self.snapshot_table {
+ let wrapper = ElementWrapper::new(element, snapshot_table);
+ return check_dependency(dependency, &element, &wrapper, &mut self.matching_context);
+ }
+ // Just invalidate if we don't have a snapshot.
+ true
+ }
+
+ fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl> {
+ return &mut self.matching_context;
+ }
+
+ fn collect_invalidations(
+ &mut self,
+ element: E,
+ _self_invalidations: &mut InvalidationVector<'a>,
+ descendant_invalidations: &mut DescendantInvalidationLists<'a>,
+ sibling_invalidations: &mut InvalidationVector<'a>,
+ ) -> bool {
+ for (scope, dependency) in self.dependencies {
+ self.note_dependency(
+ element,
+ *scope,
+ dependency,
+ descendant_invalidations,
+ sibling_invalidations,
+ )
+ }
+ false
+ }
+
+ fn should_process_descendants(&mut self, _element: E) -> bool {
+ true
+ }
+
+ fn recursion_limit_exceeded(&mut self, _element: E) {
+ unreachable!("Unexpected recursion limit");
+ }
+
+ // Don't do anything for normal invalidations.
+ fn invalidated_self(&mut self, _element: E) {}
+ fn invalidated_sibling(&mut self, _sibling: E, _of: E) {}
+ fn invalidated_descendants(&mut self, _element: E, _child: E) {}
+
+ fn found_relative_selector_invalidation(
+ &mut self,
+ element: E,
+ kind: RelativeDependencyInvalidationKind,
+ dep: &'a Dependency,
+ ) {
+ debug_assert!(dep.parent.is_some(), "Orphaned inners selector?");
+ if element.relative_selector_search_direction().is_none() {
+ return;
+ }
+ self.invalidations.push((
+ element,
+ RelativeSelectorInvalidation {
+ host: self.matching_context.current_host,
+ kind,
+ dependency: dep.parent.as_ref().unwrap(),
+ },
+ ));
+ }
+
+ fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> {
+ &self.traversal_map
+ }
+}
diff --git a/servo/components/style/invalidation/element/restyle_hints.rs b/servo/components/style/invalidation/element/restyle_hints.rs
new file mode 100644
index 0000000000..fe89636e88
--- /dev/null
+++ b/servo/components/style/invalidation/element/restyle_hints.rs
@@ -0,0 +1,191 @@
+/* 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/. */
+
+//! Restyle hints: an optimization to avoid unnecessarily matching selectors.
+
+use crate::traversal_flags::TraversalFlags;
+
+bitflags! {
+ /// The kind of restyle we need to do for a given element.
+ #[repr(C)]
+ #[derive(Clone, Copy, Debug)]
+ pub struct RestyleHint: u16 {
+ /// Do a selector match of the element.
+ const RESTYLE_SELF = 1 << 0;
+
+ /// Do a selector match of the element's pseudo-elements. Always to be combined with
+ /// RESTYLE_SELF.
+ const RESTYLE_PSEUDOS = 1 << 1;
+
+ /// Do a selector match if the element is a pseudo-element.
+ const RESTYLE_SELF_IF_PSEUDO = 1 << 2;
+
+ /// Do a selector match of the element's descendants.
+ const RESTYLE_DESCENDANTS = 1 << 3;
+
+ /// Recascade the current element.
+ const RECASCADE_SELF = 1 << 4;
+
+ /// Recascade the current element if it inherits any reset style.
+ const RECASCADE_SELF_IF_INHERIT_RESET_STYLE = 1 << 5;
+
+ /// Recascade all descendant elements.
+ const RECASCADE_DESCENDANTS = 1 << 6;
+
+ /// Replace the style data coming from CSS transitions without updating
+ /// any other style data. This hint is only processed in animation-only
+ /// traversal which is prior to normal traversal.
+ const RESTYLE_CSS_TRANSITIONS = 1 << 7;
+
+ /// Replace the style data coming from CSS animations without updating
+ /// any other style data. This hint is only processed in animation-only
+ /// traversal which is prior to normal traversal.
+ const RESTYLE_CSS_ANIMATIONS = 1 << 8;
+
+ /// Don't re-run selector-matching on the element, only the style
+ /// attribute has changed, and this change didn't have any other
+ /// dependencies.
+ const RESTYLE_STYLE_ATTRIBUTE = 1 << 9;
+
+ /// Replace the style data coming from SMIL animations without updating
+ /// any other style data. This hint is only processed in animation-only
+ /// traversal which is prior to normal traversal.
+ const RESTYLE_SMIL = 1 << 10;
+ }
+}
+
+impl RestyleHint {
+ /// Creates a new `RestyleHint` indicating that the current element and all
+ /// its descendants must be fully restyled.
+ pub fn restyle_subtree() -> Self {
+ RestyleHint::RESTYLE_SELF | RestyleHint::RESTYLE_DESCENDANTS
+ }
+
+ /// Creates a new `RestyleHint` indicating that the current element and all
+ /// its descendants must be recascaded.
+ pub fn recascade_subtree() -> Self {
+ RestyleHint::RECASCADE_SELF | RestyleHint::RECASCADE_DESCENDANTS
+ }
+
+ /// Returns whether this hint invalidates the element and all its
+ /// descendants.
+ pub fn contains_subtree(&self) -> bool {
+ self.contains(Self::restyle_subtree())
+ }
+
+ /// Returns whether we'll recascade all of the descendants.
+ pub fn will_recascade_subtree(&self) -> bool {
+ self.contains_subtree() || self.contains(Self::recascade_subtree())
+ }
+
+ /// Returns whether we need to restyle this element.
+ pub fn has_non_animation_invalidations(&self) -> bool {
+ !(*self & !Self::for_animations()).is_empty()
+ }
+
+ /// Propagates this restyle hint to a child element.
+ pub fn propagate(&mut self, traversal_flags: &TraversalFlags) -> Self {
+ use std::mem;
+
+ // In the middle of an animation only restyle, we don't need to
+ // propagate any restyle hints, and we need to remove ourselves.
+ if traversal_flags.for_animation_only() {
+ self.remove_animation_hints();
+ return Self::empty();
+ }
+
+ debug_assert!(
+ !self.has_animation_hint(),
+ "There should not be any animation restyle hints \
+ during normal traversal"
+ );
+
+ // Else we should clear ourselves, and return the propagated hint.
+ mem::replace(self, Self::empty()).propagate_for_non_animation_restyle()
+ }
+
+ /// Returns a new `RestyleHint` appropriate for children of the current element.
+ fn propagate_for_non_animation_restyle(&self) -> Self {
+ if self.contains(RestyleHint::RESTYLE_DESCENDANTS) {
+ return Self::restyle_subtree();
+ }
+ let mut result = Self::empty();
+ if self.contains(RestyleHint::RESTYLE_PSEUDOS) {
+ result |= Self::RESTYLE_SELF_IF_PSEUDO;
+ }
+ if self.contains(RestyleHint::RECASCADE_DESCENDANTS) {
+ result |= Self::recascade_subtree();
+ }
+ result
+ }
+
+ /// Returns a hint that contains all the replacement hints.
+ pub fn replacements() -> Self {
+ RestyleHint::RESTYLE_STYLE_ATTRIBUTE | Self::for_animations()
+ }
+
+ /// The replacements for the animation cascade levels.
+ #[inline]
+ pub fn for_animations() -> Self {
+ RestyleHint::RESTYLE_SMIL |
+ RestyleHint::RESTYLE_CSS_ANIMATIONS |
+ RestyleHint::RESTYLE_CSS_TRANSITIONS
+ }
+
+ /// Returns whether the hint specifies that an animation cascade level must
+ /// be replaced.
+ #[inline]
+ pub fn has_animation_hint(&self) -> bool {
+ self.intersects(Self::for_animations())
+ }
+
+ /// Returns whether the hint specifies that an animation cascade level must
+ /// be replaced.
+ #[inline]
+ pub fn has_animation_hint_or_recascade(&self) -> bool {
+ self.intersects(
+ Self::for_animations() |
+ Self::RECASCADE_SELF |
+ Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE,
+ )
+ }
+
+ /// Returns whether the hint specifies some restyle work other than an
+ /// animation cascade level replacement.
+ #[inline]
+ pub fn has_non_animation_hint(&self) -> bool {
+ !(*self & !Self::for_animations()).is_empty()
+ }
+
+ /// Returns whether the hint specifies that some cascade levels must be
+ /// replaced.
+ #[inline]
+ pub fn has_replacements(&self) -> bool {
+ self.intersects(Self::replacements())
+ }
+
+ /// Removes all of the animation-related hints.
+ #[inline]
+ pub fn remove_animation_hints(&mut self) {
+ self.remove(Self::for_animations());
+
+ // While RECASCADE_SELF is not animation-specific, we only ever add and process it during
+ // traversal. If we are here, removing animation hints, then we are in an animation-only
+ // traversal, and we know that any RECASCADE_SELF flag must have been set due to changes in
+ // inherited values after restyling for animations, and thus we want to remove it so that
+ // we don't later try to restyle the element during a normal restyle.
+ // (We could have separate RECASCADE_SELF_NORMAL and RECASCADE_SELF_ANIMATIONS flags to
+ // make it clear, but this isn't currently necessary.)
+ self.remove(Self::RECASCADE_SELF | Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE);
+ }
+}
+
+impl Default for RestyleHint {
+ fn default() -> Self {
+ Self::empty()
+ }
+}
+
+#[cfg(feature = "servo")]
+malloc_size_of_is_0!(RestyleHint);
diff --git a/servo/components/style/invalidation/element/state_and_attributes.rs b/servo/components/style/invalidation/element/state_and_attributes.rs
new file mode 100644
index 0000000000..1c58cddf1e
--- /dev/null
+++ b/servo/components/style/invalidation/element/state_and_attributes.rs
@@ -0,0 +1,601 @@
+/* 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/. */
+
+//! An invalidation processor for style changes due to state and attribute
+//! changes.
+
+use crate::context::SharedStyleContext;
+use crate::data::ElementData;
+use crate::dom::{TElement, TNode};
+use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
+use crate::invalidation::element::invalidation_map::*;
+use crate::invalidation::element::invalidator::{
+ DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap,
+};
+use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor};
+use crate::invalidation::element::restyle_hints::RestyleHint;
+use crate::selector_map::SelectorMap;
+use crate::selector_parser::Snapshot;
+use crate::stylesheets::origin::OriginSet;
+use crate::{Atom, WeakAtom};
+use dom::ElementState;
+use selectors::attr::CaseSensitivity;
+use selectors::matching::{
+ matches_selector, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags,
+ SelectorCaches, VisitedHandlingMode,
+};
+use smallvec::SmallVec;
+
+/// The collector implementation.
+struct Collector<'a, 'b: 'a, 'selectors: 'a, E>
+where
+ E: TElement,
+{
+ element: E,
+ wrapper: ElementWrapper<'b, E>,
+ snapshot: &'a Snapshot,
+ matching_context: &'a mut MatchingContext<'b, E::Impl>,
+ lookup_element: E,
+ removed_id: Option<&'a WeakAtom>,
+ added_id: Option<&'a WeakAtom>,
+ classes_removed: &'a SmallVec<[Atom; 8]>,
+ classes_added: &'a SmallVec<[Atom; 8]>,
+ state_changes: ElementState,
+ descendant_invalidations: &'a mut DescendantInvalidationLists<'selectors>,
+ sibling_invalidations: &'a mut InvalidationVector<'selectors>,
+ invalidates_self: bool,
+}
+
+/// An invalidation processor for style changes due to state and attribute
+/// changes.
+pub struct StateAndAttrInvalidationProcessor<'a, 'b: 'a, E: TElement> {
+ shared_context: &'a SharedStyleContext<'b>,
+ element: E,
+ data: &'a mut ElementData,
+ matching_context: MatchingContext<'a, E::Impl>,
+ traversal_map: SiblingTraversalMap<E>,
+}
+
+impl<'a, 'b: 'a, E: TElement + 'b> StateAndAttrInvalidationProcessor<'a, 'b, E> {
+ /// Creates a new StateAndAttrInvalidationProcessor.
+ pub fn new(
+ shared_context: &'a SharedStyleContext<'b>,
+ element: E,
+ data: &'a mut ElementData,
+ selector_caches: &'a mut SelectorCaches,
+ ) -> Self {
+ let matching_context = MatchingContext::new_for_visited(
+ MatchingMode::Normal,
+ None,
+ selector_caches,
+ VisitedHandlingMode::AllLinksVisitedAndUnvisited,
+ shared_context.quirks_mode(),
+ NeedsSelectorFlags::No,
+ MatchingForInvalidation::Yes,
+ );
+
+ Self {
+ shared_context,
+ element,
+ data,
+ matching_context,
+ traversal_map: SiblingTraversalMap::default(),
+ }
+ }
+}
+
+/// Checks a dependency against a given element and wrapper, to see if something
+/// changed.
+pub fn check_dependency<E, W>(
+ dependency: &Dependency,
+ element: &E,
+ wrapper: &W,
+ context: &mut MatchingContext<'_, E::Impl>,
+) -> bool
+where
+ E: TElement,
+ W: selectors::Element<Impl = E::Impl>,
+{
+ let matches_now = matches_selector(
+ &dependency.selector,
+ dependency.selector_offset,
+ None,
+ element,
+ context,
+ );
+
+ let matched_then = matches_selector(
+ &dependency.selector,
+ dependency.selector_offset,
+ None,
+ wrapper,
+ context,
+ );
+
+ matched_then != matches_now
+}
+
+/// Whether we should process the descendants of a given element for style
+/// invalidation.
+pub fn should_process_descendants(data: &ElementData) -> bool {
+ !data.styles.is_display_none() && !data.hint.contains(RestyleHint::RESTYLE_DESCENDANTS)
+}
+
+/// Propagates the bits after invalidating a descendant child.
+pub fn propagate_dirty_bit_up_to<E>(ancestor: E, child: E)
+where
+ E: TElement,
+{
+ // The child may not be a flattened tree child of the current element,
+ // but may be arbitrarily deep.
+ //
+ // Since we keep the traversal flags in terms of the flattened tree,
+ // we need to propagate it as appropriate.
+ let mut current = child.traversal_parent();
+ while let Some(parent) = current.take() {
+ unsafe { parent.set_dirty_descendants() };
+ current = parent.traversal_parent();
+
+ if parent == ancestor {
+ return;
+ }
+ }
+ debug_assert!(
+ false,
+ "Should've found {:?} as an ancestor of {:?}",
+ ancestor, child
+ );
+}
+
+/// Propagates the bits after invalidating a descendant child, if needed.
+pub fn invalidated_descendants<E>(element: E, child: E)
+where
+ E: TElement,
+{
+ if !child.has_data() {
+ return;
+ }
+ propagate_dirty_bit_up_to(element, child)
+}
+
+/// Sets the appropriate restyle hint after invalidating the style of a given
+/// element.
+pub fn invalidated_self<E>(element: E) -> bool
+where
+ E: TElement,
+{
+ let mut data = match element.mutate_data() {
+ Some(data) => data,
+ None => return false,
+ };
+ data.hint.insert(RestyleHint::RESTYLE_SELF);
+ true
+}
+
+/// Sets the appropriate hint after invalidating the style of a sibling.
+pub fn invalidated_sibling<E>(element: E, of: E)
+where
+ E: TElement,
+{
+ debug_assert_eq!(
+ element.as_node().parent_node(),
+ of.as_node().parent_node(),
+ "Should be siblings"
+ );
+ if !invalidated_self(element) {
+ return;
+ }
+ if element.traversal_parent() != of.traversal_parent() {
+ let parent = element.as_node().parent_element_or_host();
+ debug_assert!(
+ parent.is_some(),
+ "How can we have siblings without parent nodes?"
+ );
+ if let Some(e) = parent {
+ propagate_dirty_bit_up_to(e, element)
+ }
+ }
+}
+
+impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'a, 'a, E>
+ for StateAndAttrInvalidationProcessor<'a, 'b, E>
+where
+ E: TElement,
+{
+ /// We need to invalidate style on pseudo-elements, in order to process
+ /// changes that could otherwise end up in ::before or ::after content being
+ /// generated, and invalidate lazy pseudo caches.
+ fn invalidates_on_pseudo_element(&self) -> bool {
+ true
+ }
+
+ fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool {
+ // We cannot assert about `element` having a snapshot here (in fact it
+ // most likely won't), because it may be an arbitrary descendant or
+ // later-sibling of the element we started invalidating with.
+ let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map);
+ check_dependency(dependency, &element, &wrapper, &mut self.matching_context)
+ }
+
+ fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> {
+ &mut self.matching_context
+ }
+
+ fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> {
+ &self.traversal_map
+ }
+
+ fn collect_invalidations(
+ &mut self,
+ element: E,
+ _self_invalidations: &mut InvalidationVector<'a>,
+ descendant_invalidations: &mut DescendantInvalidationLists<'a>,
+ sibling_invalidations: &mut InvalidationVector<'a>,
+ ) -> bool {
+ debug_assert_eq!(element, self.element);
+ debug_assert!(element.has_snapshot(), "Why bothering?");
+
+ let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map);
+
+ let state_changes = wrapper.state_changes();
+ let Some(snapshot) = wrapper.snapshot() else {
+ return false;
+ };
+
+ if !snapshot.has_attrs() && state_changes.is_empty() {
+ return false;
+ }
+
+ let mut classes_removed = SmallVec::<[Atom; 8]>::new();
+ let mut classes_added = SmallVec::<[Atom; 8]>::new();
+ if snapshot.class_changed() {
+ // TODO(emilio): Do this more efficiently!
+ snapshot.each_class(|c| {
+ if !element.has_class(c, CaseSensitivity::CaseSensitive) {
+ classes_removed.push(c.0.clone())
+ }
+ });
+
+ element.each_class(|c| {
+ if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) {
+ classes_added.push(c.0.clone())
+ }
+ })
+ }
+
+ let mut id_removed = None;
+ let mut id_added = None;
+ if snapshot.id_changed() {
+ let old_id = snapshot.id_attr();
+ let current_id = element.id();
+
+ if old_id != current_id {
+ id_removed = old_id;
+ id_added = current_id;
+ }
+ }
+
+ if log_enabled!(::log::Level::Debug) {
+ debug!("Collecting changes for: {:?}", element);
+ if !state_changes.is_empty() {
+ debug!(" > state: {:?}", state_changes);
+ }
+ if snapshot.id_changed() {
+ debug!(" > id changed: +{:?} -{:?}", id_added, id_removed);
+ }
+ if snapshot.class_changed() {
+ debug!(
+ " > class changed: +{:?} -{:?}",
+ classes_added, classes_removed
+ );
+ }
+ let mut attributes_changed = false;
+ snapshot.each_attr_changed(|_| {
+ attributes_changed = true;
+ });
+ if attributes_changed {
+ debug!(
+ " > attributes changed, old: {}",
+ snapshot.debug_list_attributes()
+ )
+ }
+ }
+
+ let lookup_element = if element.implemented_pseudo_element().is_some() {
+ element.pseudo_element_originating_element().unwrap()
+ } else {
+ element
+ };
+
+ let mut shadow_rule_datas = SmallVec::<[_; 3]>::new();
+ let matches_document_author_rules =
+ element.each_applicable_non_document_style_rule_data(|data, host| {
+ shadow_rule_datas.push((data, host.opaque()))
+ });
+
+ let invalidated_self = {
+ let mut collector = Collector {
+ wrapper,
+ lookup_element,
+ state_changes,
+ element,
+ snapshot: &snapshot,
+ matching_context: &mut self.matching_context,
+ removed_id: id_removed,
+ added_id: id_added,
+ classes_removed: &classes_removed,
+ classes_added: &classes_added,
+ descendant_invalidations,
+ sibling_invalidations,
+ invalidates_self: false,
+ };
+
+ let document_origins = if !matches_document_author_rules {
+ OriginSet::ORIGIN_USER_AGENT | OriginSet::ORIGIN_USER
+ } else {
+ OriginSet::all()
+ };
+
+ for (cascade_data, origin) in self.shared_context.stylist.iter_origins() {
+ if document_origins.contains(origin.into()) {
+ collector
+ .collect_dependencies_in_invalidation_map(cascade_data.invalidation_map());
+ }
+ }
+
+ for &(ref data, ref host) in &shadow_rule_datas {
+ collector.matching_context.current_host = Some(host.clone());
+ collector.collect_dependencies_in_invalidation_map(data.invalidation_map());
+ }
+
+ collector.invalidates_self
+ };
+
+ // If we generated a ton of descendant invalidations, it's probably not
+ // worth to go ahead and try to process them.
+ //
+ // Just restyle the descendants directly.
+ //
+ // This number is completely made-up, but the page that made us add this
+ // code generated 1960+ invalidations (bug 1420741).
+ //
+ // We don't look at slotted_descendants because those don't propagate
+ // down more than one level anyway.
+ if descendant_invalidations.dom_descendants.len() > 150 {
+ self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS);
+ }
+
+ if invalidated_self {
+ self.data.hint.insert(RestyleHint::RESTYLE_SELF);
+ }
+
+ invalidated_self
+ }
+
+ fn should_process_descendants(&mut self, element: E) -> bool {
+ if element == self.element {
+ return should_process_descendants(&self.data);
+ }
+
+ match element.borrow_data() {
+ Some(d) => should_process_descendants(&d),
+ None => return false,
+ }
+ }
+
+ fn recursion_limit_exceeded(&mut self, element: E) {
+ if element == self.element {
+ self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS);
+ return;
+ }
+
+ if let Some(mut data) = element.mutate_data() {
+ data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS);
+ }
+ }
+
+ fn invalidated_descendants(&mut self, element: E, child: E) {
+ invalidated_descendants(element, child)
+ }
+
+ fn invalidated_self(&mut self, element: E) {
+ debug_assert_ne!(element, self.element);
+ invalidated_self(element);
+ }
+
+ fn invalidated_sibling(&mut self, element: E, of: E) {
+ debug_assert_ne!(element, self.element);
+ invalidated_sibling(element, of);
+ }
+}
+
+impl<'a, 'b, 'selectors, E> Collector<'a, 'b, 'selectors, E>
+where
+ E: TElement,
+ 'selectors: 'a,
+{
+ fn collect_dependencies_in_invalidation_map(&mut self, map: &'selectors InvalidationMap) {
+ let quirks_mode = self.matching_context.quirks_mode();
+ let removed_id = self.removed_id;
+ if let Some(ref id) = removed_id {
+ if let Some(deps) = map.id_to_selector.get(id, quirks_mode) {
+ for dep in deps {
+ self.scan_dependency(dep);
+ }
+ }
+ }
+
+ let added_id = self.added_id;
+ if let Some(ref id) = added_id {
+ if let Some(deps) = map.id_to_selector.get(id, quirks_mode) {
+ for dep in deps {
+ self.scan_dependency(dep);
+ }
+ }
+ }
+
+ for class in self.classes_added.iter().chain(self.classes_removed.iter()) {
+ if let Some(deps) = map.class_to_selector.get(class, quirks_mode) {
+ for dep in deps {
+ self.scan_dependency(dep);
+ }
+ }
+ }
+
+ self.snapshot.each_attr_changed(|attribute| {
+ if let Some(deps) = map.other_attribute_affecting_selectors.get(attribute) {
+ for dep in deps {
+ self.scan_dependency(dep);
+ }
+ }
+ });
+
+ self.collect_state_dependencies(&map.state_affecting_selectors)
+ }
+
+ fn collect_state_dependencies(&mut self, map: &'selectors SelectorMap<StateDependency>) {
+ if self.state_changes.is_empty() {
+ return;
+ }
+ map.lookup_with_additional(
+ self.lookup_element,
+ self.matching_context.quirks_mode(),
+ self.removed_id,
+ self.classes_removed,
+ self.state_changes,
+ |dependency| {
+ if !dependency.state.intersects(self.state_changes) {
+ return true;
+ }
+ self.scan_dependency(&dependency.dep);
+ true
+ },
+ );
+ }
+
+ /// Check whether a dependency should be taken into account.
+ #[inline]
+ fn check_dependency(&mut self, dependency: &Dependency) -> bool {
+ check_dependency(
+ dependency,
+ &self.element,
+ &self.wrapper,
+ &mut self.matching_context,
+ )
+ }
+
+ fn scan_dependency(&mut self, dependency: &'selectors Dependency) {
+ debug_assert!(
+ matches!(
+ dependency.invalidation_kind(),
+ DependencyInvalidationKind::Normal(_)
+ ),
+ "Found relative selector dependency"
+ );
+ debug!(
+ "TreeStyleInvalidator::scan_dependency({:?}, {:?})",
+ self.element, dependency
+ );
+
+ if !self.dependency_may_be_relevant(dependency) {
+ return;
+ }
+
+ if self.check_dependency(dependency) {
+ return self.note_dependency(dependency);
+ }
+ }
+
+ fn note_dependency(&mut self, dependency: &'selectors Dependency) {
+ debug_assert!(self.dependency_may_be_relevant(dependency));
+
+ let invalidation_kind = dependency.normal_invalidation_kind();
+ if matches!(invalidation_kind, NormalDependencyInvalidationKind::Element) {
+ if let Some(ref parent) = dependency.parent {
+ // We know something changed in the inner selector, go outwards
+ // now.
+ self.scan_dependency(parent);
+ } else {
+ self.invalidates_self = true;
+ }
+ return;
+ }
+
+ debug_assert_ne!(dependency.selector_offset, 0);
+ debug_assert_ne!(dependency.selector_offset, dependency.selector.len());
+
+ let invalidation =
+ Invalidation::new(&dependency, self.matching_context.current_host.clone());
+
+ self.invalidates_self |= push_invalidation(
+ invalidation,
+ invalidation_kind,
+ self.descendant_invalidations,
+ self.sibling_invalidations,
+ );
+ }
+
+ /// Returns whether `dependency` may cause us to invalidate the style of
+ /// more elements than what we've already invalidated.
+ fn dependency_may_be_relevant(&self, dependency: &Dependency) -> bool {
+ match dependency.normal_invalidation_kind() {
+ NormalDependencyInvalidationKind::Element => !self.invalidates_self,
+ NormalDependencyInvalidationKind::SlottedElements => {
+ self.element.is_html_slot_element()
+ },
+ NormalDependencyInvalidationKind::Parts => self.element.shadow_root().is_some(),
+ NormalDependencyInvalidationKind::ElementAndDescendants |
+ NormalDependencyInvalidationKind::Siblings |
+ NormalDependencyInvalidationKind::Descendants => true,
+ }
+ }
+}
+
+pub(crate) fn push_invalidation<'a>(
+ invalidation: Invalidation<'a>,
+ invalidation_kind: NormalDependencyInvalidationKind,
+ descendant_invalidations: &mut DescendantInvalidationLists<'a>,
+ sibling_invalidations: &mut InvalidationVector<'a>,
+) -> bool {
+ match invalidation_kind {
+ NormalDependencyInvalidationKind::Element => unreachable!(),
+ NormalDependencyInvalidationKind::ElementAndDescendants => {
+ descendant_invalidations.dom_descendants.push(invalidation);
+ true
+ },
+ NormalDependencyInvalidationKind::Descendants => {
+ descendant_invalidations.dom_descendants.push(invalidation);
+ false
+ },
+ NormalDependencyInvalidationKind::Siblings => {
+ sibling_invalidations.push(invalidation);
+ false
+ },
+ NormalDependencyInvalidationKind::Parts => {
+ descendant_invalidations.parts.push(invalidation);
+ false
+ },
+ NormalDependencyInvalidationKind::SlottedElements => {
+ descendant_invalidations
+ .slotted_descendants
+ .push(invalidation);
+ false
+ },
+ }
+}
+
+pub(crate) fn dependency_may_be_relevant<E: TElement>(
+ dependency: &Dependency,
+ element: &E,
+ already_invalidated_self: bool,
+) -> bool {
+ match dependency.normal_invalidation_kind() {
+ NormalDependencyInvalidationKind::Element => !already_invalidated_self,
+ NormalDependencyInvalidationKind::SlottedElements => element.is_html_slot_element(),
+ NormalDependencyInvalidationKind::Parts => element.shadow_root().is_some(),
+ NormalDependencyInvalidationKind::ElementAndDescendants |
+ NormalDependencyInvalidationKind::Siblings |
+ NormalDependencyInvalidationKind::Descendants => true,
+ }
+}
diff --git a/servo/components/style/invalidation/media_queries.rs b/servo/components/style/invalidation/media_queries.rs
new file mode 100644
index 0000000000..6928b29d3d
--- /dev/null
+++ b/servo/components/style/invalidation/media_queries.rs
@@ -0,0 +1,130 @@
+/* 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/. */
+
+//! Code related to the invalidation of media-query-affected rules.
+
+use crate::context::QuirksMode;
+use crate::media_queries::Device;
+use crate::shared_lock::SharedRwLockReadGuard;
+use crate::stylesheets::{DocumentRule, ImportRule, MediaRule};
+use crate::stylesheets::{NestedRuleIterationCondition, StylesheetContents, SupportsRule};
+use fxhash::FxHashSet;
+
+/// A key for a given media query result.
+///
+/// NOTE: It happens to be the case that all the media lists we care about
+/// happen to have a stable address, so we can just use an opaque pointer to
+/// represent them.
+///
+/// Also, note that right now when a rule or stylesheet is removed, we do a full
+/// style flush, so there's no need to worry about other item created with the
+/// same pointer address.
+///
+/// If this changes, though, we may need to remove the item from the cache if
+/// present before it goes away.
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+pub struct MediaListKey(usize);
+
+impl MediaListKey {
+ /// Create a MediaListKey from a raw usize.
+ pub fn from_raw(k: usize) -> Self {
+ MediaListKey(k)
+ }
+}
+
+/// A trait to get a given `MediaListKey` for a given item that can hold a
+/// `MediaList`.
+pub trait ToMediaListKey: Sized {
+ /// Get a `MediaListKey` for this item. This key needs to uniquely identify
+ /// the item.
+ fn to_media_list_key(&self) -> MediaListKey {
+ MediaListKey(self as *const Self as usize)
+ }
+}
+
+impl ToMediaListKey for StylesheetContents {}
+impl ToMediaListKey for ImportRule {}
+impl ToMediaListKey for MediaRule {}
+
+/// A struct that holds the result of a media query evaluation pass for the
+/// media queries that evaluated successfully.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
+pub struct EffectiveMediaQueryResults {
+ /// The set of media lists that matched last time.
+ set: FxHashSet<MediaListKey>,
+}
+
+impl EffectiveMediaQueryResults {
+ /// Trivially constructs an empty `EffectiveMediaQueryResults`.
+ pub fn new() -> Self {
+ Self {
+ set: FxHashSet::default(),
+ }
+ }
+
+ /// Resets the results, using an empty key.
+ pub fn clear(&mut self) {
+ self.set.clear()
+ }
+
+ /// Returns whether a given item was known to be effective when the results
+ /// were cached.
+ pub fn was_effective<T>(&self, item: &T) -> bool
+ where
+ T: ToMediaListKey,
+ {
+ self.set.contains(&item.to_media_list_key())
+ }
+
+ /// Notices that an effective item has been seen, and caches it as matching.
+ pub fn saw_effective<T>(&mut self, item: &T)
+ where
+ T: ToMediaListKey,
+ {
+ // NOTE(emilio): We can't assert that we don't cache the same item twice
+ // because of stylesheet reusing... shrug.
+ self.set.insert(item.to_media_list_key());
+ }
+}
+
+/// A filter that filters over effective rules, but allowing all potentially
+/// effective `@media` rules.
+pub struct PotentiallyEffectiveMediaRules;
+
+impl NestedRuleIterationCondition for PotentiallyEffectiveMediaRules {
+ fn process_import(
+ _: &SharedRwLockReadGuard,
+ _: &Device,
+ _: QuirksMode,
+ _: &ImportRule,
+ ) -> bool {
+ true
+ }
+
+ fn process_media(_: &SharedRwLockReadGuard, _: &Device, _: QuirksMode, _: &MediaRule) -> bool {
+ true
+ }
+
+ /// Whether we should process the nested rules in a given `@-moz-document` rule.
+ fn process_document(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &DocumentRule,
+ ) -> bool {
+ use crate::stylesheets::EffectiveRules;
+ EffectiveRules::process_document(guard, device, quirks_mode, rule)
+ }
+
+ /// Whether we should process the nested rules in a given `@supports` rule.
+ fn process_supports(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &SupportsRule,
+ ) -> bool {
+ use crate::stylesheets::EffectiveRules;
+ EffectiveRules::process_supports(guard, device, quirks_mode, rule)
+ }
+}
diff --git a/servo/components/style/invalidation/mod.rs b/servo/components/style/invalidation/mod.rs
new file mode 100644
index 0000000000..12b0d06853
--- /dev/null
+++ b/servo/components/style/invalidation/mod.rs
@@ -0,0 +1,10 @@
+/* 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/. */
+
+//! Different bits of code related to invalidating style.
+
+pub mod element;
+pub mod media_queries;
+pub mod stylesheets;
+pub mod viewport_units;
diff --git a/servo/components/style/invalidation/stylesheets.rs b/servo/components/style/invalidation/stylesheets.rs
new file mode 100644
index 0000000000..d845897aa4
--- /dev/null
+++ b/servo/components/style/invalidation/stylesheets.rs
@@ -0,0 +1,651 @@
+/* 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/. */
+
+//! A collection of invalidations due to changes in which stylesheets affect a
+//! document.
+
+#![deny(unsafe_code)]
+
+use crate::context::QuirksMode;
+use crate::dom::{TDocument, TElement, TNode};
+use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper};
+use crate::invalidation::element::restyle_hints::RestyleHint;
+use crate::media_queries::Device;
+use crate::selector_map::{MaybeCaseInsensitiveHashMap, PrecomputedHashMap};
+use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap};
+use crate::shared_lock::SharedRwLockReadGuard;
+use crate::stylesheets::{CssRule, StylesheetInDocument};
+use crate::stylesheets::{EffectiveRules, EffectiveRulesIterator};
+use crate::values::AtomIdent;
+use crate::LocalName as SelectorLocalName;
+use crate::{Atom, ShrinkIfNeeded};
+use selectors::parser::{Component, LocalName, Selector};
+
+/// The kind of change that happened for a given rule.
+#[repr(u32)]
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+pub enum RuleChangeKind {
+ /// The rule was inserted.
+ Insertion,
+ /// The rule was removed.
+ Removal,
+ /// Some change in the rule which we don't know about, and could have made
+ /// the rule change in any way.
+ Generic,
+ /// A change in the declarations of a style rule.
+ StyleRuleDeclarations,
+}
+
+/// A style sheet invalidation represents a kind of element or subtree that may
+/// need to be restyled. Whether it represents a whole subtree or just a single
+/// element is determined by the given InvalidationKind in
+/// StylesheetInvalidationSet's maps.
+#[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+enum Invalidation {
+ /// An element with a given id.
+ ID(AtomIdent),
+ /// An element with a given class name.
+ Class(AtomIdent),
+ /// An element with a given local name.
+ LocalName {
+ name: SelectorLocalName,
+ lower_name: SelectorLocalName,
+ },
+}
+
+impl Invalidation {
+ fn is_id(&self) -> bool {
+ matches!(*self, Invalidation::ID(..))
+ }
+
+ fn is_id_or_class(&self) -> bool {
+ matches!(*self, Invalidation::ID(..) | Invalidation::Class(..))
+ }
+}
+
+/// Whether we should invalidate just the element, or the whole subtree within
+/// it.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)]
+enum InvalidationKind {
+ None = 0,
+ Element,
+ Scope,
+}
+
+impl std::ops::BitOrAssign for InvalidationKind {
+ #[inline]
+ fn bitor_assign(&mut self, other: Self) {
+ *self = std::cmp::max(*self, other);
+ }
+}
+
+impl InvalidationKind {
+ #[inline]
+ fn is_scope(self) -> bool {
+ matches!(self, Self::Scope)
+ }
+
+ #[inline]
+ fn add(&mut self, other: Option<&InvalidationKind>) {
+ if let Some(other) = other {
+ *self |= *other;
+ }
+ }
+}
+
+/// A set of invalidations due to stylesheet additions.
+///
+/// TODO(emilio): We might be able to do the same analysis for media query
+/// changes too (or even selector changes?).
+#[derive(Debug, Default, MallocSizeOf)]
+pub struct StylesheetInvalidationSet {
+ classes: MaybeCaseInsensitiveHashMap<Atom, InvalidationKind>,
+ ids: MaybeCaseInsensitiveHashMap<Atom, InvalidationKind>,
+ local_names: PrecomputedHashMap<SelectorLocalName, InvalidationKind>,
+ fully_invalid: bool,
+}
+
+impl StylesheetInvalidationSet {
+ /// Create an empty `StylesheetInvalidationSet`.
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Mark the DOM tree styles' as fully invalid.
+ pub fn invalidate_fully(&mut self) {
+ debug!("StylesheetInvalidationSet::invalidate_fully");
+ self.clear();
+ self.fully_invalid = true;
+ }
+
+ fn shrink_if_needed(&mut self) {
+ if self.fully_invalid {
+ return;
+ }
+ self.classes.shrink_if_needed();
+ self.ids.shrink_if_needed();
+ self.local_names.shrink_if_needed();
+ }
+
+ /// Analyze the given stylesheet, and collect invalidations from their
+ /// rules, in order to avoid doing a full restyle when we style the document
+ /// next time.
+ pub fn collect_invalidations_for<S>(
+ &mut self,
+ device: &Device,
+ stylesheet: &S,
+ guard: &SharedRwLockReadGuard,
+ ) where
+ S: StylesheetInDocument,
+ {
+ debug!("StylesheetInvalidationSet::collect_invalidations_for");
+ if self.fully_invalid {
+ debug!(" > Fully invalid already");
+ return;
+ }
+
+ if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) {
+ debug!(" > Stylesheet was not effective");
+ return; // Nothing to do here.
+ }
+
+ let quirks_mode = device.quirks_mode();
+ for rule in stylesheet.effective_rules(device, guard) {
+ self.collect_invalidations_for_rule(
+ rule,
+ guard,
+ device,
+ quirks_mode,
+ /* is_generic_change = */ false,
+ );
+ if self.fully_invalid {
+ break;
+ }
+ }
+
+ self.shrink_if_needed();
+
+ debug!(" > resulting class invalidations: {:?}", self.classes);
+ debug!(" > resulting id invalidations: {:?}", self.ids);
+ debug!(
+ " > resulting local name invalidations: {:?}",
+ self.local_names
+ );
+ debug!(" > fully_invalid: {}", self.fully_invalid);
+ }
+
+ /// Clears the invalidation set, invalidating elements as needed if
+ /// `document_element` is provided.
+ ///
+ /// Returns true if any invalidations ocurred.
+ pub fn flush<E>(&mut self, document_element: Option<E>, snapshots: Option<&SnapshotMap>) -> bool
+ where
+ E: TElement,
+ {
+ debug!(
+ "Stylist::flush({:?}, snapshots: {})",
+ document_element,
+ snapshots.is_some()
+ );
+ let have_invalidations = match document_element {
+ Some(e) => self.process_invalidations(e, snapshots),
+ None => false,
+ };
+ self.clear();
+ have_invalidations
+ }
+
+ /// Returns whether there's no invalidation to process.
+ pub fn is_empty(&self) -> bool {
+ !self.fully_invalid &&
+ self.classes.is_empty() &&
+ self.ids.is_empty() &&
+ self.local_names.is_empty()
+ }
+
+ fn invalidation_kind_for<E>(
+ &self,
+ element: E,
+ snapshot: Option<&Snapshot>,
+ quirks_mode: QuirksMode,
+ ) -> InvalidationKind
+ where
+ E: TElement,
+ {
+ debug_assert!(!self.fully_invalid);
+
+ let mut kind = InvalidationKind::None;
+
+ if !self.classes.is_empty() {
+ element.each_class(|c| {
+ kind.add(self.classes.get(c, quirks_mode));
+ });
+
+ if kind.is_scope() {
+ return kind;
+ }
+
+ if let Some(snapshot) = snapshot {
+ snapshot.each_class(|c| {
+ kind.add(self.classes.get(c, quirks_mode));
+ });
+
+ if kind.is_scope() {
+ return kind;
+ }
+ }
+ }
+
+ if !self.ids.is_empty() {
+ if let Some(ref id) = element.id() {
+ kind.add(self.ids.get(id, quirks_mode));
+ if kind.is_scope() {
+ return kind;
+ }
+ }
+
+ if let Some(ref old_id) = snapshot.and_then(|s| s.id_attr()) {
+ kind.add(self.ids.get(old_id, quirks_mode));
+ if kind.is_scope() {
+ return kind;
+ }
+ }
+ }
+
+ if !self.local_names.is_empty() {
+ kind.add(self.local_names.get(element.local_name()));
+ }
+
+ kind
+ }
+
+ /// Clears the invalidation set without processing.
+ pub fn clear(&mut self) {
+ self.classes.clear();
+ self.ids.clear();
+ self.local_names.clear();
+ self.fully_invalid = false;
+ debug_assert!(self.is_empty());
+ }
+
+ fn process_invalidations<E>(&self, element: E, snapshots: Option<&SnapshotMap>) -> bool
+ where
+ E: TElement,
+ {
+ debug!("Stylist::process_invalidations({:?}, {:?})", element, self);
+
+ {
+ let mut data = match element.mutate_data() {
+ Some(data) => data,
+ None => return false,
+ };
+
+ if self.fully_invalid {
+ debug!("process_invalidations: fully_invalid({:?})", element);
+ data.hint.insert(RestyleHint::restyle_subtree());
+ return true;
+ }
+ }
+
+ if self.is_empty() {
+ debug!("process_invalidations: empty invalidation set");
+ return false;
+ }
+
+ let quirks_mode = element.as_node().owner_doc().quirks_mode();
+ self.process_invalidations_in_subtree(element, snapshots, quirks_mode)
+ }
+
+ /// Process style invalidations in a given subtree. This traverses the
+ /// subtree looking for elements that match the invalidations in our hash
+ /// map members.
+ ///
+ /// Returns whether it invalidated at least one element's style.
+ #[allow(unsafe_code)]
+ fn process_invalidations_in_subtree<E>(
+ &self,
+ element: E,
+ snapshots: Option<&SnapshotMap>,
+ quirks_mode: QuirksMode,
+ ) -> bool
+ where
+ E: TElement,
+ {
+ debug!("process_invalidations_in_subtree({:?})", element);
+ let mut data = match element.mutate_data() {
+ Some(data) => data,
+ None => return false,
+ };
+
+ if !data.has_styles() {
+ return false;
+ }
+
+ if data.hint.contains_subtree() {
+ debug!(
+ "process_invalidations_in_subtree: {:?} was already invalid",
+ element
+ );
+ return false;
+ }
+
+ let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s));
+ let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot());
+
+ match self.invalidation_kind_for(element, snapshot, quirks_mode) {
+ InvalidationKind::None => {},
+ InvalidationKind::Element => {
+ debug!(
+ "process_invalidations_in_subtree: {:?} matched self",
+ element
+ );
+ data.hint.insert(RestyleHint::RESTYLE_SELF);
+ },
+ InvalidationKind::Scope => {
+ debug!(
+ "process_invalidations_in_subtree: {:?} matched subtree",
+ element
+ );
+ data.hint.insert(RestyleHint::restyle_subtree());
+ return true;
+ },
+ }
+
+ let mut any_children_invalid = false;
+
+ for child in element.traversal_children() {
+ let child = match child.as_element() {
+ Some(e) => e,
+ None => continue,
+ };
+
+ any_children_invalid |=
+ self.process_invalidations_in_subtree(child, snapshots, quirks_mode);
+ }
+
+ if any_children_invalid {
+ debug!(
+ "Children of {:?} changed, setting dirty descendants",
+ element
+ );
+ unsafe { element.set_dirty_descendants() }
+ }
+
+ data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid
+ }
+
+ /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles
+ /// :is() / :where() etc.
+ fn scan_component(
+ component: &Component<SelectorImpl>,
+ invalidation: &mut Option<Invalidation>,
+ ) {
+ match *component {
+ Component::LocalName(LocalName {
+ ref name,
+ ref lower_name,
+ }) => {
+ if invalidation.is_none() {
+ *invalidation = Some(Invalidation::LocalName {
+ name: name.clone(),
+ lower_name: lower_name.clone(),
+ });
+ }
+ },
+ Component::Class(ref class) => {
+ if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) {
+ *invalidation = Some(Invalidation::Class(class.clone()));
+ }
+ },
+ Component::ID(ref id) => {
+ if invalidation.as_ref().map_or(true, |s| !s.is_id()) {
+ *invalidation = Some(Invalidation::ID(id.clone()));
+ }
+ },
+ _ => {
+ // Ignore everything else, at least for now.
+ },
+ }
+ }
+
+ /// Collect invalidations for a given selector.
+ ///
+ /// We look at the outermost local name, class, or ID selector to the left
+ /// of an ancestor combinator, in order to restyle only a given subtree.
+ ///
+ /// If the selector has no ancestor combinator, then we do the same for
+ /// the only sequence it has, but record it as an element invalidation
+ /// instead of a subtree invalidation.
+ ///
+ /// We prefer IDs to classs, and classes to local names, on the basis
+ /// that the former should be more specific than the latter. We also
+ /// prefer to generate subtree invalidations for the outermost part
+ /// of the selector, to reduce the amount of traversal we need to do
+ /// when flushing invalidations.
+ fn collect_invalidations(
+ &mut self,
+ selector: &Selector<SelectorImpl>,
+ quirks_mode: QuirksMode,
+ ) {
+ debug!(
+ "StylesheetInvalidationSet::collect_invalidations({:?})",
+ selector
+ );
+
+ let mut element_invalidation: Option<Invalidation> = None;
+ let mut subtree_invalidation: Option<Invalidation> = None;
+
+ let mut scan_for_element_invalidation = true;
+ let mut scan_for_subtree_invalidation = false;
+
+ let mut iter = selector.iter();
+
+ loop {
+ for component in &mut iter {
+ if scan_for_element_invalidation {
+ Self::scan_component(component, &mut element_invalidation);
+ } else if scan_for_subtree_invalidation {
+ Self::scan_component(component, &mut subtree_invalidation);
+ }
+ }
+ match iter.next_sequence() {
+ None => break,
+ Some(combinator) => {
+ scan_for_subtree_invalidation = combinator.is_ancestor();
+ },
+ }
+ scan_for_element_invalidation = false;
+ }
+
+ if let Some(s) = subtree_invalidation {
+ debug!(" > Found subtree invalidation: {:?}", s);
+ if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) {
+ return;
+ }
+ }
+
+ if let Some(s) = element_invalidation {
+ debug!(" > Found element invalidation: {:?}", s);
+ if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) {
+ return;
+ }
+ }
+
+ // The selector was of a form that we can't handle. Any element could
+ // match it, so let's just bail out.
+ debug!(" > Can't handle selector or OOMd, marking fully invalid");
+ self.invalidate_fully()
+ }
+
+ fn insert_invalidation(
+ &mut self,
+ invalidation: Invalidation,
+ kind: InvalidationKind,
+ quirks_mode: QuirksMode,
+ ) -> bool {
+ match invalidation {
+ Invalidation::Class(c) => {
+ let entry = match self.classes.try_entry(c.0, quirks_mode) {
+ Ok(e) => e,
+ Err(..) => return false,
+ };
+ *entry.or_insert(InvalidationKind::None) |= kind;
+ },
+ Invalidation::ID(i) => {
+ let entry = match self.ids.try_entry(i.0, quirks_mode) {
+ Ok(e) => e,
+ Err(..) => return false,
+ };
+ *entry.or_insert(InvalidationKind::None) |= kind;
+ },
+ Invalidation::LocalName { name, lower_name } => {
+ let insert_lower = name != lower_name;
+ if self.local_names.try_reserve(1).is_err() {
+ return false;
+ }
+ let entry = self.local_names.entry(name);
+ *entry.or_insert(InvalidationKind::None) |= kind;
+ if insert_lower {
+ if self.local_names.try_reserve(1).is_err() {
+ return false;
+ }
+ let entry = self.local_names.entry(lower_name);
+ *entry.or_insert(InvalidationKind::None) |= kind;
+ }
+ },
+ }
+
+ true
+ }
+
+ /// Collects invalidations for a given CSS rule, if not fully invalid
+ /// already.
+ ///
+ /// TODO(emilio): we can't check whether the rule is inside a non-effective
+ /// subtree, we potentially could do that.
+ pub fn rule_changed<S>(
+ &mut self,
+ stylesheet: &S,
+ rule: &CssRule,
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ change_kind: RuleChangeKind,
+ ) where
+ S: StylesheetInDocument,
+ {
+ debug!("StylesheetInvalidationSet::rule_changed");
+ if self.fully_invalid {
+ return;
+ }
+
+ if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) {
+ debug!(" > Stylesheet was not effective");
+ return; // Nothing to do here.
+ }
+
+ // If the change is generic, we don't have the old rule information to know e.g., the old
+ // media condition, or the old selector text, so we might need to invalidate more
+ // aggressively. That only applies to the changed rules, for other rules we can just
+ // collect invalidations as normal.
+ let is_generic_change = change_kind == RuleChangeKind::Generic;
+ self.collect_invalidations_for_rule(rule, guard, device, quirks_mode, is_generic_change);
+ if self.fully_invalid {
+ return;
+ }
+
+ if !is_generic_change && !EffectiveRules::is_effective(guard, device, quirks_mode, rule) {
+ return;
+ }
+
+ let rules = EffectiveRulesIterator::effective_children(device, quirks_mode, guard, rule);
+ for rule in rules {
+ self.collect_invalidations_for_rule(
+ rule,
+ guard,
+ device,
+ quirks_mode,
+ /* is_generic_change = */ false,
+ );
+ if self.fully_invalid {
+ break;
+ }
+ }
+ }
+
+ /// Collects invalidations for a given CSS rule.
+ fn collect_invalidations_for_rule(
+ &mut self,
+ rule: &CssRule,
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ is_generic_change: bool,
+ ) {
+ use crate::stylesheets::CssRule::*;
+ debug!("StylesheetInvalidationSet::collect_invalidations_for_rule");
+ debug_assert!(!self.fully_invalid, "Not worth being here!");
+
+ match *rule {
+ Style(ref lock) => {
+ if is_generic_change {
+ // TODO(emilio): We need to do this for selector / keyframe
+ // name / font-face changes, because we don't have the old
+ // selector / name. If we distinguish those changes
+ // specially, then we can at least use this invalidation for
+ // style declaration changes.
+ return self.invalidate_fully();
+ }
+
+ let style_rule = lock.read_with(guard);
+ for selector in style_rule.selectors.slice() {
+ self.collect_invalidations(selector, quirks_mode);
+ if self.fully_invalid {
+ return;
+ }
+ }
+ },
+ Namespace(..) => {
+ // It's not clear what handling changes for this correctly would
+ // look like.
+ },
+ LayerStatement(..) => {
+ // Layer statement insertions might alter styling order, so we need to always
+ // invalidate fully.
+ return self.invalidate_fully();
+ },
+ Document(..) | Import(..) | Media(..) | Supports(..) | Container(..) |
+ LayerBlock(..) => {
+ // Do nothing, relevant nested rules are visited as part of rule iteration.
+ },
+ FontFace(..) => {
+ // Do nothing, @font-face doesn't affect computed style information on it's own.
+ // We'll restyle when the font face loads, if needed.
+ },
+ Page(..) | Margin(..) => {
+ // Do nothing, we don't support OM mutations on print documents, and page rules
+ // can't affect anything else.
+ },
+ Keyframes(ref lock) => {
+ if is_generic_change {
+ return self.invalidate_fully();
+ }
+ let keyframes_rule = lock.read_with(guard);
+ if device.animation_name_may_be_referenced(&keyframes_rule.name) {
+ debug!(
+ " > Found @keyframes rule potentially referenced \
+ from the page, marking the whole tree invalid."
+ );
+ self.invalidate_fully();
+ } else {
+ // Do nothing, this animation can't affect the style of existing elements.
+ }
+ },
+ CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => {
+ debug!(" > Found unsupported rule, marking the whole subtree invalid.");
+ self.invalidate_fully();
+ },
+ }
+ }
+}
diff --git a/servo/components/style/invalidation/viewport_units.rs b/servo/components/style/invalidation/viewport_units.rs
new file mode 100644
index 0000000000..06faeb14c4
--- /dev/null
+++ b/servo/components/style/invalidation/viewport_units.rs
@@ -0,0 +1,71 @@
+/* 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/. */
+
+//! Invalidates style of all elements that depend on viewport units.
+
+use crate::data::ViewportUnitUsage;
+use crate::dom::{TElement, TNode};
+use crate::invalidation::element::restyle_hints::RestyleHint;
+
+/// Invalidates style of all elements that depend on viewport units.
+///
+/// Returns whether any element was invalidated.
+pub fn invalidate<E>(root: E) -> bool
+where
+ E: TElement,
+{
+ debug!("invalidation::viewport_units::invalidate({:?})", root);
+ invalidate_recursively(root)
+}
+
+fn invalidate_recursively<E>(element: E) -> bool
+where
+ E: TElement,
+{
+ let mut data = match element.mutate_data() {
+ Some(data) => data,
+ None => return false,
+ };
+
+ if data.hint.will_recascade_subtree() {
+ debug!("invalidate_recursively: {:?} was already invalid", element);
+ return false;
+ }
+
+ let usage = data.styles.viewport_unit_usage();
+ let uses_viewport_units = usage != ViewportUnitUsage::None;
+ if uses_viewport_units {
+ debug!(
+ "invalidate_recursively: {:?} uses viewport units {:?}",
+ element, usage
+ );
+ }
+
+ match usage {
+ ViewportUnitUsage::None => {},
+ ViewportUnitUsage::FromQuery => {
+ data.hint.insert(RestyleHint::RESTYLE_SELF);
+ },
+ ViewportUnitUsage::FromDeclaration => {
+ data.hint.insert(RestyleHint::RECASCADE_SELF);
+ },
+ }
+
+ let mut any_children_invalid = false;
+ for child in element.traversal_children() {
+ if let Some(child) = child.as_element() {
+ any_children_invalid |= invalidate_recursively(child);
+ }
+ }
+
+ if any_children_invalid {
+ debug!(
+ "invalidate_recursively: Children of {:?} changed, setting dirty descendants",
+ element
+ );
+ unsafe { element.set_dirty_descendants() }
+ }
+
+ uses_viewport_units || any_children_invalid
+}
diff --git a/servo/components/style/lib.rs b/servo/components/style/lib.rs
new file mode 100644
index 0000000000..b2a1e20ce8
--- /dev/null
+++ b/servo/components/style/lib.rs
@@ -0,0 +1,332 @@
+/* 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/. */
+
+//! Calculate [specified][specified] and [computed values][computed] from a
+//! tree of DOM nodes and a set of stylesheets.
+//!
+//! [computed]: https://drafts.csswg.org/css-cascade/#computed
+//! [specified]: https://drafts.csswg.org/css-cascade/#specified
+//!
+//! In particular, this crate contains the definitions of supported properties,
+//! the code to parse them into specified values and calculate the computed
+//! values based on the specified values, as well as the code to serialize both
+//! specified and computed values.
+//!
+//! The main entry point is [`recalc_style_at`][recalc_style_at].
+//!
+//! [recalc_style_at]: traversal/fn.recalc_style_at.html
+//!
+//! Major dependencies are the [cssparser][cssparser] and [selectors][selectors]
+//! crates.
+//!
+//! [cssparser]: ../cssparser/index.html
+//! [selectors]: ../selectors/index.html
+
+#![deny(missing_docs)]
+
+#[macro_use]
+extern crate bitflags;
+#[macro_use]
+extern crate cssparser;
+#[macro_use]
+extern crate debug_unreachable;
+#[macro_use]
+extern crate derive_more;
+#[macro_use]
+extern crate gecko_profiler;
+#[cfg(feature = "gecko")]
+#[macro_use]
+pub mod gecko_string_cache;
+#[cfg(feature = "servo")]
+#[macro_use]
+extern crate html5ever;
+#[macro_use]
+extern crate lazy_static;
+#[macro_use]
+extern crate log;
+#[macro_use]
+extern crate malloc_size_of;
+#[macro_use]
+extern crate malloc_size_of_derive;
+#[allow(unused_extern_crates)]
+#[macro_use]
+extern crate matches;
+#[cfg(feature = "gecko")]
+pub use nsstring;
+#[cfg(feature = "gecko")]
+extern crate num_cpus;
+#[macro_use]
+extern crate num_derive;
+#[macro_use]
+extern crate serde;
+pub use servo_arc;
+#[cfg(feature = "servo")]
+#[macro_use]
+extern crate servo_atoms;
+#[macro_use]
+extern crate static_assertions;
+#[macro_use]
+extern crate style_derive;
+#[macro_use]
+extern crate to_shmem_derive;
+
+#[macro_use]
+mod macros;
+
+pub mod animation;
+pub mod applicable_declarations;
+#[allow(missing_docs)] // TODO.
+#[cfg(feature = "servo")]
+pub mod attr;
+pub mod author_styles;
+pub mod bezier;
+pub mod bloom;
+pub mod color;
+#[path = "properties/computed_value_flags.rs"]
+pub mod computed_value_flags;
+pub mod context;
+pub mod counter_style;
+pub mod custom_properties;
+pub mod custom_properties_map;
+pub mod data;
+pub mod dom;
+pub mod dom_apis;
+pub mod driver;
+#[cfg(feature = "servo")]
+mod encoding_support;
+pub mod error_reporting;
+pub mod font_face;
+pub mod font_metrics;
+#[cfg(feature = "gecko")]
+#[allow(unsafe_code)]
+pub mod gecko_bindings;
+pub mod global_style_data;
+pub mod invalidation;
+#[allow(missing_docs)] // TODO.
+pub mod logical_geometry;
+pub mod matching;
+pub mod media_queries;
+pub mod parallel;
+pub mod parser;
+pub mod piecewise_linear;
+pub mod properties_and_values;
+#[macro_use]
+pub mod queries;
+pub mod rule_cache;
+pub mod rule_collector;
+pub mod rule_tree;
+pub mod scoped_tls;
+pub mod selector_map;
+pub mod selector_parser;
+pub mod shared_lock;
+pub mod sharing;
+pub mod str;
+pub mod style_adjuster;
+pub mod style_resolver;
+pub mod stylesheet_set;
+pub mod stylesheets;
+pub mod stylist;
+pub mod thread_state;
+pub mod traversal;
+pub mod traversal_flags;
+pub mod use_counters;
+#[macro_use]
+#[allow(non_camel_case_types)]
+pub mod values;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko_string_cache as string_cache;
+#[cfg(feature = "gecko")]
+pub use crate::gecko_string_cache::Atom;
+/// The namespace prefix type for Gecko, which is just an atom.
+#[cfg(feature = "gecko")]
+pub type Prefix = crate::values::AtomIdent;
+/// The local name of an element for Gecko, which is just an atom.
+#[cfg(feature = "gecko")]
+pub type LocalName = crate::values::AtomIdent;
+#[cfg(feature = "gecko")]
+pub use crate::gecko_string_cache::Namespace;
+
+#[cfg(feature = "servo")]
+pub use servo_atoms::Atom;
+
+#[cfg(feature = "servo")]
+#[allow(missing_docs)]
+pub type LocalName = crate::values::GenericAtomIdent<html5ever::LocalNameStaticSet>;
+#[cfg(feature = "servo")]
+#[allow(missing_docs)]
+pub type Namespace = crate::values::GenericAtomIdent<html5ever::NamespaceStaticSet>;
+#[cfg(feature = "servo")]
+#[allow(missing_docs)]
+pub type Prefix = crate::values::GenericAtomIdent<html5ever::PrefixStaticSet>;
+
+pub use style_traits::arc_slice::ArcSlice;
+pub use style_traits::owned_slice::OwnedSlice;
+pub use style_traits::owned_str::OwnedStr;
+
+use std::hash::{BuildHasher, Hash};
+
+pub mod properties;
+
+#[cfg(feature = "gecko")]
+#[allow(unsafe_code)]
+pub mod gecko;
+
+// uses a macro from properties
+#[cfg(feature = "servo")]
+#[allow(unsafe_code)]
+pub mod servo;
+
+macro_rules! reexport_computed_values {
+ ( $( { $name: ident } )+ ) => {
+ /// Types for [computed values][computed].
+ ///
+ /// [computed]: https://drafts.csswg.org/css-cascade/#computed
+ pub mod computed_values {
+ $(
+ pub use crate::properties::longhands::$name::computed_value as $name;
+ )+
+ // Don't use a side-specific name needlessly:
+ pub use crate::properties::longhands::border_top_style::computed_value as border_style;
+ }
+ }
+}
+longhand_properties_idents!(reexport_computed_values);
+#[cfg(feature = "gecko")]
+use crate::gecko_string_cache::WeakAtom;
+#[cfg(feature = "servo")]
+use servo_atoms::Atom as WeakAtom;
+
+/// Extension methods for selectors::attr::CaseSensitivity
+pub trait CaseSensitivityExt {
+ /// Return whether two atoms compare equal according to this case sensitivity.
+ fn eq_atom(self, a: &WeakAtom, b: &WeakAtom) -> bool;
+}
+
+impl CaseSensitivityExt for selectors::attr::CaseSensitivity {
+ #[inline]
+ fn eq_atom(self, a: &WeakAtom, b: &WeakAtom) -> bool {
+ match self {
+ selectors::attr::CaseSensitivity::CaseSensitive => a == b,
+ selectors::attr::CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b),
+ }
+ }
+}
+
+/// A trait pretty much similar to num_traits::Zero, but without the need of
+/// implementing `Add`.
+pub trait Zero {
+ /// Returns the zero value.
+ fn zero() -> Self;
+
+ /// Returns whether this value is zero.
+ fn is_zero(&self) -> bool;
+}
+
+impl<T> Zero for T
+where
+ T: num_traits::Zero,
+{
+ fn zero() -> Self {
+ <Self as num_traits::Zero>::zero()
+ }
+
+ fn is_zero(&self) -> bool {
+ <Self as num_traits::Zero>::is_zero(self)
+ }
+}
+
+/// A trait implementing a function to tell if the number is zero without a percent
+pub trait ZeroNoPercent {
+ /// So, `0px` should return `true`, but `0%` or `1px` should return `false`
+ fn is_zero_no_percent(&self) -> bool;
+}
+
+/// A trait pretty much similar to num_traits::One, but without the need of
+/// implementing `Mul`.
+pub trait One {
+ /// Reutrns the one value.
+ fn one() -> Self;
+
+ /// Returns whether this value is one.
+ fn is_one(&self) -> bool;
+}
+
+impl<T> One for T
+where
+ T: num_traits::One + PartialEq,
+{
+ fn one() -> Self {
+ <Self as num_traits::One>::one()
+ }
+
+ fn is_one(&self) -> bool {
+ *self == One::one()
+ }
+}
+
+/// An allocation error.
+///
+/// TODO(emilio): Would be nice to have more information here, or for SmallVec
+/// to return the standard error type (and then we can just return that).
+///
+/// But given we use these mostly to bail out and ignore them, it's not a big
+/// deal.
+#[derive(Debug)]
+pub struct AllocErr;
+
+impl From<smallvec::CollectionAllocErr> for AllocErr {
+ #[inline]
+ fn from(_: smallvec::CollectionAllocErr) -> Self {
+ Self
+ }
+}
+
+impl From<std::collections::TryReserveError> for AllocErr {
+ #[inline]
+ fn from(_: std::collections::TryReserveError) -> Self {
+ Self
+ }
+}
+
+/// Shrink the capacity of the collection if needed.
+pub(crate) trait ShrinkIfNeeded {
+ fn shrink_if_needed(&mut self);
+}
+
+/// We shrink the capacity of a collection if we're wasting more than a 25% of
+/// its capacity, and if the collection is arbitrarily big enough
+/// (>= CAPACITY_THRESHOLD entries).
+#[inline]
+fn should_shrink(len: usize, capacity: usize) -> bool {
+ const CAPACITY_THRESHOLD: usize = 64;
+ capacity >= CAPACITY_THRESHOLD && len + capacity / 4 < capacity
+}
+
+impl<K, V, H> ShrinkIfNeeded for std::collections::HashMap<K, V, H>
+where
+ K: Eq + Hash,
+ H: BuildHasher,
+{
+ fn shrink_if_needed(&mut self) {
+ if should_shrink(self.len(), self.capacity()) {
+ self.shrink_to_fit();
+ }
+ }
+}
+
+impl<T, H> ShrinkIfNeeded for std::collections::HashSet<T, H>
+where
+ T: Eq + Hash,
+ H: BuildHasher,
+{
+ fn shrink_if_needed(&mut self) {
+ if should_shrink(self.len(), self.capacity()) {
+ self.shrink_to_fit();
+ }
+ }
+}
+
+// TODO(emilio): Measure and see if we're wasting a lot of memory on Vec /
+// SmallVec, and if so consider shrinking those as well.
diff --git a/servo/components/style/logical_geometry.rs b/servo/components/style/logical_geometry.rs
new file mode 100644
index 0000000000..03272ae545
--- /dev/null
+++ b/servo/components/style/logical_geometry.rs
@@ -0,0 +1,1629 @@
+/* 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/. */
+
+//! Geometry in flow-relative space.
+
+use crate::properties::style_structs;
+use euclid::default::{Point2D, Rect, SideOffsets2D, Size2D};
+use euclid::num::Zero;
+use std::cmp::{max, min};
+use std::fmt::{self, Debug, Error, Formatter};
+use std::ops::{Add, Sub};
+use unicode_bidi as bidi;
+
+pub enum BlockFlowDirection {
+ TopToBottom,
+ RightToLeft,
+ LeftToRight,
+}
+
+pub enum InlineBaseDirection {
+ LeftToRight,
+ RightToLeft,
+}
+
+// TODO: improve the readability of the WritingMode serialization, refer to the Debug:fmt()
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Serialize)]
+#[repr(C)]
+pub struct WritingMode(u8);
+bitflags!(
+ impl WritingMode: u8 {
+ /// A vertical writing mode; writing-mode is vertical-rl,
+ /// vertical-lr, sideways-lr, or sideways-rl.
+ const VERTICAL = 1 << 0;
+ /// The inline flow direction is reversed against the physical
+ /// direction (i.e. right-to-left or bottom-to-top); writing-mode is
+ /// sideways-lr or direction is rtl (but not both).
+ ///
+ /// (This bit can be derived from the others, but we store it for
+ /// convenience.)
+ const INLINE_REVERSED = 1 << 1;
+ /// A vertical writing mode whose block progression direction is left-
+ /// to-right; writing-mode is vertical-lr or sideways-lr.
+ ///
+ /// Never set without VERTICAL.
+ const VERTICAL_LR = 1 << 2;
+ /// The line-over/line-under sides are inverted with respect to the
+ /// block-start/block-end edge; writing-mode is vertical-lr.
+ ///
+ /// Never set without VERTICAL and VERTICAL_LR.
+ const LINE_INVERTED = 1 << 3;
+ /// direction is rtl.
+ const RTL = 1 << 4;
+ /// All text within a vertical writing mode is displayed sideways
+ /// and runs top-to-bottom or bottom-to-top; set in these cases:
+ ///
+ /// * writing-mode: sideways-rl;
+ /// * writing-mode: sideways-lr;
+ ///
+ /// Never set without VERTICAL.
+ const VERTICAL_SIDEWAYS = 1 << 5;
+ /// Similar to VERTICAL_SIDEWAYS, but is set via text-orientation;
+ /// set in these cases:
+ ///
+ /// * writing-mode: vertical-rl; text-orientation: sideways;
+ /// * writing-mode: vertical-lr; text-orientation: sideways;
+ ///
+ /// Never set without VERTICAL.
+ const TEXT_SIDEWAYS = 1 << 6;
+ /// Horizontal text within a vertical writing mode is displayed with each
+ /// glyph upright; set in these cases:
+ ///
+ /// * writing-mode: vertical-rl; text-orientation: upright;
+ /// * writing-mode: vertical-lr: text-orientation: upright;
+ ///
+ /// Never set without VERTICAL.
+ const UPRIGHT = 1 << 7;
+ }
+);
+
+impl WritingMode {
+ /// Return a WritingMode bitflags from the relevant CSS properties.
+ pub fn new(inheritedbox_style: &style_structs::InheritedBox) -> Self {
+ use crate::properties::longhands::direction::computed_value::T as Direction;
+ use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode;
+
+ let mut flags = WritingMode::empty();
+
+ let direction = inheritedbox_style.clone_direction();
+ let writing_mode = inheritedbox_style.clone_writing_mode();
+
+ match direction {
+ Direction::Ltr => {},
+ Direction::Rtl => {
+ flags.insert(WritingMode::RTL);
+ },
+ }
+
+ match writing_mode {
+ SpecifiedWritingMode::HorizontalTb => {
+ if direction == Direction::Rtl {
+ flags.insert(WritingMode::INLINE_REVERSED);
+ }
+ },
+ SpecifiedWritingMode::VerticalRl => {
+ flags.insert(WritingMode::VERTICAL);
+ if direction == Direction::Rtl {
+ flags.insert(WritingMode::INLINE_REVERSED);
+ }
+ },
+ SpecifiedWritingMode::VerticalLr => {
+ flags.insert(WritingMode::VERTICAL);
+ flags.insert(WritingMode::VERTICAL_LR);
+ flags.insert(WritingMode::LINE_INVERTED);
+ if direction == Direction::Rtl {
+ flags.insert(WritingMode::INLINE_REVERSED);
+ }
+ },
+ #[cfg(feature = "gecko")]
+ SpecifiedWritingMode::SidewaysRl => {
+ flags.insert(WritingMode::VERTICAL);
+ flags.insert(WritingMode::VERTICAL_SIDEWAYS);
+ if direction == Direction::Rtl {
+ flags.insert(WritingMode::INLINE_REVERSED);
+ }
+ },
+ #[cfg(feature = "gecko")]
+ SpecifiedWritingMode::SidewaysLr => {
+ flags.insert(WritingMode::VERTICAL);
+ flags.insert(WritingMode::VERTICAL_LR);
+ flags.insert(WritingMode::VERTICAL_SIDEWAYS);
+ if direction == Direction::Ltr {
+ flags.insert(WritingMode::INLINE_REVERSED);
+ }
+ },
+ }
+
+ #[cfg(feature = "gecko")]
+ {
+ use crate::properties::longhands::text_orientation::computed_value::T as TextOrientation;
+
+ // text-orientation only has an effect for vertical-rl and
+ // vertical-lr values of writing-mode.
+ match writing_mode {
+ SpecifiedWritingMode::VerticalRl | SpecifiedWritingMode::VerticalLr => {
+ match inheritedbox_style.clone_text_orientation() {
+ TextOrientation::Mixed => {},
+ TextOrientation::Upright => {
+ flags.insert(WritingMode::UPRIGHT);
+
+ // https://drafts.csswg.org/css-writing-modes-3/#valdef-text-orientation-upright:
+ //
+ // > This value causes the used value of direction
+ // > to be ltr, and for the purposes of bidi
+ // > reordering, causes all characters to be treated
+ // > as strong LTR.
+ flags.remove(WritingMode::RTL);
+ flags.remove(WritingMode::INLINE_REVERSED);
+ },
+ TextOrientation::Sideways => {
+ flags.insert(WritingMode::TEXT_SIDEWAYS);
+ },
+ }
+ },
+ _ => {},
+ }
+ }
+
+ flags
+ }
+
+ /// Returns the `horizontal-tb` value.
+ pub fn horizontal_tb() -> Self {
+ Self::empty()
+ }
+
+ #[inline]
+ pub fn is_vertical(&self) -> bool {
+ self.intersects(WritingMode::VERTICAL)
+ }
+
+ #[inline]
+ pub fn is_horizontal(&self) -> bool {
+ !self.is_vertical()
+ }
+
+ /// Assuming .is_vertical(), does the block direction go left to right?
+ #[inline]
+ pub fn is_vertical_lr(&self) -> bool {
+ self.intersects(WritingMode::VERTICAL_LR)
+ }
+
+ /// Assuming .is_vertical(), does the inline direction go top to bottom?
+ #[inline]
+ pub fn is_inline_tb(&self) -> bool {
+ // https://drafts.csswg.org/css-writing-modes-3/#logical-to-physical
+ !self.intersects(WritingMode::INLINE_REVERSED)
+ }
+
+ #[inline]
+ pub fn is_bidi_ltr(&self) -> bool {
+ !self.intersects(WritingMode::RTL)
+ }
+
+ #[inline]
+ pub fn is_sideways(&self) -> bool {
+ self.intersects(WritingMode::VERTICAL_SIDEWAYS | WritingMode::TEXT_SIDEWAYS)
+ }
+
+ #[inline]
+ pub fn is_upright(&self) -> bool {
+ self.intersects(WritingMode::UPRIGHT)
+ }
+
+ /// https://drafts.csswg.org/css-writing-modes/#logical-to-physical
+ ///
+ /// | Return | line-left is… | line-right is… |
+ /// |---------|---------------|----------------|
+ /// | `true` | inline-start | inline-end |
+ /// | `false` | inline-end | inline-start |
+ #[inline]
+ pub fn line_left_is_inline_start(&self) -> bool {
+ // https://drafts.csswg.org/css-writing-modes/#inline-start
+ // “For boxes with a used direction value of ltr, this means the line-left side.
+ // For boxes with a used direction value of rtl, this means the line-right side.”
+ self.is_bidi_ltr()
+ }
+
+ #[inline]
+ pub fn inline_start_physical_side(&self) -> PhysicalSide {
+ match (self.is_vertical(), self.is_inline_tb(), self.is_bidi_ltr()) {
+ (false, _, true) => PhysicalSide::Left,
+ (false, _, false) => PhysicalSide::Right,
+ (true, true, _) => PhysicalSide::Top,
+ (true, false, _) => PhysicalSide::Bottom,
+ }
+ }
+
+ #[inline]
+ pub fn inline_end_physical_side(&self) -> PhysicalSide {
+ match (self.is_vertical(), self.is_inline_tb(), self.is_bidi_ltr()) {
+ (false, _, true) => PhysicalSide::Right,
+ (false, _, false) => PhysicalSide::Left,
+ (true, true, _) => PhysicalSide::Bottom,
+ (true, false, _) => PhysicalSide::Top,
+ }
+ }
+
+ #[inline]
+ pub fn block_start_physical_side(&self) -> PhysicalSide {
+ match (self.is_vertical(), self.is_vertical_lr()) {
+ (false, _) => PhysicalSide::Top,
+ (true, true) => PhysicalSide::Left,
+ (true, false) => PhysicalSide::Right,
+ }
+ }
+
+ #[inline]
+ pub fn block_end_physical_side(&self) -> PhysicalSide {
+ match (self.is_vertical(), self.is_vertical_lr()) {
+ (false, _) => PhysicalSide::Bottom,
+ (true, true) => PhysicalSide::Right,
+ (true, false) => PhysicalSide::Left,
+ }
+ }
+
+ #[inline]
+ pub fn start_start_physical_corner(&self) -> PhysicalCorner {
+ PhysicalCorner::from_sides(
+ self.block_start_physical_side(),
+ self.inline_start_physical_side(),
+ )
+ }
+
+ #[inline]
+ pub fn start_end_physical_corner(&self) -> PhysicalCorner {
+ PhysicalCorner::from_sides(
+ self.block_start_physical_side(),
+ self.inline_end_physical_side(),
+ )
+ }
+
+ #[inline]
+ pub fn end_start_physical_corner(&self) -> PhysicalCorner {
+ PhysicalCorner::from_sides(
+ self.block_end_physical_side(),
+ self.inline_start_physical_side(),
+ )
+ }
+
+ #[inline]
+ pub fn end_end_physical_corner(&self) -> PhysicalCorner {
+ PhysicalCorner::from_sides(
+ self.block_end_physical_side(),
+ self.inline_end_physical_side(),
+ )
+ }
+
+ #[inline]
+ pub fn block_flow_direction(&self) -> BlockFlowDirection {
+ match (self.is_vertical(), self.is_vertical_lr()) {
+ (false, _) => BlockFlowDirection::TopToBottom,
+ (true, true) => BlockFlowDirection::LeftToRight,
+ (true, false) => BlockFlowDirection::RightToLeft,
+ }
+ }
+
+ #[inline]
+ pub fn inline_base_direction(&self) -> InlineBaseDirection {
+ if self.intersects(WritingMode::RTL) {
+ InlineBaseDirection::RightToLeft
+ } else {
+ InlineBaseDirection::LeftToRight
+ }
+ }
+
+ #[inline]
+ /// The default bidirectional embedding level for this writing mode.
+ ///
+ /// Returns bidi level 0 if the mode is LTR, or 1 otherwise.
+ pub fn to_bidi_level(&self) -> bidi::Level {
+ if self.is_bidi_ltr() {
+ bidi::Level::ltr()
+ } else {
+ bidi::Level::rtl()
+ }
+ }
+
+ #[inline]
+ /// Is the text layout vertical?
+ pub fn is_text_vertical(&self) -> bool {
+ self.is_vertical() && !self.is_sideways()
+ }
+}
+
+impl fmt::Display for WritingMode {
+ fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> {
+ if self.is_vertical() {
+ write!(formatter, "V")?;
+ if self.is_vertical_lr() {
+ write!(formatter, " LR")?;
+ } else {
+ write!(formatter, " RL")?;
+ }
+ if self.is_sideways() {
+ write!(formatter, " Sideways")?;
+ }
+ if self.intersects(WritingMode::LINE_INVERTED) {
+ write!(formatter, " Inverted")?;
+ }
+ } else {
+ write!(formatter, "H")?;
+ }
+ if self.is_bidi_ltr() {
+ write!(formatter, " LTR")
+ } else {
+ write!(formatter, " RTL")
+ }
+ }
+}
+
+/// Wherever logical geometry is used, the writing mode is known based on context:
+/// every method takes a `mode` parameter.
+/// However, this context is easy to get wrong.
+/// In debug builds only, logical geometry objects store their writing mode
+/// (in addition to taking it as a parameter to methods) and check it.
+/// In non-debug builds, make this storage zero-size and the checks no-ops.
+#[cfg(not(debug_assertions))]
+#[derive(Clone, Copy, Eq, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Serialize))]
+struct DebugWritingMode;
+
+#[cfg(debug_assertions)]
+#[derive(Clone, Copy, Eq, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Serialize))]
+struct DebugWritingMode {
+ mode: WritingMode,
+}
+
+#[cfg(not(debug_assertions))]
+impl DebugWritingMode {
+ #[inline]
+ fn check(&self, _other: WritingMode) {}
+
+ #[inline]
+ fn check_debug(&self, _other: DebugWritingMode) {}
+
+ #[inline]
+ fn new(_mode: WritingMode) -> DebugWritingMode {
+ DebugWritingMode
+ }
+}
+
+#[cfg(debug_assertions)]
+impl DebugWritingMode {
+ #[inline]
+ fn check(&self, other: WritingMode) {
+ assert_eq!(self.mode, other)
+ }
+
+ #[inline]
+ fn check_debug(&self, other: DebugWritingMode) {
+ assert_eq!(self.mode, other.mode)
+ }
+
+ #[inline]
+ fn new(mode: WritingMode) -> DebugWritingMode {
+ DebugWritingMode { mode }
+ }
+}
+
+impl Debug for DebugWritingMode {
+ #[cfg(not(debug_assertions))]
+ fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> {
+ write!(formatter, "?")
+ }
+
+ #[cfg(debug_assertions)]
+ fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> {
+ write!(formatter, "{}", self.mode)
+ }
+}
+
+// Used to specify the logical direction.
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Serialize))]
+pub enum Direction {
+ Inline,
+ Block,
+}
+
+/// A 2D size in flow-relative dimensions
+#[derive(Clone, Copy, Eq, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Serialize))]
+pub struct LogicalSize<T> {
+ pub inline: T, // inline-size, a.k.a. logical width, a.k.a. measure
+ pub block: T, // block-size, a.k.a. logical height, a.k.a. extent
+ debug_writing_mode: DebugWritingMode,
+}
+
+impl<T: Debug> Debug for LogicalSize<T> {
+ fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> {
+ write!(
+ formatter,
+ "LogicalSize({:?}, i{:?}×b{:?})",
+ self.debug_writing_mode, self.inline, self.block
+ )
+ }
+}
+
+// Can not implement the Zero trait: its zero() method does not have the `mode` parameter.
+impl<T: Zero> LogicalSize<T> {
+ #[inline]
+ pub fn zero(mode: WritingMode) -> LogicalSize<T> {
+ LogicalSize {
+ inline: Zero::zero(),
+ block: Zero::zero(),
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+}
+
+impl<T> LogicalSize<T> {
+ #[inline]
+ pub fn new(mode: WritingMode, inline: T, block: T) -> LogicalSize<T> {
+ LogicalSize {
+ inline: inline,
+ block: block,
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+
+ #[inline]
+ pub fn from_physical(mode: WritingMode, size: Size2D<T>) -> LogicalSize<T> {
+ if mode.is_vertical() {
+ LogicalSize::new(mode, size.height, size.width)
+ } else {
+ LogicalSize::new(mode, size.width, size.height)
+ }
+ }
+}
+
+impl<T: Copy> LogicalSize<T> {
+ #[inline]
+ pub fn width(&self, mode: WritingMode) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ self.block
+ } else {
+ self.inline
+ }
+ }
+
+ #[inline]
+ pub fn set_width(&mut self, mode: WritingMode, width: T) {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ self.block = width
+ } else {
+ self.inline = width
+ }
+ }
+
+ #[inline]
+ pub fn height(&self, mode: WritingMode) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ self.inline
+ } else {
+ self.block
+ }
+ }
+
+ #[inline]
+ pub fn set_height(&mut self, mode: WritingMode, height: T) {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ self.inline = height
+ } else {
+ self.block = height
+ }
+ }
+
+ #[inline]
+ pub fn to_physical(&self, mode: WritingMode) -> Size2D<T> {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ Size2D::new(self.block, self.inline)
+ } else {
+ Size2D::new(self.inline, self.block)
+ }
+ }
+
+ #[inline]
+ pub fn convert(&self, mode_from: WritingMode, mode_to: WritingMode) -> LogicalSize<T> {
+ if mode_from == mode_to {
+ self.debug_writing_mode.check(mode_from);
+ *self
+ } else {
+ LogicalSize::from_physical(mode_to, self.to_physical(mode_from))
+ }
+ }
+}
+
+impl<T: Add<T, Output = T>> Add for LogicalSize<T> {
+ type Output = LogicalSize<T>;
+
+ #[inline]
+ fn add(self, other: LogicalSize<T>) -> LogicalSize<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalSize {
+ debug_writing_mode: self.debug_writing_mode,
+ inline: self.inline + other.inline,
+ block: self.block + other.block,
+ }
+ }
+}
+
+impl<T: Sub<T, Output = T>> Sub for LogicalSize<T> {
+ type Output = LogicalSize<T>;
+
+ #[inline]
+ fn sub(self, other: LogicalSize<T>) -> LogicalSize<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalSize {
+ debug_writing_mode: self.debug_writing_mode,
+ inline: self.inline - other.inline,
+ block: self.block - other.block,
+ }
+ }
+}
+
+/// A 2D point in flow-relative dimensions
+#[derive(Clone, Copy, Eq, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Serialize))]
+pub struct LogicalPoint<T> {
+ /// inline-axis coordinate
+ pub i: T,
+ /// block-axis coordinate
+ pub b: T,
+ debug_writing_mode: DebugWritingMode,
+}
+
+impl<T: Debug> Debug for LogicalPoint<T> {
+ fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> {
+ write!(
+ formatter,
+ "LogicalPoint({:?} (i{:?}, b{:?}))",
+ self.debug_writing_mode, self.i, self.b
+ )
+ }
+}
+
+// Can not implement the Zero trait: its zero() method does not have the `mode` parameter.
+impl<T: Zero> LogicalPoint<T> {
+ #[inline]
+ pub fn zero(mode: WritingMode) -> LogicalPoint<T> {
+ LogicalPoint {
+ i: Zero::zero(),
+ b: Zero::zero(),
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+}
+
+impl<T: Copy> LogicalPoint<T> {
+ #[inline]
+ pub fn new(mode: WritingMode, i: T, b: T) -> LogicalPoint<T> {
+ LogicalPoint {
+ i: i,
+ b: b,
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+}
+
+impl<T: Copy + Sub<T, Output = T>> LogicalPoint<T> {
+ #[inline]
+ pub fn from_physical(
+ mode: WritingMode,
+ point: Point2D<T>,
+ container_size: Size2D<T>,
+ ) -> LogicalPoint<T> {
+ if mode.is_vertical() {
+ LogicalPoint {
+ i: if mode.is_inline_tb() {
+ point.y
+ } else {
+ container_size.height - point.y
+ },
+ b: if mode.is_vertical_lr() {
+ point.x
+ } else {
+ container_size.width - point.x
+ },
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ } else {
+ LogicalPoint {
+ i: if mode.is_bidi_ltr() {
+ point.x
+ } else {
+ container_size.width - point.x
+ },
+ b: point.y,
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+ }
+
+ #[inline]
+ pub fn x(&self, mode: WritingMode, container_size: Size2D<T>) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_vertical_lr() {
+ self.b
+ } else {
+ container_size.width - self.b
+ }
+ } else {
+ if mode.is_bidi_ltr() {
+ self.i
+ } else {
+ container_size.width - self.i
+ }
+ }
+ }
+
+ #[inline]
+ pub fn set_x(&mut self, mode: WritingMode, x: T, container_size: Size2D<T>) {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ self.b = if mode.is_vertical_lr() {
+ x
+ } else {
+ container_size.width - x
+ }
+ } else {
+ self.i = if mode.is_bidi_ltr() {
+ x
+ } else {
+ container_size.width - x
+ }
+ }
+ }
+
+ #[inline]
+ pub fn y(&self, mode: WritingMode, container_size: Size2D<T>) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_inline_tb() {
+ self.i
+ } else {
+ container_size.height - self.i
+ }
+ } else {
+ self.b
+ }
+ }
+
+ #[inline]
+ pub fn set_y(&mut self, mode: WritingMode, y: T, container_size: Size2D<T>) {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ self.i = if mode.is_inline_tb() {
+ y
+ } else {
+ container_size.height - y
+ }
+ } else {
+ self.b = y
+ }
+ }
+
+ #[inline]
+ pub fn to_physical(&self, mode: WritingMode, container_size: Size2D<T>) -> Point2D<T> {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ Point2D::new(
+ if mode.is_vertical_lr() {
+ self.b
+ } else {
+ container_size.width - self.b
+ },
+ if mode.is_inline_tb() {
+ self.i
+ } else {
+ container_size.height - self.i
+ },
+ )
+ } else {
+ Point2D::new(
+ if mode.is_bidi_ltr() {
+ self.i
+ } else {
+ container_size.width - self.i
+ },
+ self.b,
+ )
+ }
+ }
+
+ #[inline]
+ pub fn convert(
+ &self,
+ mode_from: WritingMode,
+ mode_to: WritingMode,
+ container_size: Size2D<T>,
+ ) -> LogicalPoint<T> {
+ if mode_from == mode_to {
+ self.debug_writing_mode.check(mode_from);
+ *self
+ } else {
+ LogicalPoint::from_physical(
+ mode_to,
+ self.to_physical(mode_from, container_size),
+ container_size,
+ )
+ }
+ }
+}
+
+impl<T: Copy + Add<T, Output = T>> LogicalPoint<T> {
+ /// This doesn’t really makes sense,
+ /// but happens when dealing with multiple origins.
+ #[inline]
+ pub fn add_point(&self, other: &LogicalPoint<T>) -> LogicalPoint<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalPoint {
+ debug_writing_mode: self.debug_writing_mode,
+ i: self.i + other.i,
+ b: self.b + other.b,
+ }
+ }
+}
+
+impl<T: Copy + Add<T, Output = T>> Add<LogicalSize<T>> for LogicalPoint<T> {
+ type Output = LogicalPoint<T>;
+
+ #[inline]
+ fn add(self, other: LogicalSize<T>) -> LogicalPoint<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalPoint {
+ debug_writing_mode: self.debug_writing_mode,
+ i: self.i + other.inline,
+ b: self.b + other.block,
+ }
+ }
+}
+
+impl<T: Copy + Sub<T, Output = T>> Sub<LogicalSize<T>> for LogicalPoint<T> {
+ type Output = LogicalPoint<T>;
+
+ #[inline]
+ fn sub(self, other: LogicalSize<T>) -> LogicalPoint<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalPoint {
+ debug_writing_mode: self.debug_writing_mode,
+ i: self.i - other.inline,
+ b: self.b - other.block,
+ }
+ }
+}
+
+/// A "margin" in flow-relative dimensions
+/// Represents the four sides of the margins, borders, or padding of a CSS box,
+/// or a combination of those.
+/// A positive "margin" can be added to a rectangle to obtain a bigger rectangle.
+#[derive(Clone, Copy, Eq, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Serialize))]
+pub struct LogicalMargin<T> {
+ pub block_start: T,
+ pub inline_end: T,
+ pub block_end: T,
+ pub inline_start: T,
+ debug_writing_mode: DebugWritingMode,
+}
+
+impl<T: Debug> Debug for LogicalMargin<T> {
+ fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> {
+ let writing_mode_string = if cfg!(debug_assertions) {
+ format!("{:?}, ", self.debug_writing_mode)
+ } else {
+ "".to_owned()
+ };
+
+ write!(
+ formatter,
+ "LogicalMargin({}i:{:?}..{:?} b:{:?}..{:?})",
+ writing_mode_string,
+ self.inline_start,
+ self.inline_end,
+ self.block_start,
+ self.block_end
+ )
+ }
+}
+
+impl<T: Zero> LogicalMargin<T> {
+ #[inline]
+ pub fn zero(mode: WritingMode) -> LogicalMargin<T> {
+ LogicalMargin {
+ block_start: Zero::zero(),
+ inline_end: Zero::zero(),
+ block_end: Zero::zero(),
+ inline_start: Zero::zero(),
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+}
+
+impl<T> LogicalMargin<T> {
+ #[inline]
+ pub fn new(
+ mode: WritingMode,
+ block_start: T,
+ inline_end: T,
+ block_end: T,
+ inline_start: T,
+ ) -> LogicalMargin<T> {
+ LogicalMargin {
+ block_start,
+ inline_end,
+ block_end,
+ inline_start,
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+
+ #[inline]
+ pub fn from_physical(mode: WritingMode, offsets: SideOffsets2D<T>) -> LogicalMargin<T> {
+ let block_start;
+ let inline_end;
+ let block_end;
+ let inline_start;
+ if mode.is_vertical() {
+ if mode.is_vertical_lr() {
+ block_start = offsets.left;
+ block_end = offsets.right;
+ } else {
+ block_start = offsets.right;
+ block_end = offsets.left;
+ }
+ if mode.is_inline_tb() {
+ inline_start = offsets.top;
+ inline_end = offsets.bottom;
+ } else {
+ inline_start = offsets.bottom;
+ inline_end = offsets.top;
+ }
+ } else {
+ block_start = offsets.top;
+ block_end = offsets.bottom;
+ if mode.is_bidi_ltr() {
+ inline_start = offsets.left;
+ inline_end = offsets.right;
+ } else {
+ inline_start = offsets.right;
+ inline_end = offsets.left;
+ }
+ }
+ LogicalMargin::new(mode, block_start, inline_end, block_end, inline_start)
+ }
+}
+
+impl<T: Copy> LogicalMargin<T> {
+ #[inline]
+ pub fn new_all_same(mode: WritingMode, value: T) -> LogicalMargin<T> {
+ LogicalMargin::new(mode, value, value, value, value)
+ }
+
+ #[inline]
+ pub fn top(&self, mode: WritingMode) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_inline_tb() {
+ self.inline_start
+ } else {
+ self.inline_end
+ }
+ } else {
+ self.block_start
+ }
+ }
+
+ #[inline]
+ pub fn set_top(&mut self, mode: WritingMode, top: T) {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_inline_tb() {
+ self.inline_start = top
+ } else {
+ self.inline_end = top
+ }
+ } else {
+ self.block_start = top
+ }
+ }
+
+ #[inline]
+ pub fn right(&self, mode: WritingMode) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_vertical_lr() {
+ self.block_end
+ } else {
+ self.block_start
+ }
+ } else {
+ if mode.is_bidi_ltr() {
+ self.inline_end
+ } else {
+ self.inline_start
+ }
+ }
+ }
+
+ #[inline]
+ pub fn set_right(&mut self, mode: WritingMode, right: T) {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_vertical_lr() {
+ self.block_end = right
+ } else {
+ self.block_start = right
+ }
+ } else {
+ if mode.is_bidi_ltr() {
+ self.inline_end = right
+ } else {
+ self.inline_start = right
+ }
+ }
+ }
+
+ #[inline]
+ pub fn bottom(&self, mode: WritingMode) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_inline_tb() {
+ self.inline_end
+ } else {
+ self.inline_start
+ }
+ } else {
+ self.block_end
+ }
+ }
+
+ #[inline]
+ pub fn set_bottom(&mut self, mode: WritingMode, bottom: T) {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_inline_tb() {
+ self.inline_end = bottom
+ } else {
+ self.inline_start = bottom
+ }
+ } else {
+ self.block_end = bottom
+ }
+ }
+
+ #[inline]
+ pub fn left(&self, mode: WritingMode) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_vertical_lr() {
+ self.block_start
+ } else {
+ self.block_end
+ }
+ } else {
+ if mode.is_bidi_ltr() {
+ self.inline_start
+ } else {
+ self.inline_end
+ }
+ }
+ }
+
+ #[inline]
+ pub fn set_left(&mut self, mode: WritingMode, left: T) {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ if mode.is_vertical_lr() {
+ self.block_start = left
+ } else {
+ self.block_end = left
+ }
+ } else {
+ if mode.is_bidi_ltr() {
+ self.inline_start = left
+ } else {
+ self.inline_end = left
+ }
+ }
+ }
+
+ #[inline]
+ pub fn to_physical(&self, mode: WritingMode) -> SideOffsets2D<T> {
+ self.debug_writing_mode.check(mode);
+ let top;
+ let right;
+ let bottom;
+ let left;
+ if mode.is_vertical() {
+ if mode.is_vertical_lr() {
+ left = self.block_start;
+ right = self.block_end;
+ } else {
+ right = self.block_start;
+ left = self.block_end;
+ }
+ if mode.is_inline_tb() {
+ top = self.inline_start;
+ bottom = self.inline_end;
+ } else {
+ bottom = self.inline_start;
+ top = self.inline_end;
+ }
+ } else {
+ top = self.block_start;
+ bottom = self.block_end;
+ if mode.is_bidi_ltr() {
+ left = self.inline_start;
+ right = self.inline_end;
+ } else {
+ right = self.inline_start;
+ left = self.inline_end;
+ }
+ }
+ SideOffsets2D::new(top, right, bottom, left)
+ }
+
+ #[inline]
+ pub fn convert(&self, mode_from: WritingMode, mode_to: WritingMode) -> LogicalMargin<T> {
+ if mode_from == mode_to {
+ self.debug_writing_mode.check(mode_from);
+ *self
+ } else {
+ LogicalMargin::from_physical(mode_to, self.to_physical(mode_from))
+ }
+ }
+}
+
+impl<T: PartialEq + Zero> LogicalMargin<T> {
+ #[inline]
+ pub fn is_zero(&self) -> bool {
+ self.block_start == Zero::zero() &&
+ self.inline_end == Zero::zero() &&
+ self.block_end == Zero::zero() &&
+ self.inline_start == Zero::zero()
+ }
+}
+
+impl<T: Copy + Add<T, Output = T>> LogicalMargin<T> {
+ #[inline]
+ pub fn inline_start_end(&self) -> T {
+ self.inline_start + self.inline_end
+ }
+
+ #[inline]
+ pub fn block_start_end(&self) -> T {
+ self.block_start + self.block_end
+ }
+
+ #[inline]
+ pub fn start_end(&self, direction: Direction) -> T {
+ match direction {
+ Direction::Inline => self.inline_start + self.inline_end,
+ Direction::Block => self.block_start + self.block_end,
+ }
+ }
+
+ #[inline]
+ pub fn top_bottom(&self, mode: WritingMode) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ self.inline_start_end()
+ } else {
+ self.block_start_end()
+ }
+ }
+
+ #[inline]
+ pub fn left_right(&self, mode: WritingMode) -> T {
+ self.debug_writing_mode.check(mode);
+ if mode.is_vertical() {
+ self.block_start_end()
+ } else {
+ self.inline_start_end()
+ }
+ }
+}
+
+impl<T: Add<T, Output = T>> Add for LogicalMargin<T> {
+ type Output = LogicalMargin<T>;
+
+ #[inline]
+ fn add(self, other: LogicalMargin<T>) -> LogicalMargin<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalMargin {
+ debug_writing_mode: self.debug_writing_mode,
+ block_start: self.block_start + other.block_start,
+ inline_end: self.inline_end + other.inline_end,
+ block_end: self.block_end + other.block_end,
+ inline_start: self.inline_start + other.inline_start,
+ }
+ }
+}
+
+impl<T: Sub<T, Output = T>> Sub for LogicalMargin<T> {
+ type Output = LogicalMargin<T>;
+
+ #[inline]
+ fn sub(self, other: LogicalMargin<T>) -> LogicalMargin<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalMargin {
+ debug_writing_mode: self.debug_writing_mode,
+ block_start: self.block_start - other.block_start,
+ inline_end: self.inline_end - other.inline_end,
+ block_end: self.block_end - other.block_end,
+ inline_start: self.inline_start - other.inline_start,
+ }
+ }
+}
+
+/// A rectangle in flow-relative dimensions
+#[derive(Clone, Copy, Eq, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Serialize))]
+pub struct LogicalRect<T> {
+ pub start: LogicalPoint<T>,
+ pub size: LogicalSize<T>,
+ debug_writing_mode: DebugWritingMode,
+}
+
+impl<T: Debug> Debug for LogicalRect<T> {
+ fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> {
+ let writing_mode_string = if cfg!(debug_assertions) {
+ format!("{:?}, ", self.debug_writing_mode)
+ } else {
+ "".to_owned()
+ };
+
+ write!(
+ formatter,
+ "LogicalRect({}i{:?}×b{:?}, @ (i{:?},b{:?}))",
+ writing_mode_string, self.size.inline, self.size.block, self.start.i, self.start.b
+ )
+ }
+}
+
+impl<T: Zero> LogicalRect<T> {
+ #[inline]
+ pub fn zero(mode: WritingMode) -> LogicalRect<T> {
+ LogicalRect {
+ start: LogicalPoint::zero(mode),
+ size: LogicalSize::zero(mode),
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+}
+
+impl<T: Copy> LogicalRect<T> {
+ #[inline]
+ pub fn new(
+ mode: WritingMode,
+ inline_start: T,
+ block_start: T,
+ inline: T,
+ block: T,
+ ) -> LogicalRect<T> {
+ LogicalRect {
+ start: LogicalPoint::new(mode, inline_start, block_start),
+ size: LogicalSize::new(mode, inline, block),
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+
+ #[inline]
+ pub fn from_point_size(
+ mode: WritingMode,
+ start: LogicalPoint<T>,
+ size: LogicalSize<T>,
+ ) -> LogicalRect<T> {
+ start.debug_writing_mode.check(mode);
+ size.debug_writing_mode.check(mode);
+ LogicalRect {
+ start: start,
+ size: size,
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+}
+
+impl<T: Copy + Add<T, Output = T> + Sub<T, Output = T>> LogicalRect<T> {
+ #[inline]
+ pub fn from_physical(
+ mode: WritingMode,
+ rect: Rect<T>,
+ container_size: Size2D<T>,
+ ) -> LogicalRect<T> {
+ let inline_start;
+ let block_start;
+ let inline;
+ let block;
+ if mode.is_vertical() {
+ inline = rect.size.height;
+ block = rect.size.width;
+ if mode.is_vertical_lr() {
+ block_start = rect.origin.x;
+ } else {
+ block_start = container_size.width - (rect.origin.x + rect.size.width);
+ }
+ if mode.is_inline_tb() {
+ inline_start = rect.origin.y;
+ } else {
+ inline_start = container_size.height - (rect.origin.y + rect.size.height);
+ }
+ } else {
+ inline = rect.size.width;
+ block = rect.size.height;
+ block_start = rect.origin.y;
+ if mode.is_bidi_ltr() {
+ inline_start = rect.origin.x;
+ } else {
+ inline_start = container_size.width - (rect.origin.x + rect.size.width);
+ }
+ }
+ LogicalRect {
+ start: LogicalPoint::new(mode, inline_start, block_start),
+ size: LogicalSize::new(mode, inline, block),
+ debug_writing_mode: DebugWritingMode::new(mode),
+ }
+ }
+
+ #[inline]
+ pub fn inline_end(&self) -> T {
+ self.start.i + self.size.inline
+ }
+
+ #[inline]
+ pub fn block_end(&self) -> T {
+ self.start.b + self.size.block
+ }
+
+ #[inline]
+ pub fn to_physical(&self, mode: WritingMode, container_size: Size2D<T>) -> Rect<T> {
+ self.debug_writing_mode.check(mode);
+ let x;
+ let y;
+ let width;
+ let height;
+ if mode.is_vertical() {
+ width = self.size.block;
+ height = self.size.inline;
+ if mode.is_vertical_lr() {
+ x = self.start.b;
+ } else {
+ x = container_size.width - self.block_end();
+ }
+ if mode.is_inline_tb() {
+ y = self.start.i;
+ } else {
+ y = container_size.height - self.inline_end();
+ }
+ } else {
+ width = self.size.inline;
+ height = self.size.block;
+ y = self.start.b;
+ if mode.is_bidi_ltr() {
+ x = self.start.i;
+ } else {
+ x = container_size.width - self.inline_end();
+ }
+ }
+ Rect {
+ origin: Point2D::new(x, y),
+ size: Size2D::new(width, height),
+ }
+ }
+
+ #[inline]
+ pub fn convert(
+ &self,
+ mode_from: WritingMode,
+ mode_to: WritingMode,
+ container_size: Size2D<T>,
+ ) -> LogicalRect<T> {
+ if mode_from == mode_to {
+ self.debug_writing_mode.check(mode_from);
+ *self
+ } else {
+ LogicalRect::from_physical(
+ mode_to,
+ self.to_physical(mode_from, container_size),
+ container_size,
+ )
+ }
+ }
+
+ pub fn translate_by_size(&self, offset: LogicalSize<T>) -> LogicalRect<T> {
+ LogicalRect {
+ start: self.start + offset,
+ ..*self
+ }
+ }
+
+ pub fn translate(&self, offset: &LogicalPoint<T>) -> LogicalRect<T> {
+ LogicalRect {
+ start: self.start +
+ LogicalSize {
+ inline: offset.i,
+ block: offset.b,
+ debug_writing_mode: offset.debug_writing_mode,
+ },
+ size: self.size,
+ debug_writing_mode: self.debug_writing_mode,
+ }
+ }
+}
+
+impl<T: Copy + Ord + Add<T, Output = T> + Sub<T, Output = T>> LogicalRect<T> {
+ #[inline]
+ pub fn union(&self, other: &LogicalRect<T>) -> LogicalRect<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+
+ let inline_start = min(self.start.i, other.start.i);
+ let block_start = min(self.start.b, other.start.b);
+ LogicalRect {
+ start: LogicalPoint {
+ i: inline_start,
+ b: block_start,
+ debug_writing_mode: self.debug_writing_mode,
+ },
+ size: LogicalSize {
+ inline: max(self.inline_end(), other.inline_end()) - inline_start,
+ block: max(self.block_end(), other.block_end()) - block_start,
+ debug_writing_mode: self.debug_writing_mode,
+ },
+ debug_writing_mode: self.debug_writing_mode,
+ }
+ }
+}
+
+impl<T: Copy + Add<T, Output = T> + Sub<T, Output = T>> Add<LogicalMargin<T>> for LogicalRect<T> {
+ type Output = LogicalRect<T>;
+
+ #[inline]
+ fn add(self, other: LogicalMargin<T>) -> LogicalRect<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalRect {
+ start: LogicalPoint {
+ // Growing a rectangle on the start side means pushing its
+ // start point on the negative direction.
+ i: self.start.i - other.inline_start,
+ b: self.start.b - other.block_start,
+ debug_writing_mode: self.debug_writing_mode,
+ },
+ size: LogicalSize {
+ inline: self.size.inline + other.inline_start_end(),
+ block: self.size.block + other.block_start_end(),
+ debug_writing_mode: self.debug_writing_mode,
+ },
+ debug_writing_mode: self.debug_writing_mode,
+ }
+ }
+}
+
+impl<T: Copy + Add<T, Output = T> + Sub<T, Output = T>> Sub<LogicalMargin<T>> for LogicalRect<T> {
+ type Output = LogicalRect<T>;
+
+ #[inline]
+ fn sub(self, other: LogicalMargin<T>) -> LogicalRect<T> {
+ self.debug_writing_mode
+ .check_debug(other.debug_writing_mode);
+ LogicalRect {
+ start: LogicalPoint {
+ // Shrinking a rectangle on the start side means pushing its
+ // start point on the positive direction.
+ i: self.start.i + other.inline_start,
+ b: self.start.b + other.block_start,
+ debug_writing_mode: self.debug_writing_mode,
+ },
+ size: LogicalSize {
+ inline: self.size.inline - other.inline_start_end(),
+ block: self.size.block - other.block_start_end(),
+ debug_writing_mode: self.debug_writing_mode,
+ },
+ debug_writing_mode: self.debug_writing_mode,
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[repr(u8)]
+pub enum LogicalAxis {
+ Block = 0,
+ Inline,
+}
+
+impl LogicalAxis {
+ #[inline]
+ pub fn to_physical(self, wm: WritingMode) -> PhysicalAxis {
+ if wm.is_horizontal() == (self == Self::Inline) {
+ PhysicalAxis::Horizontal
+ } else {
+ PhysicalAxis::Vertical
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[repr(u8)]
+pub enum LogicalSide {
+ BlockStart = 0,
+ BlockEnd,
+ InlineStart,
+ InlineEnd,
+}
+
+impl LogicalSide {
+ fn is_block(self) -> bool {
+ matches!(self, Self::BlockStart | Self::BlockEnd)
+ }
+
+ #[inline]
+ pub fn to_physical(self, wm: WritingMode) -> PhysicalSide {
+ // Block mapping depends only on vertical+vertical-lr
+ static BLOCK_MAPPING: [[PhysicalSide; 2]; 4] = [
+ [PhysicalSide::Top, PhysicalSide::Bottom], // horizontal-tb
+ [PhysicalSide::Right, PhysicalSide::Left], // vertical-rl
+ [PhysicalSide::Bottom, PhysicalSide::Top], // (horizontal-bt)
+ [PhysicalSide::Left, PhysicalSide::Right], // vertical-lr
+ ];
+
+ if self.is_block() {
+ let vertical = wm.is_vertical();
+ let lr = wm.is_vertical_lr();
+ let index = (vertical as usize) | ((lr as usize) << 1);
+ return BLOCK_MAPPING[index][self as usize];
+ }
+
+ // start = 0, end = 1
+ let edge = self as usize - 2;
+ // Inline axis sides depend on all three of writing-mode, text-orientation and direction,
+ // which are encoded in the VERTICAL, INLINE_REVERSED, VERTICAL_LR and LINE_INVERTED bits.
+ //
+ // bit 0 = the VERTICAL value
+ // bit 1 = the INLINE_REVERSED value
+ // bit 2 = the VERTICAL_LR value
+ // bit 3 = the LINE_INVERTED value
+ //
+ // Note that not all of these combinations can actually be specified via CSS: there is no
+ // horizontal-bt writing-mode, and no text-orientation value that produces "inverted"
+ // text. (The former 'sideways-left' value, no longer in the spec, would have produced
+ // this in vertical-rl mode.)
+ static INLINE_MAPPING: [[PhysicalSide; 2]; 16] = [
+ [PhysicalSide::Left, PhysicalSide::Right], // horizontal-tb ltr
+ [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-rl ltr
+ [PhysicalSide::Right, PhysicalSide::Left], // horizontal-tb rtl
+ [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-rl rtl
+ [PhysicalSide::Right, PhysicalSide::Left], // (horizontal-bt) (inverted) ltr
+ [PhysicalSide::Top, PhysicalSide::Bottom], // sideways-lr rtl
+ [PhysicalSide::Left, PhysicalSide::Right], // (horizontal-bt) (inverted) rtl
+ [PhysicalSide::Bottom, PhysicalSide::Top], // sideways-lr ltr
+ [PhysicalSide::Left, PhysicalSide::Right], // horizontal-tb (inverted) rtl
+ [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-rl (inverted) rtl
+ [PhysicalSide::Right, PhysicalSide::Left], // horizontal-tb (inverted) ltr
+ [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-rl (inverted) ltr
+ [PhysicalSide::Left, PhysicalSide::Right], // (horizontal-bt) ltr
+ [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-lr ltr
+ [PhysicalSide::Right, PhysicalSide::Left], // (horizontal-bt) rtl
+ [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-lr rtl
+ ];
+
+ debug_assert!(
+ WritingMode::VERTICAL.bits() == 0x01 &&
+ WritingMode::INLINE_REVERSED.bits() == 0x02 &&
+ WritingMode::VERTICAL_LR.bits() == 0x04 &&
+ WritingMode::LINE_INVERTED.bits() == 0x08
+ );
+ let index = (wm.bits() & 0xF) as usize;
+ INLINE_MAPPING[index][edge]
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[repr(u8)]
+pub enum LogicalCorner {
+ StartStart = 0,
+ StartEnd,
+ EndStart,
+ EndEnd,
+}
+
+impl LogicalCorner {
+ #[inline]
+ pub fn to_physical(self, wm: WritingMode) -> PhysicalCorner {
+ static CORNER_TO_SIDES: [[LogicalSide; 2]; 4] = [
+ [LogicalSide::BlockStart, LogicalSide::InlineStart],
+ [LogicalSide::BlockStart, LogicalSide::InlineEnd],
+ [LogicalSide::BlockEnd, LogicalSide::InlineStart],
+ [LogicalSide::BlockEnd, LogicalSide::InlineEnd],
+ ];
+
+ let [block, inline] = CORNER_TO_SIDES[self as usize];
+ let block = block.to_physical(wm);
+ let inline = inline.to_physical(wm);
+ PhysicalCorner::from_sides(block, inline)
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[repr(u8)]
+pub enum PhysicalAxis {
+ Vertical = 0,
+ Horizontal,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[repr(u8)]
+pub enum PhysicalSide {
+ Top = 0,
+ Right,
+ Bottom,
+ Left,
+}
+
+impl PhysicalSide {
+ fn orthogonal_to(self, other: Self) -> bool {
+ matches!(self, Self::Top | Self::Bottom) != matches!(other, Self::Top | Self::Bottom)
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[repr(u8)]
+pub enum PhysicalCorner {
+ TopLeft = 0,
+ TopRight,
+ BottomRight,
+ BottomLeft,
+}
+
+impl PhysicalCorner {
+ fn from_sides(a: PhysicalSide, b: PhysicalSide) -> Self {
+ debug_assert!(a.orthogonal_to(b), "Sides should be orthogonal");
+ // Only some of these are possible, since we expect only orthogonal values. If the two
+ // sides were to be parallel, we fall back to returning TopLeft.
+ const IMPOSSIBLE: PhysicalCorner = PhysicalCorner::TopLeft;
+ static SIDES_TO_CORNER: [[PhysicalCorner; 4]; 4] = [
+ [
+ IMPOSSIBLE,
+ PhysicalCorner::TopRight,
+ IMPOSSIBLE,
+ PhysicalCorner::TopLeft,
+ ],
+ [
+ PhysicalCorner::TopRight,
+ IMPOSSIBLE,
+ PhysicalCorner::BottomRight,
+ IMPOSSIBLE,
+ ],
+ [
+ IMPOSSIBLE,
+ PhysicalCorner::BottomRight,
+ IMPOSSIBLE,
+ PhysicalCorner::BottomLeft,
+ ],
+ [
+ PhysicalCorner::TopLeft,
+ IMPOSSIBLE,
+ PhysicalCorner::BottomLeft,
+ IMPOSSIBLE,
+ ],
+ ];
+ SIDES_TO_CORNER[a as usize][b as usize]
+ }
+}
diff --git a/servo/components/style/macros.rs b/servo/components/style/macros.rs
new file mode 100644
index 0000000000..4795345185
--- /dev/null
+++ b/servo/components/style/macros.rs
@@ -0,0 +1,98 @@
+/* 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/. */
+
+//! Various macro helpers.
+
+macro_rules! exclusive_value {
+ (($value:ident, $set:expr) => $ident:path) => {
+ if $value.intersects($set) {
+ return Err(());
+ } else {
+ $ident
+ }
+ };
+}
+
+#[cfg(feature = "gecko")]
+macro_rules! impl_gecko_keyword_conversions {
+ ($name:ident, $utype:ty) => {
+ impl From<$utype> for $name {
+ fn from(bits: $utype) -> $name {
+ $name::from_gecko_keyword(bits)
+ }
+ }
+
+ impl From<$name> for $utype {
+ fn from(v: $name) -> $utype {
+ v.to_gecko_keyword()
+ }
+ }
+ };
+}
+
+macro_rules! trivial_to_computed_value {
+ ($name:ty) => {
+ impl $crate::values::computed::ToComputedValue for $name {
+ type ComputedValue = $name;
+
+ fn to_computed_value(&self, _: &$crate::values::computed::Context) -> Self {
+ self.clone()
+ }
+
+ fn from_computed_value(other: &Self) -> Self {
+ other.clone()
+ }
+ }
+ };
+}
+
+/// A macro to parse an identifier, or return an `UnexpectedIdent` error
+/// otherwise.
+///
+/// FIXME(emilio): The fact that `UnexpectedIdent` is a `SelectorParseError`
+/// doesn't make a lot of sense to me.
+macro_rules! try_match_ident_ignore_ascii_case {
+ ($input:expr, $( $match_body:tt )*) => {{
+ let location = $input.current_source_location();
+ let ident = $input.expect_ident()?;
+ match_ignore_ascii_case! { &ident,
+ $( $match_body )*
+ _ => return Err(location.new_custom_error(
+ ::selectors::parser::SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+ ))
+ }
+ }}
+}
+
+#[cfg(feature = "servo")]
+macro_rules! local_name {
+ ($s:tt) => {
+ $crate::values::GenericAtomIdent(html5ever::local_name!($s))
+ };
+}
+
+#[cfg(feature = "servo")]
+macro_rules! ns {
+ () => {
+ $crate::values::GenericAtomIdent(html5ever::ns!())
+ };
+ ($s:tt) => {
+ $crate::values::GenericAtomIdent(html5ever::ns!($s))
+ };
+}
+
+#[cfg(feature = "gecko")]
+macro_rules! local_name {
+ ($s:tt) => {
+ $crate::values::AtomIdent(atom!($s))
+ };
+}
+
+/// Asserts the size of a type at compile time.
+macro_rules! size_of_test {
+ ($t: ty, $expected_size: expr) => {
+ #[cfg(target_pointer_width = "64")]
+ const_assert_eq!(std::mem::size_of::<$t>(), $expected_size);
+ };
+}
diff --git a/servo/components/style/matching.rs b/servo/components/style/matching.rs
new file mode 100644
index 0000000000..646d08a9e3
--- /dev/null
+++ b/servo/components/style/matching.rs
@@ -0,0 +1,1128 @@
+/* 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/. */
+
+//! High-level interface to CSS selector matching.
+
+#![allow(unsafe_code)]
+#![deny(missing_docs)]
+
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::context::{CascadeInputs, ElementCascadeInputs, QuirksMode};
+use crate::context::{SharedStyleContext, StyleContext};
+use crate::data::{ElementData, ElementStyles};
+use crate::dom::TElement;
+#[cfg(feature = "servo")]
+use crate::dom::TNode;
+use crate::invalidation::element::restyle_hints::RestyleHint;
+use crate::properties::longhands::display::computed_value::T as Display;
+use crate::properties::ComputedValues;
+use crate::properties::PropertyDeclarationBlock;
+use crate::rule_tree::{CascadeLevel, StrongRuleNode};
+use crate::selector_parser::{PseudoElement, RestyleDamage};
+use crate::shared_lock::Locked;
+use crate::style_resolver::ResolvedElementStyles;
+use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement};
+use crate::stylesheets::layer_rule::LayerOrder;
+use crate::stylist::RuleInclusion;
+use crate::traversal_flags::TraversalFlags;
+use servo_arc::{Arc, ArcBorrow};
+
+/// Represents the result of comparing an element's old and new style.
+#[derive(Debug)]
+pub struct StyleDifference {
+ /// The resulting damage.
+ pub damage: RestyleDamage,
+
+ /// Whether any styles changed.
+ pub change: StyleChange,
+}
+
+/// Represents whether or not the style of an element has changed.
+#[derive(Clone, Copy, Debug)]
+pub enum StyleChange {
+ /// The style hasn't changed.
+ Unchanged,
+ /// The style has changed.
+ Changed {
+ /// Whether only reset structs changed.
+ reset_only: bool,
+ },
+}
+
+/// Whether or not newly computed values for an element need to be cascaded to
+/// children (or children might need to be re-matched, e.g., for container
+/// queries).
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+pub enum ChildRestyleRequirement {
+ /// Old and new computed values were the same, or we otherwise know that
+ /// we won't bother recomputing style for children, so we can skip cascading
+ /// the new values into child elements.
+ CanSkipCascade = 0,
+ /// The same as `MustCascadeChildren`, but we only need to actually
+ /// recascade if the child inherits any explicit reset style.
+ MustCascadeChildrenIfInheritResetStyle = 1,
+ /// Old and new computed values were different, so we must cascade the
+ /// new values to children.
+ MustCascadeChildren = 2,
+ /// The same as `MustCascadeChildren`, but for the entire subtree. This is
+ /// used to handle root font-size updates needing to recascade the whole
+ /// document.
+ MustCascadeDescendants = 3,
+ /// We need to re-match the whole subttree. This is used to handle container
+ /// query relative unit changes for example. Container query size changes
+ /// also trigger re-match, but after layout.
+ MustMatchDescendants = 4,
+}
+
+/// Determines which styles are being cascaded currently.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum CascadeVisitedMode {
+ /// Cascade the regular, unvisited styles.
+ Unvisited,
+ /// Cascade the styles used when an element's relevant link is visited. A
+ /// "relevant link" is the element being matched if it is a link or the
+ /// nearest ancestor link.
+ Visited,
+}
+
+trait PrivateMatchMethods: TElement {
+ fn replace_single_rule_node(
+ context: &SharedStyleContext,
+ level: CascadeLevel,
+ layer_order: LayerOrder,
+ pdb: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>,
+ path: &mut StrongRuleNode,
+ ) -> bool {
+ let stylist = &context.stylist;
+ let guards = &context.guards;
+
+ let mut important_rules_changed = false;
+ let new_node = stylist.rule_tree().update_rule_at_level(
+ level,
+ layer_order,
+ pdb,
+ path,
+ guards,
+ &mut important_rules_changed,
+ );
+ if let Some(n) = new_node {
+ *path = n;
+ }
+ important_rules_changed
+ }
+
+ /// Updates the rule nodes without re-running selector matching, using just
+ /// the rule tree, for a specific visited mode.
+ ///
+ /// Returns true if an !important rule was replaced.
+ fn replace_rules_internal(
+ &self,
+ replacements: RestyleHint,
+ context: &mut StyleContext<Self>,
+ cascade_visited: CascadeVisitedMode,
+ cascade_inputs: &mut ElementCascadeInputs,
+ ) -> bool {
+ debug_assert!(
+ replacements.intersects(RestyleHint::replacements()) &&
+ (replacements & !RestyleHint::replacements()).is_empty()
+ );
+
+ let primary_rules = match cascade_visited {
+ CascadeVisitedMode::Unvisited => cascade_inputs.primary.rules.as_mut(),
+ CascadeVisitedMode::Visited => cascade_inputs.primary.visited_rules.as_mut(),
+ };
+
+ let primary_rules = match primary_rules {
+ Some(r) => r,
+ None => return false,
+ };
+
+ if !context.shared.traversal_flags.for_animation_only() {
+ let mut result = false;
+ if replacements.contains(RestyleHint::RESTYLE_STYLE_ATTRIBUTE) {
+ let style_attribute = self.style_attribute();
+ result |= Self::replace_single_rule_node(
+ context.shared,
+ CascadeLevel::same_tree_author_normal(),
+ LayerOrder::root(),
+ style_attribute,
+ primary_rules,
+ );
+ result |= Self::replace_single_rule_node(
+ context.shared,
+ CascadeLevel::same_tree_author_important(),
+ LayerOrder::root(),
+ style_attribute,
+ primary_rules,
+ );
+ // FIXME(emilio): Still a hack!
+ self.unset_dirty_style_attribute();
+ }
+ return result;
+ }
+
+ // Animation restyle hints are processed prior to other restyle
+ // hints in the animation-only traversal.
+ //
+ // Non-animation restyle hints will be processed in a subsequent
+ // normal traversal.
+ if replacements.intersects(RestyleHint::for_animations()) {
+ debug_assert!(context.shared.traversal_flags.for_animation_only());
+
+ if replacements.contains(RestyleHint::RESTYLE_SMIL) {
+ Self::replace_single_rule_node(
+ context.shared,
+ CascadeLevel::SMILOverride,
+ LayerOrder::root(),
+ self.smil_override(),
+ primary_rules,
+ );
+ }
+
+ if replacements.contains(RestyleHint::RESTYLE_CSS_TRANSITIONS) {
+ Self::replace_single_rule_node(
+ context.shared,
+ CascadeLevel::Transitions,
+ LayerOrder::root(),
+ self.transition_rule(&context.shared)
+ .as_ref()
+ .map(|a| a.borrow_arc()),
+ primary_rules,
+ );
+ }
+
+ if replacements.contains(RestyleHint::RESTYLE_CSS_ANIMATIONS) {
+ Self::replace_single_rule_node(
+ context.shared,
+ CascadeLevel::Animations,
+ LayerOrder::root(),
+ self.animation_rule(&context.shared)
+ .as_ref()
+ .map(|a| a.borrow_arc()),
+ primary_rules,
+ );
+ }
+ }
+
+ false
+ }
+
+ /// If there is no transition rule in the ComputedValues, it returns None.
+ fn after_change_style(
+ &self,
+ context: &mut StyleContext<Self>,
+ primary_style: &Arc<ComputedValues>,
+ ) -> Option<Arc<ComputedValues>> {
+ let rule_node = primary_style.rules();
+ let without_transition_rules = context
+ .shared
+ .stylist
+ .rule_tree()
+ .remove_transition_rule_if_applicable(rule_node);
+ if without_transition_rules == *rule_node {
+ // We don't have transition rule in this case, so return None to let
+ // the caller use the original ComputedValues.
+ return None;
+ }
+
+ // FIXME(bug 868975): We probably need to transition visited style as
+ // well.
+ let inputs = CascadeInputs {
+ rules: Some(without_transition_rules),
+ visited_rules: primary_style.visited_rules().cloned(),
+ flags: primary_style.flags.for_cascade_inputs(),
+ };
+
+ // Actually `PseudoElementResolution` doesn't really matter.
+ let style = StyleResolverForElement::new(
+ *self,
+ context,
+ RuleInclusion::All,
+ PseudoElementResolution::IfApplicable,
+ )
+ .cascade_style_and_visited_with_default_parents(inputs);
+
+ Some(style.0)
+ }
+
+ fn needs_animations_update(
+ &self,
+ context: &mut StyleContext<Self>,
+ old_style: Option<&ComputedValues>,
+ new_style: &ComputedValues,
+ pseudo_element: Option<PseudoElement>,
+ ) -> bool {
+ let new_ui_style = new_style.get_ui();
+ let new_style_specifies_animations = new_ui_style.specifies_animations();
+
+ let has_animations = self.has_css_animations(&context.shared, pseudo_element);
+ if !new_style_specifies_animations && !has_animations {
+ return false;
+ }
+
+ let old_style = match old_style {
+ Some(old) => old,
+ // If we have no old style but have animations, we may be a
+ // pseudo-element which was re-created without style changes.
+ //
+ // This can happen when we reframe the pseudo-element without
+ // restyling it (due to content insertion on a flex container or
+ // such, for example). See bug 1564366.
+ //
+ // FIXME(emilio): The really right fix for this is keeping the
+ // pseudo-element itself around on reframes, but that's a bit
+ // harder. If we do that we can probably remove quite a lot of the
+ // EffectSet complexity though, since right now it's stored on the
+ // parent element for pseudo-elements given we need to keep it
+ // around...
+ None => {
+ return new_style_specifies_animations || new_style.is_pseudo_style();
+ },
+ };
+
+ let old_ui_style = old_style.get_ui();
+
+ let keyframes_could_have_changed = context
+ .shared
+ .traversal_flags
+ .contains(TraversalFlags::ForCSSRuleChanges);
+
+ // If the traversal is triggered due to changes in CSS rules changes, we
+ // need to try to update all CSS animations on the element if the
+ // element has or will have CSS animation style regardless of whether
+ // the animation is running or not.
+ //
+ // TODO: We should check which @keyframes were added/changed/deleted and
+ // update only animations corresponding to those @keyframes.
+ if keyframes_could_have_changed {
+ return true;
+ }
+
+ // If the animations changed, well...
+ if !old_ui_style.animations_equals(new_ui_style) {
+ return true;
+ }
+
+ let old_display = old_style.clone_display();
+ let new_display = new_style.clone_display();
+
+ // If we were display: none, we may need to trigger animations.
+ if old_display == Display::None && new_display != Display::None {
+ return new_style_specifies_animations;
+ }
+
+ // If we are becoming display: none, we may need to stop animations.
+ if old_display != Display::None && new_display == Display::None {
+ return has_animations;
+ }
+
+ // We might need to update animations if writing-mode or direction
+ // changed, and any of the animations contained logical properties.
+ //
+ // We may want to be more granular, but it's probably not worth it.
+ if new_style.writing_mode != old_style.writing_mode {
+ return has_animations;
+ }
+
+ false
+ }
+
+ fn might_need_transitions_update(
+ &self,
+ context: &StyleContext<Self>,
+ old_style: Option<&ComputedValues>,
+ new_style: &ComputedValues,
+ pseudo_element: Option<PseudoElement>,
+ ) -> bool {
+ let old_style = match old_style {
+ Some(v) => v,
+ None => return false,
+ };
+
+ if !self.has_css_transitions(context.shared, pseudo_element) &&
+ !new_style.get_ui().specifies_transitions()
+ {
+ return false;
+ }
+
+ if old_style.clone_display().is_none() {
+ return false;
+ }
+
+ return true;
+ }
+
+ /// Create a SequentialTask for resolving descendants in a SMIL display
+ /// property animation if the display property changed from none.
+ #[cfg(feature = "gecko")]
+ fn handle_display_change_for_smil_if_needed(
+ &self,
+ context: &mut StyleContext<Self>,
+ old_values: Option<&ComputedValues>,
+ new_values: &ComputedValues,
+ restyle_hints: RestyleHint,
+ ) {
+ use crate::context::PostAnimationTasks;
+
+ if !restyle_hints.intersects(RestyleHint::RESTYLE_SMIL) {
+ return;
+ }
+
+ if new_values.is_display_property_changed_from_none(old_values) {
+ // When display value is changed from none to other, we need to
+ // traverse descendant elements in a subsequent normal
+ // traversal (we can't traverse them in this animation-only restyle
+ // since we have no way to know whether the decendants
+ // need to be traversed at the beginning of the animation-only
+ // restyle).
+ let task = crate::context::SequentialTask::process_post_animation(
+ *self,
+ PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL,
+ );
+ context.thread_local.tasks.push(task);
+ }
+ }
+
+ #[cfg(feature = "gecko")]
+ fn process_animations(
+ &self,
+ context: &mut StyleContext<Self>,
+ old_styles: &mut ElementStyles,
+ new_styles: &mut ResolvedElementStyles,
+ restyle_hint: RestyleHint,
+ important_rules_changed: bool,
+ ) {
+ use crate::context::UpdateAnimationsTasks;
+
+ let new_values = new_styles.primary_style_mut();
+ let old_values = &old_styles.primary;
+ if context.shared.traversal_flags.for_animation_only() {
+ self.handle_display_change_for_smil_if_needed(
+ context,
+ old_values.as_deref(),
+ new_values,
+ restyle_hint,
+ );
+ return;
+ }
+
+ // Bug 868975: These steps should examine and update the visited styles
+ // in addition to the unvisited styles.
+
+ let mut tasks = UpdateAnimationsTasks::empty();
+
+ if old_values.as_deref().map_or_else(
+ || new_values.get_ui().specifies_scroll_timelines(),
+ |old| !old.get_ui().scroll_timelines_equals(new_values.get_ui()),
+ ) {
+ tasks.insert(UpdateAnimationsTasks::SCROLL_TIMELINES);
+ }
+
+ if old_values.as_deref().map_or_else(
+ || new_values.get_ui().specifies_view_timelines(),
+ |old| !old.get_ui().view_timelines_equals(new_values.get_ui()),
+ ) {
+ tasks.insert(UpdateAnimationsTasks::VIEW_TIMELINES);
+ }
+
+ if self.needs_animations_update(
+ context,
+ old_values.as_deref(),
+ new_values,
+ /* pseudo_element = */ None,
+ ) {
+ tasks.insert(UpdateAnimationsTasks::CSS_ANIMATIONS);
+ }
+
+ let before_change_style = if self.might_need_transitions_update(
+ context,
+ old_values.as_deref(),
+ new_values,
+ /* pseudo_element = */ None,
+ ) {
+ let after_change_style =
+ if self.has_css_transitions(context.shared, /* pseudo_element = */ None) {
+ self.after_change_style(context, new_values)
+ } else {
+ None
+ };
+
+ // In order to avoid creating a SequentialTask for transitions which
+ // may not be updated, we check it per property to make sure Gecko
+ // side will really update transition.
+ let needs_transitions_update = {
+ // We borrow new_values here, so need to add a scope to make
+ // sure we release it before assigning a new value to it.
+ let after_change_style_ref = after_change_style.as_ref().unwrap_or(&new_values);
+
+ self.needs_transitions_update(old_values.as_ref().unwrap(), after_change_style_ref)
+ };
+
+ if needs_transitions_update {
+ if let Some(values_without_transitions) = after_change_style {
+ *new_values = values_without_transitions;
+ }
+ tasks.insert(UpdateAnimationsTasks::CSS_TRANSITIONS);
+
+ // We need to clone old_values into SequentialTask, so we can
+ // use it later.
+ old_values.clone()
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ if self.has_animations(&context.shared) {
+ tasks.insert(UpdateAnimationsTasks::EFFECT_PROPERTIES);
+ if important_rules_changed {
+ tasks.insert(UpdateAnimationsTasks::CASCADE_RESULTS);
+ }
+ if new_values.is_display_property_changed_from_none(old_values.as_deref()) {
+ tasks.insert(UpdateAnimationsTasks::DISPLAY_CHANGED_FROM_NONE);
+ }
+ }
+
+ if !tasks.is_empty() {
+ let task = crate::context::SequentialTask::update_animations(
+ *self,
+ before_change_style,
+ tasks,
+ );
+ context.thread_local.tasks.push(task);
+ }
+ }
+
+ #[cfg(feature = "servo")]
+ fn process_animations(
+ &self,
+ context: &mut StyleContext<Self>,
+ old_styles: &mut ElementStyles,
+ new_resolved_styles: &mut ResolvedElementStyles,
+ _restyle_hint: RestyleHint,
+ _important_rules_changed: bool,
+ ) {
+ use crate::animation::AnimationSetKey;
+ use crate::dom::TDocument;
+
+ let style_changed = self.process_animations_for_style(
+ context,
+ &mut old_styles.primary,
+ new_resolved_styles.primary_style_mut(),
+ /* pseudo_element = */ None,
+ );
+
+ // If we have modified animation or transitions, we recascade style for this node.
+ if style_changed {
+ let mut rule_node = new_resolved_styles.primary_style().rules().clone();
+ let declarations = context.shared.animations.get_all_declarations(
+ &AnimationSetKey::new_for_non_pseudo(self.as_node().opaque()),
+ context.shared.current_time_for_animations,
+ self.as_node().owner_doc().shared_lock(),
+ );
+ Self::replace_single_rule_node(
+ &context.shared,
+ CascadeLevel::Transitions,
+ declarations.transitions.as_ref().map(|a| a.borrow_arc()),
+ &mut rule_node,
+ );
+ Self::replace_single_rule_node(
+ &context.shared,
+ CascadeLevel::Animations,
+ declarations.animations.as_ref().map(|a| a.borrow_arc()),
+ &mut rule_node,
+ );
+
+ if rule_node != *new_resolved_styles.primary_style().rules() {
+ let inputs = CascadeInputs {
+ rules: Some(rule_node),
+ visited_rules: new_resolved_styles.primary_style().visited_rules().cloned(),
+ };
+
+ new_resolved_styles.primary.style = StyleResolverForElement::new(
+ *self,
+ context,
+ RuleInclusion::All,
+ PseudoElementResolution::IfApplicable,
+ )
+ .cascade_style_and_visited_with_default_parents(inputs);
+ }
+ }
+
+ self.process_animations_for_pseudo(
+ context,
+ old_styles,
+ new_resolved_styles,
+ PseudoElement::Before,
+ );
+ self.process_animations_for_pseudo(
+ context,
+ old_styles,
+ new_resolved_styles,
+ PseudoElement::After,
+ );
+ }
+
+ #[cfg(feature = "servo")]
+ fn process_animations_for_pseudo(
+ &self,
+ context: &mut StyleContext<Self>,
+ old_styles: &mut ElementStyles,
+ new_resolved_styles: &mut ResolvedElementStyles,
+ pseudo_element: PseudoElement,
+ ) {
+ use crate::animation::AnimationSetKey;
+ use crate::dom::TDocument;
+
+ let key = AnimationSetKey::new_for_pseudo(self.as_node().opaque(), pseudo_element.clone());
+ let mut style = match new_resolved_styles.pseudos.get(&pseudo_element) {
+ Some(style) => Arc::clone(style),
+ None => {
+ context
+ .shared
+ .animations
+ .cancel_all_animations_for_key(&key);
+ return;
+ },
+ };
+
+ let mut old_style = old_styles.pseudos.get(&pseudo_element).cloned();
+ self.process_animations_for_style(
+ context,
+ &mut old_style,
+ &mut style,
+ Some(pseudo_element.clone()),
+ );
+
+ let declarations = context.shared.animations.get_all_declarations(
+ &key,
+ context.shared.current_time_for_animations,
+ self.as_node().owner_doc().shared_lock(),
+ );
+ if declarations.is_empty() {
+ return;
+ }
+
+ let mut rule_node = style.rules().clone();
+ Self::replace_single_rule_node(
+ &context.shared,
+ CascadeLevel::Transitions,
+ LayerOrder::root(),
+ declarations.transitions.as_ref().map(|a| a.borrow_arc()),
+ &mut rule_node,
+ );
+ Self::replace_single_rule_node(
+ &context.shared,
+ CascadeLevel::Animations,
+ LayerOrder::root(),
+ declarations.animations.as_ref().map(|a| a.borrow_arc()),
+ &mut rule_node,
+ );
+ if rule_node == *style.rules() {
+ return;
+ }
+
+ let inputs = CascadeInputs {
+ rules: Some(rule_node),
+ visited_rules: style.visited_rules().cloned(),
+ };
+
+ let new_style = StyleResolverForElement::new(
+ *self,
+ context,
+ RuleInclusion::All,
+ PseudoElementResolution::IfApplicable,
+ )
+ .cascade_style_and_visited_for_pseudo_with_default_parents(
+ inputs,
+ &pseudo_element,
+ &new_resolved_styles.primary,
+ );
+
+ new_resolved_styles
+ .pseudos
+ .set(&pseudo_element, new_style.0);
+ }
+
+ #[cfg(feature = "servo")]
+ fn process_animations_for_style(
+ &self,
+ context: &mut StyleContext<Self>,
+ old_values: &mut Option<Arc<ComputedValues>>,
+ new_values: &mut Arc<ComputedValues>,
+ pseudo_element: Option<PseudoElement>,
+ ) -> bool {
+ use crate::animation::{AnimationSetKey, AnimationState};
+
+ // We need to call this before accessing the `ElementAnimationSet` from the
+ // map because this call will do a RwLock::read().
+ let needs_animations_update = self.needs_animations_update(
+ context,
+ old_values.as_deref(),
+ new_values,
+ pseudo_element,
+ );
+
+ let might_need_transitions_update = self.might_need_transitions_update(
+ context,
+ old_values.as_deref(),
+ new_values,
+ pseudo_element,
+ );
+
+ let mut after_change_style = None;
+ if might_need_transitions_update {
+ after_change_style = self.after_change_style(context, new_values);
+ }
+
+ let key = AnimationSetKey::new(self.as_node().opaque(), pseudo_element);
+ let shared_context = context.shared;
+ let mut animation_set = shared_context
+ .animations
+ .sets
+ .write()
+ .remove(&key)
+ .unwrap_or_default();
+
+ // Starting animations is expensive, because we have to recalculate the style
+ // for all the keyframes. We only want to do this if we think that there's a
+ // chance that the animations really changed.
+ if needs_animations_update {
+ let mut resolver = StyleResolverForElement::new(
+ *self,
+ context,
+ RuleInclusion::All,
+ PseudoElementResolution::IfApplicable,
+ );
+
+ animation_set.update_animations_for_new_style::<Self>(
+ *self,
+ &shared_context,
+ &new_values,
+ &mut resolver,
+ );
+ }
+
+ animation_set.update_transitions_for_new_style(
+ might_need_transitions_update,
+ &shared_context,
+ old_values.as_ref(),
+ after_change_style.as_ref().unwrap_or(new_values),
+ );
+
+ // We clear away any finished transitions, but retain animations, because they
+ // might still be used for proper calculation of `animation-fill-mode`. This
+ // should change the computed values in the style, so we don't need to mark
+ // this set as dirty.
+ animation_set
+ .transitions
+ .retain(|transition| transition.state != AnimationState::Finished);
+
+ // If the ElementAnimationSet is empty, and don't store it in order to
+ // save memory and to avoid extra processing later.
+ let changed_animations = animation_set.dirty;
+ if !animation_set.is_empty() {
+ animation_set.dirty = false;
+ shared_context
+ .animations
+ .sets
+ .write()
+ .insert(key, animation_set);
+ }
+
+ changed_animations
+ }
+
+ /// Computes and applies non-redundant damage.
+ fn accumulate_damage_for(
+ &self,
+ shared_context: &SharedStyleContext,
+ damage: &mut RestyleDamage,
+ old_values: &ComputedValues,
+ new_values: &ComputedValues,
+ pseudo: Option<&PseudoElement>,
+ ) -> ChildRestyleRequirement {
+ debug!("accumulate_damage_for: {:?}", self);
+ debug_assert!(!shared_context
+ .traversal_flags
+ .contains(TraversalFlags::FinalAnimationTraversal));
+
+ let difference = self.compute_style_difference(old_values, new_values, pseudo);
+
+ *damage |= difference.damage;
+
+ debug!(" > style difference: {:?}", difference);
+
+ // We need to cascade the children in order to ensure the correct
+ // propagation of inherited computed value flags.
+ if old_values.flags.maybe_inherited() != new_values.flags.maybe_inherited() {
+ debug!(
+ " > flags changed: {:?} != {:?}",
+ old_values.flags, new_values.flags
+ );
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+
+ if old_values.effective_zoom != new_values.effective_zoom {
+ // Zoom changes need to get propagated to children.
+ debug!(
+ " > zoom changed: {:?} != {:?}",
+ old_values.effective_zoom, new_values.effective_zoom
+ );
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+
+ match difference.change {
+ StyleChange::Unchanged => return ChildRestyleRequirement::CanSkipCascade,
+ StyleChange::Changed { reset_only } => {
+ // If inherited properties changed, the best we can do is
+ // cascade the children.
+ if !reset_only {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+ },
+ }
+
+ let old_display = old_values.clone_display();
+ let new_display = new_values.clone_display();
+
+ if old_display != new_display {
+ // If we used to be a display: none element, and no longer are, our
+ // children need to be restyled because they're unstyled.
+ if old_display == Display::None {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+ // Blockification of children may depend on our display value,
+ // so we need to actually do the recascade. We could potentially
+ // do better, but it doesn't seem worth it.
+ if old_display.is_item_container() != new_display.is_item_container() {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+ // We may also need to blockify and un-blockify descendants if our
+ // display goes from / to display: contents, since the "layout
+ // parent style" changes.
+ if old_display.is_contents() || new_display.is_contents() {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+ // Line break suppression may also be affected if the display
+ // type changes from ruby to non-ruby.
+ #[cfg(feature = "gecko")]
+ {
+ if old_display.is_ruby_type() != new_display.is_ruby_type() {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+ }
+ }
+
+ // Children with justify-items: auto may depend on our
+ // justify-items property value.
+ //
+ // Similarly, we could potentially do better, but this really
+ // seems not common enough to care about.
+ #[cfg(feature = "gecko")]
+ {
+ use crate::values::specified::align::AlignFlags;
+
+ let old_justify_items = old_values.get_position().clone_justify_items();
+ let new_justify_items = new_values.get_position().clone_justify_items();
+
+ let was_legacy_justify_items =
+ old_justify_items.computed.0.contains(AlignFlags::LEGACY);
+
+ let is_legacy_justify_items = new_justify_items.computed.0.contains(AlignFlags::LEGACY);
+
+ if is_legacy_justify_items != was_legacy_justify_items {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+
+ if was_legacy_justify_items && old_justify_items.computed != new_justify_items.computed
+ {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+ }
+
+ #[cfg(feature = "servo")]
+ {
+ // We may need to set or propagate the CAN_BE_FRAGMENTED bit
+ // on our children.
+ if old_values.is_multicol() != new_values.is_multicol() {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+ }
+
+ // We could prove that, if our children don't inherit reset
+ // properties, we can stop the cascade.
+ ChildRestyleRequirement::MustCascadeChildrenIfInheritResetStyle
+ }
+}
+
+impl<E: TElement> PrivateMatchMethods for E {}
+
+/// The public API that elements expose for selector matching.
+pub trait MatchMethods: TElement {
+ /// Returns the closest parent element that doesn't have a display: contents
+ /// style (and thus generates a box).
+ ///
+ /// This is needed to correctly handle blockification of flex and grid
+ /// items.
+ ///
+ /// Returns itself if the element has no parent. In practice this doesn't
+ /// happen because the root element is blockified per spec, but it could
+ /// happen if we decide to not blockify for roots of disconnected subtrees,
+ /// which is a kind of dubious behavior.
+ fn layout_parent(&self) -> Self {
+ let mut current = self.clone();
+ loop {
+ current = match current.traversal_parent() {
+ Some(el) => el,
+ None => return current,
+ };
+
+ let is_display_contents = current
+ .borrow_data()
+ .unwrap()
+ .styles
+ .primary()
+ .is_display_contents();
+
+ if !is_display_contents {
+ return current;
+ }
+ }
+ }
+
+ /// Updates the styles with the new ones, diffs them, and stores the restyle
+ /// damage.
+ fn finish_restyle(
+ &self,
+ context: &mut StyleContext<Self>,
+ data: &mut ElementData,
+ mut new_styles: ResolvedElementStyles,
+ important_rules_changed: bool,
+ ) -> ChildRestyleRequirement {
+ use std::cmp;
+
+ self.process_animations(
+ context,
+ &mut data.styles,
+ &mut new_styles,
+ data.hint,
+ important_rules_changed,
+ );
+
+ // First of all, update the styles.
+ let old_styles = data.set_styles(new_styles);
+
+ let new_primary_style = data.styles.primary.as_ref().unwrap();
+
+ let mut restyle_requirement = ChildRestyleRequirement::CanSkipCascade;
+ let is_root = new_primary_style
+ .flags
+ .contains(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE);
+ let is_container = !new_primary_style
+ .get_box()
+ .clone_container_type()
+ .is_normal();
+ if is_root || is_container {
+ let device = context.shared.stylist.device();
+ let old_style = old_styles.primary.as_ref();
+ let new_font_size = new_primary_style.get_font().clone_font_size();
+ let old_font_size = old_style.map(|s| s.get_font().clone_font_size());
+
+ if old_font_size != Some(new_font_size) {
+ if is_root {
+ debug_assert!(self.owner_doc_matches_for_testing(device));
+ device.set_root_font_size(new_font_size.computed_size().into());
+ if device.used_root_font_size() {
+ // If the root font-size changed since last time, and something
+ // in the document did use rem units, ensure we recascade the
+ // entire tree.
+ restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants;
+ }
+ }
+
+ if is_container && old_font_size.is_some() {
+ // TODO(emilio): Maybe only do this if we were matched
+ // against relative font sizes?
+ // Also, maybe we should do this as well for font-family /
+ // etc changes (for ex/ch/ic units to work correctly)? We
+ // should probably do the optimization mentioned above if
+ // so.
+ restyle_requirement = ChildRestyleRequirement::MustMatchDescendants;
+ }
+ }
+
+ // For line-height, we want the fully resolved value, as `normal` also depends on other font properties.
+ let new_line_height = device
+ .calc_line_height(
+ &new_primary_style.get_font(),
+ new_primary_style.writing_mode,
+ None,
+ )
+ .0;
+ let old_line_height = old_style.map(|s| {
+ device
+ .calc_line_height(&s.get_font(), s.writing_mode, None)
+ .0
+ });
+
+ if restyle_requirement != ChildRestyleRequirement::MustMatchDescendants &&
+ old_line_height != Some(new_line_height)
+ {
+ if is_root {
+ debug_assert!(self.owner_doc_matches_for_testing(device));
+ device.set_root_line_height(new_line_height.into());
+ if device.used_root_line_height() {
+ restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants;
+ }
+ }
+
+ if is_container && old_line_height.is_some() {
+ restyle_requirement = ChildRestyleRequirement::MustMatchDescendants;
+ }
+ }
+ }
+
+ if context.shared.stylist.quirks_mode() == QuirksMode::Quirks {
+ if self.is_html_document_body_element() {
+ // NOTE(emilio): We _could_ handle dynamic changes to it if it
+ // changes and before we reach our children the cascade stops,
+ // but we don't track right now whether we use the document body
+ // color, and nobody else handles that properly anyway.
+ let device = context.shared.stylist.device();
+
+ // Needed for the "inherit from body" quirk.
+ let text_color = new_primary_style.get_inherited_text().clone_color();
+ device.set_body_text_color(text_color);
+ }
+ }
+
+ // Don't accumulate damage if we're in the final animation traversal.
+ if context
+ .shared
+ .traversal_flags
+ .contains(TraversalFlags::FinalAnimationTraversal)
+ {
+ return ChildRestyleRequirement::MustCascadeChildren;
+ }
+
+ // Also, don't do anything if there was no style.
+ let old_primary_style = match old_styles.primary {
+ Some(s) => s,
+ None => return ChildRestyleRequirement::MustCascadeChildren,
+ };
+
+ let old_container_type = old_primary_style.clone_container_type();
+ let new_container_type = new_primary_style.clone_container_type();
+ if old_container_type != new_container_type && !new_container_type.is_size_container_type()
+ {
+ // Stopped being a size container. Re-evaluate container queries and units on all our descendants.
+ // Changes into and between different size containment is handled in `UpdateContainerQueryStyles`.
+ restyle_requirement = ChildRestyleRequirement::MustMatchDescendants;
+ } else if old_container_type.is_size_container_type() &&
+ !old_primary_style.is_display_contents() &&
+ new_primary_style.is_display_contents()
+ {
+ // Also re-evaluate when a container gets 'display: contents', since size queries will now evaluate to unknown.
+ // Other displays like 'inline' will keep generating a box, so they are handled in `UpdateContainerQueryStyles`.
+ restyle_requirement = ChildRestyleRequirement::MustMatchDescendants;
+ }
+
+ restyle_requirement = cmp::max(
+ restyle_requirement,
+ self.accumulate_damage_for(
+ context.shared,
+ &mut data.damage,
+ &old_primary_style,
+ new_primary_style,
+ None,
+ ),
+ );
+
+ if data.styles.pseudos.is_empty() && old_styles.pseudos.is_empty() {
+ // This is the common case; no need to examine pseudos here.
+ return restyle_requirement;
+ }
+
+ let pseudo_styles = old_styles
+ .pseudos
+ .as_array()
+ .iter()
+ .zip(data.styles.pseudos.as_array().iter());
+
+ for (i, (old, new)) in pseudo_styles.enumerate() {
+ match (old, new) {
+ (&Some(ref old), &Some(ref new)) => {
+ self.accumulate_damage_for(
+ context.shared,
+ &mut data.damage,
+ old,
+ new,
+ Some(&PseudoElement::from_eager_index(i)),
+ );
+ },
+ (&None, &None) => {},
+ _ => {
+ // It's possible that we're switching from not having
+ // ::before/::after at all to having styles for them but not
+ // actually having a useful pseudo-element. Check for that
+ // case.
+ let pseudo = PseudoElement::from_eager_index(i);
+ let new_pseudo_should_exist =
+ new.as_ref().map_or(false, |s| pseudo.should_exist(s));
+ let old_pseudo_should_exist =
+ old.as_ref().map_or(false, |s| pseudo.should_exist(s));
+ if new_pseudo_should_exist != old_pseudo_should_exist {
+ data.damage |= RestyleDamage::reconstruct();
+ return restyle_requirement;
+ }
+ },
+ }
+ }
+
+ restyle_requirement
+ }
+
+ /// Updates the rule nodes without re-running selector matching, using just
+ /// the rule tree.
+ ///
+ /// Returns true if an !important rule was replaced.
+ fn replace_rules(
+ &self,
+ replacements: RestyleHint,
+ context: &mut StyleContext<Self>,
+ cascade_inputs: &mut ElementCascadeInputs,
+ ) -> bool {
+ let mut result = false;
+ result |= self.replace_rules_internal(
+ replacements,
+ context,
+ CascadeVisitedMode::Unvisited,
+ cascade_inputs,
+ );
+ result |= self.replace_rules_internal(
+ replacements,
+ context,
+ CascadeVisitedMode::Visited,
+ cascade_inputs,
+ );
+ result
+ }
+
+ /// Given the old and new style of this element, and whether it's a
+ /// pseudo-element, compute the restyle damage used to determine which
+ /// kind of layout or painting operations we'll need.
+ fn compute_style_difference(
+ &self,
+ old_values: &ComputedValues,
+ new_values: &ComputedValues,
+ pseudo: Option<&PseudoElement>,
+ ) -> StyleDifference {
+ debug_assert!(pseudo.map_or(true, |p| p.is_eager()));
+ RestyleDamage::compute_style_difference(old_values, new_values)
+ }
+}
+
+impl<E: TElement> MatchMethods for E {}
diff --git a/servo/components/style/media_queries/media_list.rs b/servo/components/style/media_queries/media_list.rs
new file mode 100644
index 0000000000..3c2ba9ee5c
--- /dev/null
+++ b/servo/components/style/media_queries/media_list.rs
@@ -0,0 +1,150 @@
+/* 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/. */
+
+//! A media query list:
+//!
+//! https://drafts.csswg.org/mediaqueries/#typedef-media-query-list
+
+use super::{Device, MediaQuery, Qualifier};
+use crate::context::QuirksMode;
+use crate::error_reporting::ContextualParseError;
+use crate::parser::ParserContext;
+use crate::queries::condition::KleeneValue;
+use crate::values::computed;
+use cssparser::{Delimiter, Parser};
+use cssparser::{ParserInput, Token};
+
+/// A type that encapsulates a media query list.
+#[derive(Clone, MallocSizeOf, ToCss, ToShmem)]
+#[css(comma, derive_debug)]
+pub struct MediaList {
+ /// The list of media queries.
+ #[css(iterable)]
+ pub media_queries: Vec<MediaQuery>,
+}
+
+impl MediaList {
+ /// Parse a media query list from CSS.
+ ///
+ /// Always returns a media query list. If any invalid media query is
+ /// found, the media query list is only filled with the equivalent of
+ /// "not all", see:
+ ///
+ /// <https://drafts.csswg.org/mediaqueries/#error-handling>
+ pub fn parse(context: &ParserContext, input: &mut Parser) -> Self {
+ if input.is_exhausted() {
+ return Self::empty();
+ }
+
+ let mut media_queries = vec![];
+ loop {
+ let start_position = input.position();
+ match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(context, i)) {
+ Ok(mq) => {
+ media_queries.push(mq);
+ },
+ Err(err) => {
+ media_queries.push(MediaQuery::never_matching());
+ let location = err.location;
+ let error = ContextualParseError::InvalidMediaRule(
+ input.slice_from(start_position),
+ err,
+ );
+ context.log_css_error(location, error);
+ },
+ }
+
+ match input.next() {
+ Ok(&Token::Comma) => {},
+ Ok(_) => unreachable!(),
+ Err(_) => break,
+ }
+ }
+
+ MediaList { media_queries }
+ }
+
+ /// Create an empty MediaList.
+ pub fn empty() -> Self {
+ MediaList {
+ media_queries: vec![],
+ }
+ }
+
+ /// Evaluate a whole `MediaList` against `Device`.
+ pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool {
+ // Check if it is an empty media query list or any queries match.
+ // https://drafts.csswg.org/mediaqueries-4/#mq-list
+ if self.media_queries.is_empty() {
+ return true;
+ }
+
+ computed::Context::for_media_query_evaluation(device, quirks_mode, |context| {
+ self.media_queries.iter().any(|mq| {
+ let mut query_match = if mq.media_type.matches(device.media_type()) {
+ mq.condition
+ .as_ref()
+ .map_or(KleeneValue::True, |c| c.matches(context))
+ } else {
+ KleeneValue::False
+ };
+
+ // Apply the logical NOT qualifier to the result
+ if matches!(mq.qualifier, Some(Qualifier::Not)) {
+ query_match = !query_match;
+ }
+ query_match.to_bool(/* unknown = */ false)
+ })
+ })
+ }
+
+ /// Whether this `MediaList` contains no media queries.
+ pub fn is_empty(&self) -> bool {
+ self.media_queries.is_empty()
+ }
+
+ /// Whether this `MediaList` depends on the viewport size.
+ pub fn is_viewport_dependent(&self) -> bool {
+ self.media_queries.iter().any(|q| q.is_viewport_dependent())
+ }
+
+ /// Append a new media query item to the media list.
+ /// <https://drafts.csswg.org/cssom/#dom-medialist-appendmedium>
+ ///
+ /// Returns true if added, false if fail to parse the medium string.
+ pub fn append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool {
+ let mut input = ParserInput::new(new_medium);
+ let mut parser = Parser::new(&mut input);
+ let new_query = match MediaQuery::parse(&context, &mut parser) {
+ Ok(query) => query,
+ Err(_) => {
+ return false;
+ },
+ };
+ // This algorithm doesn't actually matches the current spec,
+ // but it matches the behavior of Gecko and Edge.
+ // See https://github.com/w3c/csswg-drafts/issues/697
+ self.media_queries.retain(|query| query != &new_query);
+ self.media_queries.push(new_query);
+ true
+ }
+
+ /// Delete a media query from the media list.
+ /// <https://drafts.csswg.org/cssom/#dom-medialist-deletemedium>
+ ///
+ /// Returns true if found and deleted, false otherwise.
+ pub fn delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool {
+ let mut input = ParserInput::new(old_medium);
+ let mut parser = Parser::new(&mut input);
+ let old_query = match MediaQuery::parse(context, &mut parser) {
+ Ok(query) => query,
+ Err(_) => {
+ return false;
+ },
+ };
+ let old_len = self.media_queries.len();
+ self.media_queries.retain(|query| query != &old_query);
+ old_len != self.media_queries.len()
+ }
+}
diff --git a/servo/components/style/media_queries/media_query.rs b/servo/components/style/media_queries/media_query.rs
new file mode 100644
index 0000000000..c30a445393
--- /dev/null
+++ b/servo/components/style/media_queries/media_query.rs
@@ -0,0 +1,193 @@
+/* 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/. */
+
+//! A media query:
+//!
+//! https://drafts.csswg.org/mediaqueries/#typedef-media-query
+
+use crate::parser::ParserContext;
+use crate::queries::{FeatureFlags, FeatureType, QueryCondition};
+use crate::str::string_as_ascii_lowercase;
+use crate::values::CustomIdent;
+use crate::Atom;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// <https://drafts.csswg.org/mediaqueries/#mq-prefix>
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
+pub enum Qualifier {
+ /// Hide a media query from legacy UAs:
+ /// <https://drafts.csswg.org/mediaqueries/#mq-only>
+ Only,
+ /// Negate a media query:
+ /// <https://drafts.csswg.org/mediaqueries/#mq-not>
+ Not,
+}
+
+/// <https://drafts.csswg.org/mediaqueries/#media-types>
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub struct MediaType(pub CustomIdent);
+
+impl MediaType {
+ /// The `screen` media type.
+ pub fn screen() -> Self {
+ MediaType(CustomIdent(atom!("screen")))
+ }
+
+ /// The `print` media type.
+ pub fn print() -> Self {
+ MediaType(CustomIdent(atom!("print")))
+ }
+
+ fn parse(name: &str) -> Result<Self, ()> {
+ // From https://drafts.csswg.org/mediaqueries/#mq-syntax:
+ //
+ // The <media-type> production does not include the keywords only, not, and, or, and layer.
+ //
+ // Here we also perform the to-ascii-lowercase part of the serialization
+ // algorithm: https://drafts.csswg.org/cssom/#serializing-media-queries
+ match_ignore_ascii_case! { name,
+ "not" | "or" | "and" | "only" | "layer" => Err(()),
+ _ => Ok(MediaType(CustomIdent(Atom::from(string_as_ascii_lowercase(name))))),
+ }
+ }
+}
+
+/// A [media query][mq].
+///
+/// [mq]: https://drafts.csswg.org/mediaqueries/
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct MediaQuery {
+ /// The qualifier for this query.
+ pub qualifier: Option<Qualifier>,
+ /// The media type for this query, that can be known, unknown, or "all".
+ pub media_type: MediaQueryType,
+ /// The condition that this media query contains. This cannot have `or`
+ /// in the first level.
+ pub condition: Option<QueryCondition>,
+}
+
+impl ToCss for MediaQuery {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if let Some(qual) = self.qualifier {
+ qual.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+
+ match self.media_type {
+ MediaQueryType::All => {
+ // We need to print "all" if there's a qualifier, or there's
+ // just an empty list of expressions.
+ //
+ // Otherwise, we'd serialize media queries like "(min-width:
+ // 40px)" in "all (min-width: 40px)", which is unexpected.
+ if self.qualifier.is_some() || self.condition.is_none() {
+ dest.write_str("all")?;
+ }
+ },
+ MediaQueryType::Concrete(MediaType(ref desc)) => desc.to_css(dest)?,
+ }
+
+ let condition = match self.condition {
+ Some(ref c) => c,
+ None => return Ok(()),
+ };
+
+ if self.media_type != MediaQueryType::All || self.qualifier.is_some() {
+ dest.write_str(" and ")?;
+ }
+
+ condition.to_css(dest)
+ }
+}
+
+impl MediaQuery {
+ /// Return a media query that never matches, used for when we fail to parse
+ /// a given media query.
+ pub fn never_matching() -> Self {
+ Self {
+ qualifier: Some(Qualifier::Not),
+ media_type: MediaQueryType::All,
+ condition: None,
+ }
+ }
+
+ /// Returns whether this media query depends on the viewport.
+ pub fn is_viewport_dependent(&self) -> bool {
+ self.condition.as_ref().map_or(false, |c| {
+ return c
+ .cumulative_flags()
+ .contains(FeatureFlags::VIEWPORT_DEPENDENT);
+ })
+ }
+
+ /// Parse a media query given css input.
+ ///
+ /// Returns an error if any of the expressions is unknown.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let (qualifier, explicit_media_type) = input
+ .try_parse(|input| -> Result<_, ()> {
+ let qualifier = input.try_parse(Qualifier::parse).ok();
+ let ident = input.expect_ident().map_err(|_| ())?;
+ let media_type = MediaQueryType::parse(&ident)?;
+ Ok((qualifier, Some(media_type)))
+ })
+ .unwrap_or_default();
+
+ let condition = if explicit_media_type.is_none() {
+ Some(QueryCondition::parse(context, input, FeatureType::Media)?)
+ } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() {
+ Some(QueryCondition::parse_disallow_or(
+ context,
+ input,
+ FeatureType::Media,
+ )?)
+ } else {
+ None
+ };
+
+ let media_type = explicit_media_type.unwrap_or(MediaQueryType::All);
+ Ok(Self {
+ qualifier,
+ media_type,
+ condition,
+ })
+ }
+}
+
+/// <http://dev.w3.org/csswg/mediaqueries-3/#media0>
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub enum MediaQueryType {
+ /// A media type that matches every device.
+ All,
+ /// A specific media type.
+ Concrete(MediaType),
+}
+
+impl MediaQueryType {
+ fn parse(ident: &str) -> Result<Self, ()> {
+ match_ignore_ascii_case! { ident,
+ "all" => return Ok(MediaQueryType::All),
+ _ => (),
+ };
+
+ // If parseable, accept this type as a concrete type.
+ MediaType::parse(ident).map(MediaQueryType::Concrete)
+ }
+
+ /// Returns whether this media query type matches a MediaType.
+ pub fn matches(&self, other: MediaType) -> bool {
+ match *self {
+ MediaQueryType::All => true,
+ MediaQueryType::Concrete(ref known_type) => *known_type == other,
+ }
+ }
+}
diff --git a/servo/components/style/media_queries/mod.rs b/servo/components/style/media_queries/mod.rs
new file mode 100644
index 0000000000..833f6f53cb
--- /dev/null
+++ b/servo/components/style/media_queries/mod.rs
@@ -0,0 +1,18 @@
+/* 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/. */
+
+//! [Media queries][mq].
+//!
+//! [mq]: https://drafts.csswg.org/mediaqueries/
+
+mod media_list;
+mod media_query;
+
+pub use self::media_list::MediaList;
+pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier};
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::media_queries::Device;
+#[cfg(feature = "servo")]
+pub use crate::servo::media_queries::Device;
diff --git a/servo/components/style/parallel.rs b/servo/components/style/parallel.rs
new file mode 100644
index 0000000000..0b2ccf46d1
--- /dev/null
+++ b/servo/components/style/parallel.rs
@@ -0,0 +1,194 @@
+/* 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/. */
+
+//! Implements parallel traversal over the DOM tree.
+//!
+//! This traversal is based on Rayon, and therefore its safety is largely
+//! verified by the type system.
+//!
+//! The primary trickiness and fine print for the above relates to the
+//! thread safety of the DOM nodes themselves. Accessing a DOM element
+//! concurrently on multiple threads is actually mostly "safe", since all
+//! the mutable state is protected by an AtomicRefCell, and so we'll
+//! generally panic if something goes wrong. Still, we try to to enforce our
+//! thread invariants at compile time whenever possible. As such, TNode and
+//! TElement are not Send, so ordinary style system code cannot accidentally
+//! share them with other threads. In the parallel traversal, we explicitly
+//! invoke |unsafe { SendNode::new(n) }| to put nodes in containers that may
+//! be sent to other threads. This occurs in only a handful of places and is
+//! easy to grep for. At the time of this writing, there is no other unsafe
+//! code in the parallel traversal.
+
+#![deny(missing_docs)]
+
+use crate::context::{StyleContext, ThreadLocalStyleContext};
+use crate::dom::{OpaqueNode, SendNode, TElement};
+use crate::scoped_tls::ScopedTLS;
+use crate::traversal::{DomTraversal, PerLevelTraversalData};
+use rayon;
+use std::collections::VecDeque;
+
+/// The minimum stack size for a thread in the styling pool, in kilobytes.
+pub const STYLE_THREAD_STACK_SIZE_KB: usize = 256;
+
+/// The stack margin. If we get this deep in the stack, we will skip recursive
+/// optimizations to ensure that there is sufficient room for non-recursive work.
+///
+/// We allocate large safety margins because certain OS calls can use very large
+/// amounts of stack space [1]. Reserving a larger-than-necessary stack costs us
+/// address space, but if we keep our safety margin big, we will generally avoid
+/// committing those extra pages, and only use them in edge cases that would
+/// otherwise cause crashes.
+///
+/// When measured with 128KB stacks and 40KB margin, we could support 53
+/// levels of recursion before the limiter kicks in, on x86_64-Linux [2]. When
+/// we doubled the stack size, we added it all to the safety margin, so we should
+/// be able to get the same amount of recursion.
+///
+/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1395708#c15
+/// [2] See Gecko bug 1376883 for more discussion on the measurements.
+pub const STACK_SAFETY_MARGIN_KB: usize = 168;
+
+/// A callback to create our thread local context. This needs to be
+/// out of line so we don't allocate stack space for the entire struct
+/// in the caller.
+#[inline(never)]
+pub(crate) fn create_thread_local_context<'scope, E>(slot: &mut Option<ThreadLocalStyleContext<E>>)
+where
+ E: TElement + 'scope,
+{
+ *slot = Some(ThreadLocalStyleContext::new());
+}
+
+// Sends one chunk of work to the thread-pool.
+fn distribute_one_chunk<'a, 'scope, E, D>(
+ items: VecDeque<SendNode<E::ConcreteNode>>,
+ traversal_root: OpaqueNode,
+ work_unit_max: usize,
+ traversal_data: PerLevelTraversalData,
+ scope: &'a rayon::ScopeFifo<'scope>,
+ traversal: &'scope D,
+ tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>,
+) where
+ E: TElement + 'scope,
+ D: DomTraversal<E>,
+{
+ scope.spawn_fifo(move |scope| {
+ gecko_profiler_label!(Layout, StyleComputation);
+ let mut tlc = tls.ensure(create_thread_local_context);
+ let mut context = StyleContext {
+ shared: traversal.shared_context(),
+ thread_local: &mut *tlc,
+ };
+ style_trees(
+ &mut context,
+ items,
+ traversal_root,
+ work_unit_max,
+ traversal_data,
+ Some(scope),
+ traversal,
+ tls,
+ );
+ })
+}
+
+/// Distributes all items into the thread pool, in `work_unit_max` chunks.
+fn distribute_work<'a, 'scope, E, D>(
+ mut items: VecDeque<SendNode<E::ConcreteNode>>,
+ traversal_root: OpaqueNode,
+ work_unit_max: usize,
+ traversal_data: PerLevelTraversalData,
+ scope: &'a rayon::ScopeFifo<'scope>,
+ traversal: &'scope D,
+ tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>,
+) where
+ E: TElement + 'scope,
+ D: DomTraversal<E>,
+{
+ while items.len() > work_unit_max {
+ let rest = items.split_off(work_unit_max);
+ distribute_one_chunk(
+ items,
+ traversal_root,
+ work_unit_max,
+ traversal_data,
+ scope,
+ traversal,
+ tls,
+ );
+ items = rest;
+ }
+ distribute_one_chunk(
+ items,
+ traversal_root,
+ work_unit_max,
+ traversal_data,
+ scope,
+ traversal,
+ tls,
+ );
+}
+
+/// Processes `discovered` items, possibly spawning work in other threads as needed.
+#[inline]
+pub fn style_trees<'a, 'scope, E, D>(
+ context: &mut StyleContext<E>,
+ mut discovered: VecDeque<SendNode<E::ConcreteNode>>,
+ traversal_root: OpaqueNode,
+ work_unit_max: usize,
+ mut traversal_data: PerLevelTraversalData,
+ scope: Option<&'a rayon::ScopeFifo<'scope>>,
+ traversal: &'scope D,
+ tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>,
+) where
+ E: TElement + 'scope,
+ D: DomTraversal<E>,
+{
+ let local_queue_size = if tls.current_thread_index() == 0 {
+ static_prefs::pref!("layout.css.stylo-local-work-queue.in-main-thread")
+ } else {
+ static_prefs::pref!("layout.css.stylo-local-work-queue.in-worker")
+ } as usize;
+
+ let mut nodes_remaining_at_current_depth = discovered.len();
+ while let Some(node) = discovered.pop_front() {
+ let mut children_to_process = 0isize;
+ traversal.process_preorder(&traversal_data, context, *node, |n| {
+ children_to_process += 1;
+ discovered.push_back(unsafe { SendNode::new(n) });
+ });
+
+ traversal.handle_postorder_traversal(context, traversal_root, *node, children_to_process);
+
+ nodes_remaining_at_current_depth -= 1;
+
+ // If we have enough children at the next depth in the DOM, spawn them to a different job
+ // relatively soon, while keeping always at least `local_queue_size` worth of work for
+ // ourselves.
+ let discovered_children = discovered.len() - nodes_remaining_at_current_depth;
+ if discovered_children >= work_unit_max &&
+ discovered.len() >= local_queue_size + work_unit_max &&
+ scope.is_some()
+ {
+ let kept_work = std::cmp::max(nodes_remaining_at_current_depth, local_queue_size);
+ let mut traversal_data_copy = traversal_data.clone();
+ traversal_data_copy.current_dom_depth += 1;
+ distribute_work(
+ discovered.split_off(kept_work),
+ traversal_root,
+ work_unit_max,
+ traversal_data_copy,
+ scope.unwrap(),
+ traversal,
+ tls,
+ );
+ }
+
+ if nodes_remaining_at_current_depth == 0 {
+ traversal_data.current_dom_depth += 1;
+ nodes_remaining_at_current_depth = discovered.len();
+ }
+ }
+}
diff --git a/servo/components/style/parser.rs b/servo/components/style/parser.rs
new file mode 100644
index 0000000000..893625854f
--- /dev/null
+++ b/servo/components/style/parser.rs
@@ -0,0 +1,178 @@
+/* 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/. */
+
+//! The context within which CSS code is parsed.
+
+use crate::context::QuirksMode;
+use crate::error_reporting::{ContextualParseError, ParseErrorReporter};
+use crate::stylesheets::{CssRuleType, CssRuleTypes, Namespaces, Origin, UrlExtraData};
+use crate::use_counters::UseCounters;
+use cssparser::{Parser, SourceLocation, UnicodeRange};
+use std::borrow::Cow;
+use style_traits::{OneOrMoreSeparated, ParseError, ParsingMode, Separator};
+
+/// The data that the parser needs from outside in order to parse a stylesheet.
+pub struct ParserContext<'a> {
+ /// The `Origin` of the stylesheet, whether it's a user, author or
+ /// user-agent stylesheet.
+ pub stylesheet_origin: Origin,
+ /// The extra data we need for resolving url values.
+ pub url_data: &'a UrlExtraData,
+ /// The current rule types, if any.
+ pub rule_types: CssRuleTypes,
+ /// The mode to use when parsing.
+ pub parsing_mode: ParsingMode,
+ /// The quirks mode of this stylesheet.
+ pub quirks_mode: QuirksMode,
+ /// The active error reporter, or none if error reporting is disabled.
+ error_reporter: Option<&'a dyn ParseErrorReporter>,
+ /// The currently active namespaces.
+ pub namespaces: Cow<'a, Namespaces>,
+ /// The use counters we want to record while parsing style rules, if any.
+ pub use_counters: Option<&'a UseCounters>,
+}
+
+impl<'a> ParserContext<'a> {
+ /// Create a parser context.
+ #[inline]
+ pub fn new(
+ stylesheet_origin: Origin,
+ url_data: &'a UrlExtraData,
+ rule_type: Option<CssRuleType>,
+ parsing_mode: ParsingMode,
+ quirks_mode: QuirksMode,
+ namespaces: Cow<'a, Namespaces>,
+ error_reporter: Option<&'a dyn ParseErrorReporter>,
+ use_counters: Option<&'a UseCounters>,
+ ) -> Self {
+ Self {
+ stylesheet_origin,
+ url_data,
+ rule_types: rule_type.map(CssRuleTypes::from).unwrap_or_default(),
+ parsing_mode,
+ quirks_mode,
+ error_reporter,
+ namespaces,
+ use_counters,
+ }
+ }
+
+ /// Temporarily sets the rule_type and executes the callback function, returning its result.
+ pub fn nest_for_rule<R>(
+ &mut self,
+ rule_type: CssRuleType,
+ cb: impl FnOnce(&mut Self) -> R,
+ ) -> R {
+ let old_rule_types = self.rule_types;
+ self.rule_types.insert(rule_type);
+ let r = cb(self);
+ self.rule_types = old_rule_types;
+ r
+ }
+
+ /// Whether we're in a @page rule.
+ #[inline]
+ pub fn in_page_rule(&self) -> bool {
+ self.rule_types.contains(CssRuleType::Page)
+ }
+
+ /// Get the rule type, which assumes that one is available.
+ pub fn rule_types(&self) -> CssRuleTypes {
+ self.rule_types
+ }
+
+ /// Returns whether CSS error reporting is enabled.
+ #[inline]
+ pub fn error_reporting_enabled(&self) -> bool {
+ self.error_reporter.is_some()
+ }
+
+ /// Record a CSS parse error with this context’s error reporting.
+ pub fn log_css_error(&self, location: SourceLocation, error: ContextualParseError) {
+ let error_reporter = match self.error_reporter {
+ Some(r) => r,
+ None => return,
+ };
+
+ error_reporter.report_error(self.url_data, location, error)
+ }
+
+ /// Whether we're in a user-agent stylesheet.
+ #[inline]
+ pub fn in_ua_sheet(&self) -> bool {
+ self.stylesheet_origin == Origin::UserAgent
+ }
+
+ /// Returns whether chrome-only rules should be parsed.
+ #[inline]
+ pub fn chrome_rules_enabled(&self) -> bool {
+ self.url_data.chrome_rules_enabled() || self.stylesheet_origin != Origin::Author
+ }
+}
+
+/// A trait to abstract parsing of a specified value given a `ParserContext` and
+/// CSS input.
+///
+/// This can be derived on keywords with `#[derive(Parse)]`.
+///
+/// The derive code understands the following attributes on each of the variants:
+///
+/// * `#[parse(aliases = "foo,bar")]` can be used to alias a value with another
+/// at parse-time.
+///
+/// * `#[parse(condition = "function")]` can be used to make the parsing of the
+/// value conditional on `function`, which needs to fulfill
+/// `fn(&ParserContext) -> bool`.
+pub trait Parse: Sized {
+ /// Parse a value of this type.
+ ///
+ /// Returns an error on failure.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>>;
+}
+
+impl<T> Parse for Vec<T>
+where
+ T: Parse + OneOrMoreSeparated,
+ <T as OneOrMoreSeparated>::S: Separator,
+{
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ <T as OneOrMoreSeparated>::S::parse(input, |i| T::parse(context, i))
+ }
+}
+
+impl<T> Parse for Box<T>
+where
+ T: Parse,
+{
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ T::parse(context, input).map(Box::new)
+ }
+}
+
+impl Parse for crate::OwnedStr {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(input.expect_string()?.as_ref().to_owned().into())
+ }
+}
+
+impl Parse for UnicodeRange {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(UnicodeRange::parse(input)?)
+ }
+}
diff --git a/servo/components/style/piecewise_linear.rs b/servo/components/style/piecewise_linear.rs
new file mode 100644
index 0000000000..1cabd01ea1
--- /dev/null
+++ b/servo/components/style/piecewise_linear.rs
@@ -0,0 +1,281 @@
+/* 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/. */
+
+//! A piecewise linear function, following CSS linear easing
+use crate::values::computed::Percentage;
+use core::slice::Iter;
+/// draft as in https://github.com/w3c/csswg-drafts/pull/6533.
+use euclid::approxeq::ApproxEq;
+use itertools::Itertools;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+use crate::values::CSSFloat;
+
+type ValueType = CSSFloat;
+/// a single entry in a piecewise linear function.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToResolvedValue,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[repr(C)]
+pub struct PiecewiseLinearFunctionEntry {
+ pub x: ValueType,
+ pub y: ValueType,
+}
+
+impl ToCss for PiecewiseLinearFunctionEntry {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.y.to_css(dest)?;
+ dest.write_char(' ')?;
+ Percentage(self.x).to_css(dest)
+ }
+}
+
+/// Representation of a piecewise linear function, a series of linear functions.
+#[derive(
+ Default,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToResolvedValue,
+ ToCss,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[repr(C)]
+#[css(comma)]
+pub struct PiecewiseLinearFunction {
+ #[css(iterable)]
+ #[ignore_malloc_size_of = "Arc"]
+ #[shmem(field_bound)]
+ entries: crate::ArcSlice<PiecewiseLinearFunctionEntry>,
+}
+
+/// Parameters to define one linear stop.
+pub type PiecewiseLinearFunctionBuildParameters = (CSSFloat, Option<CSSFloat>);
+
+impl PiecewiseLinearFunction {
+ /// Interpolate y value given x and two points. The linear function will be rooted at the asymptote.
+ fn interpolate(
+ x: ValueType,
+ prev: PiecewiseLinearFunctionEntry,
+ next: PiecewiseLinearFunctionEntry,
+ asymptote: &PiecewiseLinearFunctionEntry,
+ ) -> ValueType {
+ // Short circuit if the x is on prev or next.
+ // `next` point is preferred as per spec.
+ if x.approx_eq(&next.x) {
+ return next.y;
+ }
+ if x.approx_eq(&prev.x) {
+ return prev.y;
+ }
+ // Avoid division by zero.
+ if prev.x.approx_eq(&next.x) {
+ return next.y;
+ }
+ let slope = (next.y - prev.y) / (next.x - prev.x);
+ return slope * (x - asymptote.x) + asymptote.y;
+ }
+
+ /// Get the y value of the piecewise linear function given the x value, as per
+ /// https://drafts.csswg.org/css-easing-2/#linear-easing-function-output
+ pub fn at(&self, x: ValueType) -> ValueType {
+ if !x.is_finite() {
+ return if x > 0.0 { 1.0 } else { 0.0 };
+ }
+ if self.entries.is_empty() {
+ // Implied y = x, as per spec.
+ return x;
+ }
+ if self.entries.len() == 1 {
+ // Implied y = <constant>, as per spec.
+ return self.entries[0].y;
+ }
+ // Spec dictates the valid input domain is [0, 1]. Outside of this range, the output
+ // should be calculated as if the slopes at start and end extend to infinity. However, if the
+ // start/end have two points of the same position, the line should extend along the x-axis.
+ // The function doesn't have to cover the input domain, in which case the extension logic
+ // applies even if the input falls in the input domain.
+ // Also, we're guaranteed to have at least two elements at this point.
+ if x < self.entries[0].x {
+ return Self::interpolate(x, self.entries[0], self.entries[1], &self.entries[0]);
+ }
+ let mut rev_iter = self.entries.iter().rev();
+ let last = rev_iter.next().unwrap();
+ if x >= last.x {
+ let second_last = rev_iter.next().unwrap();
+ return Self::interpolate(x, *second_last, *last, last);
+ }
+
+ // Now we know the input sits within the domain explicitly defined by our function.
+ for (point_b, point_a) in self.entries.iter().rev().tuple_windows() {
+ // Need to let point A be the _last_ point where its x is less than the input x,
+ // hence the reverse traversal.
+ if x < point_a.x {
+ continue;
+ }
+ return Self::interpolate(x, *point_a, *point_b, point_a);
+ }
+ unreachable!("Input is supposed to be within the entries' min & max!");
+ }
+
+ #[allow(missing_docs)]
+ pub fn iter(&self) -> Iter<PiecewiseLinearFunctionEntry> {
+ self.entries.iter()
+ }
+}
+
+/// Entry of a piecewise linear function while building, where the calculation of x value can be deferred.
+#[derive(Clone, Copy)]
+struct BuildEntry {
+ x: Option<ValueType>,
+ y: ValueType,
+}
+
+/// Builder object to generate a linear function.
+#[derive(Default)]
+pub struct PiecewiseLinearFunctionBuilder {
+ largest_x: Option<ValueType>,
+ smallest_x: Option<ValueType>,
+ entries: Vec<BuildEntry>,
+}
+
+impl PiecewiseLinearFunctionBuilder {
+ /// Create a builder for a known amount of linear stop entries.
+ pub fn with_capacity(len: usize) -> Self {
+ PiecewiseLinearFunctionBuilder {
+ largest_x: None,
+ smallest_x: None,
+ entries: Vec::with_capacity(len),
+ }
+ }
+
+ fn create_entry(&mut self, y: ValueType, x: Option<ValueType>) {
+ let x = match x {
+ Some(x) if x.is_finite() => x,
+ _ if self.entries.is_empty() => 0.0, // First x is 0 if not specified (Or not finite)
+ _ => {
+ self.entries.push(BuildEntry { x: None, y });
+ return;
+ },
+ };
+ // Specified x value cannot regress, as per spec.
+ let x = match self.largest_x {
+ Some(largest_x) => x.max(largest_x),
+ None => x,
+ };
+ self.largest_x = Some(x);
+ // Whatever we see the earliest is the smallest value.
+ if self.smallest_x.is_none() {
+ self.smallest_x = Some(x);
+ }
+ self.entries.push(BuildEntry { x: Some(x), y });
+ }
+
+ /// Add a new entry into the piecewise linear function with specified y value.
+ /// If the start x value is given, that is where the x value will be. Otherwise,
+ /// the x value is calculated later. If the end x value is specified, a flat segment
+ /// is generated. If start x value is not specified but end x is, it is treated as
+ /// start x.
+ pub fn push(&mut self, y: CSSFloat, x_start: Option<CSSFloat>) {
+ self.create_entry(y, x_start)
+ }
+
+ /// Finish building the piecewise linear function by resolving all undefined x values,
+ /// then return the result.
+ pub fn build(mut self) -> PiecewiseLinearFunction {
+ if self.entries.is_empty() {
+ return PiecewiseLinearFunction::default();
+ }
+ if self.entries.len() == 1 {
+ // Don't bother resolving anything.
+ return PiecewiseLinearFunction {
+ entries: crate::ArcSlice::from_iter(std::iter::once(
+ PiecewiseLinearFunctionEntry {
+ x: 0.,
+ y: self.entries[0].y,
+ },
+ )),
+ };
+ }
+ // Guaranteed at least two elements.
+ // Start element's x value should've been assigned when the first value was pushed.
+ debug_assert!(
+ self.entries[0].x.is_some(),
+ "Expected an entry with x defined!"
+ );
+ // Spec asserts that if the last entry does not have an x value, it is assigned the largest seen x value.
+ self.entries
+ .last_mut()
+ .unwrap()
+ .x
+ .get_or_insert(self.largest_x.filter(|x| x > &1.0).unwrap_or(1.0));
+ // Now we have at least two elements with x values, with start & end x values guaranteed.
+
+ let mut result = Vec::with_capacity(self.entries.len());
+ result.push(PiecewiseLinearFunctionEntry {
+ x: self.entries[0].x.unwrap(),
+ y: self.entries[0].y,
+ });
+ for (i, e) in self.entries.iter().enumerate().skip(1) {
+ if e.x.is_none() {
+ // Need to calculate x values by first finding an entry with the first
+ // defined x value (Guaranteed to exist as the list end has it defined).
+ continue;
+ }
+ // x is defined for this element.
+ let divisor = i - result.len() + 1;
+ // Any element(s) with undefined x to assign?
+ if divisor != 1 {
+ // Have at least one element in result at all times.
+ let start_x = result.last().unwrap().x;
+ let increment = (e.x.unwrap() - start_x) / divisor as ValueType;
+ // Grab every element with undefined x to this point, which starts at the end of the result
+ // array, and ending right before the current index. Then, assigned the evenly divided
+ // x values.
+ result.extend(
+ self.entries[result.len()..i]
+ .iter()
+ .enumerate()
+ .map(|(j, e)| {
+ debug_assert!(e.x.is_none(), "Expected an entry with x undefined!");
+ PiecewiseLinearFunctionEntry {
+ x: increment * (j + 1) as ValueType + start_x,
+ y: e.y,
+ }
+ }),
+ );
+ }
+ result.push(PiecewiseLinearFunctionEntry {
+ x: e.x.unwrap(),
+ y: e.y,
+ });
+ }
+ debug_assert_eq!(
+ result.len(),
+ self.entries.len(),
+ "Should've mapped one-to-one!"
+ );
+ PiecewiseLinearFunction {
+ entries: crate::ArcSlice::from_iter(result.into_iter()),
+ }
+ }
+}
diff --git a/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl
new file mode 100644
index 0000000000..9593025a47
--- /dev/null
+++ b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl
Binary files differ
diff --git a/servo/components/style/properties/build.py b/servo/components/style/properties/build.py
new file mode 100644
index 0000000000..6c3ee0cf66
--- /dev/null
+++ b/servo/components/style/properties/build.py
@@ -0,0 +1,176 @@
+# 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 json
+import os.path
+import re
+import sys
+
+BASE = os.path.dirname(__file__.replace("\\", "/"))
+sys.path.insert(0, os.path.join(BASE, "Mako-1.1.2-py2.py3-none-any.whl"))
+sys.path.insert(0, BASE) # For importing `data.py`
+
+from mako import exceptions
+from mako.lookup import TemplateLookup
+from mako.template import Template
+
+import data
+
+RE_PYTHON_ADDR = re.compile(r"<.+? object at 0x[0-9a-fA-F]+>")
+
+OUT_DIR = os.environ.get("OUT_DIR", "")
+
+STYLE_STRUCT_LIST = [
+ "background",
+ "border",
+ "box",
+ "column",
+ "counters",
+ "effects",
+ "font",
+ "inherited_box",
+ "inherited_svg",
+ "inherited_table",
+ "inherited_text",
+ "inherited_ui",
+ "list",
+ "margin",
+ "outline",
+ "page",
+ "padding",
+ "position",
+ "svg",
+ "table",
+ "text",
+ "ui",
+ "xul",
+]
+
+
+def main():
+ usage = (
+ "Usage: %s [ servo-2013 | servo-2020 | gecko ] [ style-crate | geckolib <template> | html ]"
+ % sys.argv[0]
+ )
+ if len(sys.argv) < 3:
+ abort(usage)
+ engine = sys.argv[1]
+ output = sys.argv[2]
+
+ if engine not in ["servo-2013", "servo-2020", "gecko"] or output not in [
+ "style-crate",
+ "geckolib",
+ "html",
+ ]:
+ abort(usage)
+
+ properties = data.PropertiesData(engine=engine)
+ files = {}
+ for kind in ["longhands", "shorthands"]:
+ files[kind] = {}
+ for struct in STYLE_STRUCT_LIST:
+ file_name = os.path.join(BASE, kind, "{}.mako.rs".format(struct))
+ if kind == "shorthands" and not os.path.exists(file_name):
+ files[kind][struct] = ""
+ continue
+ files[kind][struct] = render(
+ file_name,
+ engine=engine,
+ data=properties,
+ )
+ properties_template = os.path.join(BASE, "properties.mako.rs")
+ files["properties"] = render(
+ properties_template,
+ engine=engine,
+ data=properties,
+ __file__=properties_template,
+ OUT_DIR=OUT_DIR,
+ )
+ if output == "style-crate":
+ write(OUT_DIR, "properties.rs", files["properties"])
+ for kind in ["longhands", "shorthands"]:
+ for struct in files[kind]:
+ write(
+ os.path.join(OUT_DIR, kind),
+ "{}.rs".format(struct),
+ files[kind][struct],
+ )
+
+ if engine == "gecko":
+ template = os.path.join(BASE, "gecko.mako.rs")
+ rust = render(template, data=properties)
+ write(OUT_DIR, "gecko_properties.rs", rust)
+
+ if engine in ["servo-2013", "servo-2020"]:
+ if engine == "servo-2013":
+ pref_attr = "servo_2013_pref"
+ if engine == "servo-2020":
+ pref_attr = "servo_2020_pref"
+ properties_dict = {
+ kind: {
+ p.name: {"pref": getattr(p, pref_attr)}
+ for prop in properties_list
+ if prop.enabled_in_content()
+ for p in [prop] + prop.alias
+ }
+ for kind, properties_list in [
+ ("longhands", properties.longhands),
+ ("shorthands", properties.shorthands),
+ ]
+ }
+ as_html = render(
+ os.path.join(BASE, "properties.html.mako"), properties=properties_dict
+ )
+ as_json = json.dumps(properties_dict, indent=4, sort_keys=True)
+ doc_servo = os.path.join(BASE, "..", "..", "..", "target", "doc", "servo")
+ write(doc_servo, "css-properties.html", as_html)
+ write(doc_servo, "css-properties.json", as_json)
+ write(OUT_DIR, "css-properties.json", as_json)
+ elif output == "geckolib":
+ if len(sys.argv) < 4:
+ abort(usage)
+ template = sys.argv[3]
+ header = render(template, data=properties)
+ sys.stdout.write(header)
+
+
+def abort(message):
+ print(message, file=sys.stderr)
+ sys.exit(1)
+
+
+def render(filename, **context):
+ try:
+ lookup = TemplateLookup(
+ directories=[BASE], input_encoding="utf8", strict_undefined=True
+ )
+ template = Template(
+ open(filename, "rb").read(),
+ filename=filename,
+ input_encoding="utf8",
+ lookup=lookup,
+ strict_undefined=True,
+ )
+ # Uncomment to debug generated Python code:
+ # write("/tmp", "mako_%s.py" % os.path.basename(filename), template.code)
+ return template.render(**context)
+ except Exception:
+ # Uncomment to see a traceback in generated Python code:
+ # raise
+ abort(exceptions.text_error_template().render())
+
+
+def write(directory, filename, content):
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ full_path = os.path.join(directory, filename)
+ open(full_path, "w", encoding="utf-8").write(content)
+
+ python_addr = RE_PYTHON_ADDR.search(content)
+ if python_addr:
+ abort('Found "{}" in {} ({})'.format(python_addr.group(0), filename, full_path))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/servo/components/style/properties/cascade.rs b/servo/components/style/properties/cascade.rs
new file mode 100644
index 0000000000..59a8a65876
--- /dev/null
+++ b/servo/components/style/properties/cascade.rs
@@ -0,0 +1,1381 @@
+/* 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/. */
+
+//! The main cascading algorithm of the style system.
+
+use crate::applicable_declarations::CascadePriority;
+use crate::color::AbsoluteColor;
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::custom_properties::{
+ CustomPropertiesBuilder, DeferFontRelativeCustomPropertyResolution,
+};
+use crate::dom::TElement;
+use crate::font_metrics::FontMetricsOrientation;
+use crate::logical_geometry::WritingMode;
+use crate::properties::{
+ property_counts, CSSWideKeyword, ComputedValues, DeclarationImportanceIterator, Importance,
+ LonghandId, LonghandIdSet, PrioritaryPropertyId, PropertyDeclaration, PropertyDeclarationId,
+ PropertyFlags, ShorthandsWithPropertyReferencesCache, StyleBuilder, CASCADE_PROPERTY,
+};
+use crate::rule_cache::{RuleCache, RuleCacheConditions};
+use crate::rule_tree::{CascadeLevel, StrongRuleNode};
+use crate::selector_parser::PseudoElement;
+use crate::shared_lock::StylesheetGuards;
+use crate::style_adjuster::StyleAdjuster;
+use crate::stylesheets::container_rule::ContainerSizeQuery;
+use crate::stylesheets::{layer_rule::LayerOrder, Origin};
+use crate::stylist::Stylist;
+use crate::values::specified::length::FontBaseSize;
+use crate::values::{computed, specified};
+use fxhash::FxHashMap;
+use servo_arc::Arc;
+use smallvec::SmallVec;
+use std::borrow::Cow;
+use std::mem;
+
+/// Whether we're resolving a style with the purposes of reparenting for ::first-line.
+#[derive(Copy, Clone)]
+#[allow(missing_docs)]
+pub enum FirstLineReparenting<'a> {
+ No,
+ Yes {
+ /// The style we're re-parenting for ::first-line. ::first-line only affects inherited
+ /// properties so we use this to avoid some work and also ensure correctness by copying the
+ /// reset structs from this style.
+ style_to_reparent: &'a ComputedValues,
+ },
+}
+
+/// Performs the CSS cascade, computing new styles for an element from its parent style.
+///
+/// The arguments are:
+///
+/// * `device`: Used to get the initial viewport and other external state.
+///
+/// * `rule_node`: The rule node in the tree that represent the CSS rules that
+/// matched.
+///
+/// * `parent_style`: The parent style, if applicable; if `None`, this is the root node.
+///
+/// Returns the computed values.
+/// * `flags`: Various flags.
+///
+pub fn cascade<E>(
+ stylist: &Stylist,
+ pseudo: Option<&PseudoElement>,
+ rule_node: &StrongRuleNode,
+ guards: &StylesheetGuards,
+ parent_style: Option<&ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ first_line_reparenting: FirstLineReparenting,
+ visited_rules: Option<&StrongRuleNode>,
+ cascade_input_flags: ComputedValueFlags,
+ rule_cache: Option<&RuleCache>,
+ rule_cache_conditions: &mut RuleCacheConditions,
+ element: Option<E>,
+) -> Arc<ComputedValues>
+where
+ E: TElement,
+{
+ cascade_rules(
+ stylist,
+ pseudo,
+ rule_node,
+ guards,
+ parent_style,
+ layout_parent_style,
+ first_line_reparenting,
+ CascadeMode::Unvisited { visited_rules },
+ cascade_input_flags,
+ rule_cache,
+ rule_cache_conditions,
+ element,
+ )
+}
+
+struct DeclarationIterator<'a> {
+ // Global to the iteration.
+ guards: &'a StylesheetGuards<'a>,
+ restriction: Option<PropertyFlags>,
+ // The rule we're iterating over.
+ current_rule_node: Option<&'a StrongRuleNode>,
+ // Per rule state.
+ declarations: DeclarationImportanceIterator<'a>,
+ origin: Origin,
+ importance: Importance,
+ priority: CascadePriority,
+}
+
+impl<'a> DeclarationIterator<'a> {
+ #[inline]
+ fn new(
+ rule_node: &'a StrongRuleNode,
+ guards: &'a StylesheetGuards,
+ pseudo: Option<&PseudoElement>,
+ ) -> Self {
+ let restriction = pseudo.and_then(|p| p.property_restriction());
+ let mut iter = Self {
+ guards,
+ current_rule_node: Some(rule_node),
+ origin: Origin::UserAgent,
+ importance: Importance::Normal,
+ priority: CascadePriority::new(CascadeLevel::UANormal, LayerOrder::root()),
+ declarations: DeclarationImportanceIterator::default(),
+ restriction,
+ };
+ iter.update_for_node(rule_node);
+ iter
+ }
+
+ fn update_for_node(&mut self, node: &'a StrongRuleNode) {
+ self.priority = node.cascade_priority();
+ let level = self.priority.cascade_level();
+ self.origin = level.origin();
+ self.importance = level.importance();
+ let guard = match self.origin {
+ Origin::Author => self.guards.author,
+ Origin::User | Origin::UserAgent => self.guards.ua_or_user,
+ };
+ self.declarations = match node.style_source() {
+ Some(source) => source.read(guard).declaration_importance_iter(),
+ None => DeclarationImportanceIterator::default(),
+ };
+ }
+}
+
+impl<'a> Iterator for DeclarationIterator<'a> {
+ type Item = (&'a PropertyDeclaration, CascadePriority);
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ if let Some((decl, importance)) = self.declarations.next_back() {
+ if self.importance != importance {
+ continue;
+ }
+
+ if let Some(restriction) = self.restriction {
+ // decl.id() is either a longhand or a custom
+ // property. Custom properties are always allowed, but
+ // longhands are only allowed if they have our
+ // restriction flag set.
+ if let PropertyDeclarationId::Longhand(id) = decl.id() {
+ if !id.flags().contains(restriction) && self.origin != Origin::UserAgent {
+ continue;
+ }
+ }
+ }
+
+ return Some((decl, self.priority));
+ }
+
+ let next_node = self.current_rule_node.take()?.parent()?;
+ self.current_rule_node = Some(next_node);
+ self.update_for_node(next_node);
+ }
+ }
+}
+
+fn cascade_rules<E>(
+ stylist: &Stylist,
+ pseudo: Option<&PseudoElement>,
+ rule_node: &StrongRuleNode,
+ guards: &StylesheetGuards,
+ parent_style: Option<&ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ first_line_reparenting: FirstLineReparenting,
+ cascade_mode: CascadeMode,
+ cascade_input_flags: ComputedValueFlags,
+ rule_cache: Option<&RuleCache>,
+ rule_cache_conditions: &mut RuleCacheConditions,
+ element: Option<E>,
+) -> Arc<ComputedValues>
+where
+ E: TElement,
+{
+ apply_declarations(
+ stylist,
+ pseudo,
+ rule_node,
+ guards,
+ DeclarationIterator::new(rule_node, guards, pseudo),
+ parent_style,
+ layout_parent_style,
+ first_line_reparenting,
+ cascade_mode,
+ cascade_input_flags,
+ rule_cache,
+ rule_cache_conditions,
+ element,
+ )
+}
+
+/// Whether we're cascading for visited or unvisited styles.
+#[derive(Clone, Copy)]
+pub enum CascadeMode<'a, 'b> {
+ /// We're cascading for unvisited styles.
+ Unvisited {
+ /// The visited rules that should match the visited style.
+ visited_rules: Option<&'a StrongRuleNode>,
+ },
+ /// We're cascading for visited styles.
+ Visited {
+ /// The cascade for our unvisited style.
+ unvisited_context: &'a computed::Context<'b>,
+ },
+}
+
+fn iter_declarations<'builder, 'decls: 'builder>(
+ iter: impl Iterator<Item = (&'decls PropertyDeclaration, CascadePriority)>,
+ declarations: &mut Declarations<'decls>,
+ mut custom_builder: Option<&mut CustomPropertiesBuilder<'builder, 'decls>>,
+) {
+ for (declaration, priority) in iter {
+ if let PropertyDeclaration::Custom(ref declaration) = *declaration {
+ if let Some(ref mut builder) = custom_builder {
+ builder.cascade(declaration, priority);
+ }
+ } else {
+ let id = declaration.id().as_longhand().unwrap();
+ declarations.note_declaration(declaration, priority, id);
+ if let Some(ref mut builder) = custom_builder {
+ if let PropertyDeclaration::WithVariables(ref v) = declaration {
+ builder.note_potentially_cyclic_non_custom_dependency(id, v);
+ }
+ }
+ }
+ }
+}
+
+/// NOTE: This function expects the declaration with more priority to appear
+/// first.
+pub fn apply_declarations<'a, E, I>(
+ stylist: &'a Stylist,
+ pseudo: Option<&'a PseudoElement>,
+ rules: &StrongRuleNode,
+ guards: &StylesheetGuards,
+ iter: I,
+ parent_style: Option<&'a ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ first_line_reparenting: FirstLineReparenting<'a>,
+ cascade_mode: CascadeMode,
+ cascade_input_flags: ComputedValueFlags,
+ rule_cache: Option<&'a RuleCache>,
+ rule_cache_conditions: &'a mut RuleCacheConditions,
+ element: Option<E>,
+) -> Arc<ComputedValues>
+where
+ E: TElement + 'a,
+ I: Iterator<Item = (&'a PropertyDeclaration, CascadePriority)>,
+{
+ debug_assert!(layout_parent_style.is_none() || parent_style.is_some());
+ let device = stylist.device();
+ let inherited_style = parent_style.unwrap_or(device.default_computed_values());
+ let is_root_element = pseudo.is_none() && element.map_or(false, |e| e.is_root());
+
+ let container_size_query =
+ ContainerSizeQuery::for_option_element(element, Some(inherited_style), pseudo.is_some());
+
+ let mut context = computed::Context::new(
+ // We'd really like to own the rules here to avoid refcount traffic, but
+ // animation's usage of `apply_declarations` make this tricky. See bug
+ // 1375525.
+ StyleBuilder::new(
+ device,
+ Some(stylist),
+ parent_style,
+ pseudo,
+ Some(rules.clone()),
+ is_root_element,
+ ),
+ stylist.quirks_mode(),
+ rule_cache_conditions,
+ container_size_query,
+ );
+
+ context.style().add_flags(cascade_input_flags);
+
+ let using_cached_reset_properties;
+ let ignore_colors = !context.builder.device.use_document_colors();
+ let mut cascade = Cascade::new(first_line_reparenting, ignore_colors);
+ let mut declarations = Default::default();
+ let mut shorthand_cache = ShorthandsWithPropertyReferencesCache::default();
+ let properties_to_apply = match cascade_mode {
+ CascadeMode::Visited { unvisited_context } => {
+ context.builder.custom_properties = unvisited_context.builder.custom_properties.clone();
+ context.builder.writing_mode = unvisited_context.builder.writing_mode;
+ // We never insert visited styles into the cache so we don't need to try looking it up.
+ // It also wouldn't be super-profitable, only a handful :visited properties are
+ // non-inherited.
+ using_cached_reset_properties = false;
+ // TODO(bug 1859385): If we match the same rules when visited and unvisited, we could
+ // try to avoid gathering the declarations. That'd be:
+ // unvisited_context.builder.rules.as_ref() == Some(rules)
+ iter_declarations(iter, &mut declarations, None);
+
+ LonghandIdSet::visited_dependent()
+ },
+ CascadeMode::Unvisited { visited_rules } => {
+ let deferred_custom_properties = {
+ let mut builder = CustomPropertiesBuilder::new(stylist, &mut context);
+ iter_declarations(iter, &mut declarations, Some(&mut builder));
+ // Detect cycles, remove properties participating in them, and resolve properties, except:
+ // * Registered custom properties that depend on font-relative properties (Resolved)
+ // when prioritary properties are resolved), and
+ // * Any property that, in turn, depend on properties like above.
+ builder.build(DeferFontRelativeCustomPropertyResolution::Yes)
+ };
+
+ // Resolve prioritary properties - Guaranteed to not fall into a cycle with existing custom
+ // properties.
+ cascade.apply_prioritary_properties(&mut context, &declarations, &mut shorthand_cache);
+
+ // Resolve the deferred custom properties.
+ if let Some(deferred) = deferred_custom_properties {
+ CustomPropertiesBuilder::build_deferred(deferred, stylist, &mut context);
+ }
+
+ if let Some(visited_rules) = visited_rules {
+ cascade.compute_visited_style_if_needed(
+ &mut context,
+ element,
+ parent_style,
+ layout_parent_style,
+ visited_rules,
+ guards,
+ );
+ }
+
+ using_cached_reset_properties = cascade.try_to_use_cached_reset_properties(
+ &mut context.builder,
+ rule_cache,
+ guards,
+ );
+
+ if using_cached_reset_properties {
+ LonghandIdSet::late_group_only_inherited()
+ } else {
+ LonghandIdSet::late_group()
+ }
+ },
+ };
+
+ cascade.apply_non_prioritary_properties(
+ &mut context,
+ &declarations.longhand_declarations,
+ &mut shorthand_cache,
+ &properties_to_apply,
+ );
+
+ cascade.finished_applying_properties(&mut context.builder);
+
+ std::mem::drop(cascade);
+
+ context.builder.clear_modified_reset();
+
+ if matches!(cascade_mode, CascadeMode::Unvisited { .. }) {
+ StyleAdjuster::new(&mut context.builder)
+ .adjust(layout_parent_style.unwrap_or(inherited_style), element);
+ }
+
+ if context.builder.modified_reset() || using_cached_reset_properties {
+ // If we adjusted any reset structs, we can't cache this ComputedValues.
+ //
+ // Also, if we re-used existing reset structs, don't bother caching it back again. (Aside
+ // from being wasted effort, it will be wrong, since context.rule_cache_conditions won't be
+ // set appropriately if we didn't compute those reset properties.)
+ context.rule_cache_conditions.borrow_mut().set_uncacheable();
+ }
+
+ context.builder.build()
+}
+
+/// For ignored colors mode, we sometimes want to do something equivalent to
+/// "revert-or-initial", where we `revert` for a given origin, but then apply a
+/// given initial value if nothing in other origins did override it.
+///
+/// This is a bit of a clunky way of achieving this.
+type DeclarationsToApplyUnlessOverriden = SmallVec<[PropertyDeclaration; 2]>;
+
+fn tweak_when_ignoring_colors(
+ context: &computed::Context,
+ longhand_id: LonghandId,
+ origin: Origin,
+ declaration: &mut Cow<PropertyDeclaration>,
+ declarations_to_apply_unless_overridden: &mut DeclarationsToApplyUnlessOverriden,
+) {
+ use crate::values::computed::ToComputedValue;
+ use crate::values::specified::Color;
+
+ if !longhand_id.ignored_when_document_colors_disabled() {
+ return;
+ }
+
+ let is_ua_or_user_rule = matches!(origin, Origin::User | Origin::UserAgent);
+ if is_ua_or_user_rule {
+ return;
+ }
+
+ // Always honor colors if forced-color-adjust is set to none.
+ let forced = context
+ .builder
+ .get_inherited_text()
+ .clone_forced_color_adjust();
+ if forced == computed::ForcedColorAdjust::None {
+ return;
+ }
+
+ // Don't override background-color on ::-moz-color-swatch. It is set as an
+ // author style (via the style attribute), but it's pretty important for it
+ // to show up for obvious reasons :)
+ if context
+ .builder
+ .pseudo
+ .map_or(false, |p| p.is_color_swatch()) &&
+ longhand_id == LonghandId::BackgroundColor
+ {
+ return;
+ }
+
+ fn alpha_channel(color: &Color, context: &computed::Context) -> f32 {
+ // We assume here currentColor is opaque.
+ color
+ .to_computed_value(context)
+ .resolve_to_absolute(&AbsoluteColor::BLACK)
+ .alpha
+ }
+
+ // A few special-cases ahead.
+ match **declaration {
+ PropertyDeclaration::BackgroundColor(ref color) => {
+ // We honor system colors and transparent colors unconditionally.
+ //
+ // NOTE(emilio): We honor transparent unconditionally, like we do
+ // for color, even though it causes issues like bug 1625036. The
+ // reasoning is that the conditions that trigger that (having
+ // mismatched widget and default backgrounds) are both uncommon, and
+ // broken in other applications as well, and not honoring
+ // transparent makes stuff uglier or break unconditionally
+ // (bug 1666059, bug 1755713).
+ if color.honored_in_forced_colors_mode(/* allow_transparent = */ true) {
+ return;
+ }
+ // For background-color, we revert or initial-with-preserved-alpha
+ // otherwise, this is needed to preserve semi-transparent
+ // backgrounds.
+ let alpha = alpha_channel(color, context);
+ if alpha == 0.0 {
+ return;
+ }
+ let mut color = context.builder.device.default_background_color();
+ color.alpha = alpha;
+ declarations_to_apply_unless_overridden
+ .push(PropertyDeclaration::BackgroundColor(color.into()))
+ },
+ PropertyDeclaration::Color(ref color) => {
+ // We honor color: transparent and system colors.
+ if color
+ .0
+ .honored_in_forced_colors_mode(/* allow_transparent = */ true)
+ {
+ return;
+ }
+ // If the inherited color would be transparent, but we would
+ // override this with a non-transparent color, then override it with
+ // the default color. Otherwise just let it inherit through.
+ if context
+ .builder
+ .get_parent_inherited_text()
+ .clone_color()
+ .alpha ==
+ 0.0
+ {
+ let color = context.builder.device.default_color();
+ declarations_to_apply_unless_overridden.push(PropertyDeclaration::Color(
+ specified::ColorPropertyValue(color.into()),
+ ))
+ }
+ },
+ // We honor url background-images if backplating.
+ #[cfg(feature = "gecko")]
+ PropertyDeclaration::BackgroundImage(ref bkg) => {
+ use crate::values::generics::image::Image;
+ if static_prefs::pref!("browser.display.permit_backplate") {
+ if bkg
+ .0
+ .iter()
+ .all(|image| matches!(*image, Image::Url(..) | Image::None))
+ {
+ return;
+ }
+ }
+ },
+ _ => {
+ // We honor system colors more generally for all colors.
+ //
+ // We used to honor transparent but that causes accessibility
+ // regressions like bug 1740924.
+ //
+ // NOTE(emilio): This doesn't handle caret-color and accent-color
+ // because those use a slightly different syntax (<color> | auto for
+ // example).
+ //
+ // That's probably fine though, as using a system color for
+ // caret-color doesn't make sense (using currentColor is fine), and
+ // we ignore accent-color in high-contrast-mode anyways.
+ if let Some(color) = declaration.color_value() {
+ if color.honored_in_forced_colors_mode(/* allow_transparent = */ false) {
+ return;
+ }
+ }
+ },
+ }
+
+ *declaration.to_mut() =
+ PropertyDeclaration::css_wide_keyword(longhand_id, CSSWideKeyword::Revert);
+}
+
+/// We track the index only for prioritary properties. For other properties we can just iterate.
+type DeclarationIndex = u16;
+
+/// "Prioritary" properties are properties that other properties depend on in one way or another.
+///
+/// We keep track of their position in the declaration vector, in order to be able to cascade them
+/// separately in precise order.
+#[derive(Copy, Clone)]
+struct PrioritaryDeclarationPosition {
+ // DeclarationIndex::MAX signals no index.
+ most_important: DeclarationIndex,
+ least_important: DeclarationIndex,
+}
+
+impl Default for PrioritaryDeclarationPosition {
+ fn default() -> Self {
+ Self {
+ most_important: DeclarationIndex::MAX,
+ least_important: DeclarationIndex::MAX,
+ }
+ }
+}
+
+#[derive(Copy, Clone)]
+struct Declaration<'a> {
+ decl: &'a PropertyDeclaration,
+ priority: CascadePriority,
+ next_index: DeclarationIndex,
+}
+
+/// The set of property declarations from our rules.
+#[derive(Default)]
+struct Declarations<'a> {
+ /// Whether we have any prioritary property. This is just a minor optimization.
+ has_prioritary_properties: bool,
+ /// A list of all the applicable longhand declarations.
+ longhand_declarations: SmallVec<[Declaration<'a>; 32]>,
+ /// The prioritary property position data.
+ prioritary_positions: [PrioritaryDeclarationPosition; property_counts::PRIORITARY],
+}
+
+impl<'a> Declarations<'a> {
+ fn note_prioritary_property(&mut self, id: PrioritaryPropertyId) {
+ let new_index = self.longhand_declarations.len();
+ if new_index >= DeclarationIndex::MAX as usize {
+ // This prioritary property is past the amount of declarations we can track. Let's give
+ // up applying it to prevent getting confused.
+ return;
+ }
+
+ self.has_prioritary_properties = true;
+ let new_index = new_index as DeclarationIndex;
+ let position = &mut self.prioritary_positions[id as usize];
+ if position.most_important == DeclarationIndex::MAX {
+ // We still haven't seen this property, record the current position as the most
+ // prioritary index.
+ position.most_important = new_index;
+ } else {
+ // Let the previous item in the list know about us.
+ self.longhand_declarations[position.least_important as usize].next_index = new_index;
+ }
+ position.least_important = new_index;
+ }
+
+ fn note_declaration(
+ &mut self,
+ decl: &'a PropertyDeclaration,
+ priority: CascadePriority,
+ id: LonghandId,
+ ) {
+ if let Some(id) = PrioritaryPropertyId::from_longhand(id) {
+ self.note_prioritary_property(id);
+ }
+ self.longhand_declarations.push(Declaration {
+ decl,
+ priority,
+ next_index: 0,
+ });
+ }
+}
+
+struct Cascade<'b> {
+ first_line_reparenting: FirstLineReparenting<'b>,
+ ignore_colors: bool,
+ seen: LonghandIdSet,
+ author_specified: LonghandIdSet,
+ reverted_set: LonghandIdSet,
+ reverted: FxHashMap<LonghandId, (CascadePriority, bool)>,
+ declarations_to_apply_unless_overridden: DeclarationsToApplyUnlessOverriden,
+}
+
+impl<'b> Cascade<'b> {
+ fn new(first_line_reparenting: FirstLineReparenting<'b>, ignore_colors: bool) -> Self {
+ Self {
+ first_line_reparenting,
+ ignore_colors,
+ seen: LonghandIdSet::default(),
+ author_specified: LonghandIdSet::default(),
+ reverted_set: Default::default(),
+ reverted: Default::default(),
+ declarations_to_apply_unless_overridden: Default::default(),
+ }
+ }
+
+ fn substitute_variables_if_needed<'cache, 'decl>(
+ &self,
+ context: &mut computed::Context,
+ shorthand_cache: &'cache mut ShorthandsWithPropertyReferencesCache,
+ declaration: &'decl PropertyDeclaration,
+ ) -> Cow<'decl, PropertyDeclaration>
+ where
+ 'cache: 'decl,
+ {
+ let declaration = match *declaration {
+ PropertyDeclaration::WithVariables(ref declaration) => declaration,
+ ref d => return Cow::Borrowed(d),
+ };
+
+ if !declaration.id.inherited() {
+ context.rule_cache_conditions.borrow_mut().set_uncacheable();
+
+ // NOTE(emilio): We only really need to add the `display` /
+ // `content` flag if the CSS variable has not been specified on our
+ // declarations, but we don't have that information at this point,
+ // and it doesn't seem like an important enough optimization to
+ // warrant it.
+ match declaration.id {
+ LonghandId::Display => {
+ context
+ .builder
+ .add_flags(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE);
+ },
+ LonghandId::Content => {
+ context
+ .builder
+ .add_flags(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE);
+ },
+ _ => {},
+ }
+ }
+
+ debug_assert!(
+ context.builder.stylist.is_some(),
+ "Need a Stylist to substitute variables!"
+ );
+ declaration.value.substitute_variables(
+ declaration.id,
+ context.builder.custom_properties(),
+ context.builder.stylist.unwrap(),
+ context,
+ shorthand_cache,
+ )
+ }
+
+ fn apply_one_prioritary_property(
+ &mut self,
+ context: &mut computed::Context,
+ decls: &Declarations,
+ cache: &mut ShorthandsWithPropertyReferencesCache,
+ id: PrioritaryPropertyId,
+ ) -> bool {
+ let mut index = decls.prioritary_positions[id as usize].most_important;
+ if index == DeclarationIndex::MAX {
+ return false;
+ }
+
+ let longhand_id = id.to_longhand();
+ debug_assert!(
+ !longhand_id.is_logical(),
+ "That could require more book-keeping"
+ );
+ loop {
+ let decl = decls.longhand_declarations[index as usize];
+ self.apply_one_longhand(context, longhand_id, decl.decl, decl.priority, cache);
+ if self.seen.contains(longhand_id) {
+ return true; // Common case, we're done.
+ }
+ debug_assert!(
+ self.reverted_set.contains(longhand_id),
+ "How else can we fail to apply a prioritary property?"
+ );
+ debug_assert!(
+ decl.next_index == 0 || decl.next_index > index,
+ "should make progress! {} -> {}",
+ index,
+ decl.next_index,
+ );
+ index = decl.next_index;
+ if index == 0 {
+ break;
+ }
+ }
+ false
+ }
+
+ fn apply_prioritary_properties(
+ &mut self,
+ context: &mut computed::Context,
+ decls: &Declarations,
+ cache: &mut ShorthandsWithPropertyReferencesCache,
+ ) {
+ // Keeps apply_one_prioritary_property calls readable, considering the repititious
+ // arguments.
+ macro_rules! apply {
+ ($prop:ident) => {
+ self.apply_one_prioritary_property(
+ context,
+ decls,
+ cache,
+ PrioritaryPropertyId::$prop,
+ )
+ };
+ }
+
+ if !decls.has_prioritary_properties {
+ return;
+ }
+
+ let has_writing_mode = apply!(WritingMode) | apply!(Direction) | apply!(TextOrientation);
+ if has_writing_mode {
+ self.compute_writing_mode(context);
+ }
+
+ if apply!(Zoom) {
+ self.compute_zoom(context);
+ }
+
+ // Compute font-family.
+ let has_font_family = apply!(FontFamily);
+ let has_lang = apply!(XLang);
+ if has_lang {
+ self.recompute_initial_font_family_if_needed(&mut context.builder);
+ }
+ if has_font_family {
+ self.prioritize_user_fonts_if_needed(&mut context.builder);
+ }
+
+ // Compute font-size.
+ if apply!(XTextScale) {
+ self.unzoom_fonts_if_needed(&mut context.builder);
+ }
+ let has_font_size = apply!(FontSize);
+ let has_math_depth = apply!(MathDepth);
+ let has_min_font_size_ratio = apply!(MozMinFontSizeRatio);
+
+ if has_math_depth && has_font_size {
+ self.recompute_math_font_size_if_needed(context);
+ }
+ if has_lang || has_font_family {
+ self.recompute_keyword_font_size_if_needed(context);
+ }
+ if has_font_size || has_min_font_size_ratio || has_lang || has_font_family {
+ self.constrain_font_size_if_needed(&mut context.builder);
+ }
+
+ // Compute the rest of the first-available-font-affecting properties.
+ apply!(FontWeight);
+ apply!(FontStretch);
+ apply!(FontStyle);
+ apply!(FontSizeAdjust);
+
+ apply!(ColorScheme);
+ apply!(ForcedColorAdjust);
+
+ // Compute the line height.
+ apply!(LineHeight);
+ }
+
+ fn apply_non_prioritary_properties(
+ &mut self,
+ context: &mut computed::Context,
+ longhand_declarations: &[Declaration],
+ shorthand_cache: &mut ShorthandsWithPropertyReferencesCache,
+ properties_to_apply: &LonghandIdSet,
+ ) {
+ debug_assert!(!properties_to_apply.contains_any(LonghandIdSet::prioritary_properties()));
+ debug_assert!(self.declarations_to_apply_unless_overridden.is_empty());
+ for declaration in &*longhand_declarations {
+ let mut longhand_id = declaration.decl.id().as_longhand().unwrap();
+ if !properties_to_apply.contains(longhand_id) {
+ continue;
+ }
+ debug_assert!(PrioritaryPropertyId::from_longhand(longhand_id).is_none());
+ let is_logical = longhand_id.is_logical();
+ if is_logical {
+ let wm = context.builder.writing_mode;
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(wm);
+ longhand_id = longhand_id.to_physical(wm);
+ }
+ self.apply_one_longhand(
+ context,
+ longhand_id,
+ declaration.decl,
+ declaration.priority,
+ shorthand_cache,
+ );
+ }
+ if !self.declarations_to_apply_unless_overridden.is_empty() {
+ debug_assert!(self.ignore_colors);
+ for declaration in std::mem::take(&mut self.declarations_to_apply_unless_overridden) {
+ let longhand_id = declaration.id().as_longhand().unwrap();
+ debug_assert!(!longhand_id.is_logical());
+ if !self.seen.contains(longhand_id) {
+ unsafe {
+ self.do_apply_declaration(context, longhand_id, &declaration);
+ }
+ }
+ }
+ }
+ }
+
+ fn apply_one_longhand(
+ &mut self,
+ context: &mut computed::Context,
+ longhand_id: LonghandId,
+ declaration: &PropertyDeclaration,
+ priority: CascadePriority,
+ cache: &mut ShorthandsWithPropertyReferencesCache,
+ ) {
+ debug_assert!(!longhand_id.is_logical());
+ let origin = priority.cascade_level().origin();
+ if self.seen.contains(longhand_id) {
+ return;
+ }
+
+ if self.reverted_set.contains(longhand_id) {
+ if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&longhand_id) {
+ if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) {
+ return;
+ }
+ }
+ }
+
+ let mut declaration = self.substitute_variables_if_needed(context, cache, declaration);
+
+ // When document colors are disabled, do special handling of
+ // properties that are marked as ignored in that mode.
+ if self.ignore_colors {
+ tweak_when_ignoring_colors(
+ context,
+ longhand_id,
+ origin,
+ &mut declaration,
+ &mut self.declarations_to_apply_unless_overridden,
+ );
+ }
+
+ let is_unset = match declaration.get_css_wide_keyword() {
+ Some(keyword) => match keyword {
+ CSSWideKeyword::RevertLayer | CSSWideKeyword::Revert => {
+ let origin_revert = keyword == CSSWideKeyword::Revert;
+ // We intentionally don't want to insert it into `self.seen`, `reverted` takes
+ // care of rejecting other declarations as needed.
+ self.reverted_set.insert(longhand_id);
+ self.reverted.insert(longhand_id, (priority, origin_revert));
+ return;
+ },
+ CSSWideKeyword::Unset => true,
+ CSSWideKeyword::Inherit => longhand_id.inherited(),
+ CSSWideKeyword::Initial => !longhand_id.inherited(),
+ },
+ None => false,
+ };
+
+ self.seen.insert(longhand_id);
+ if origin == Origin::Author {
+ self.author_specified.insert(longhand_id);
+ }
+
+ if is_unset {
+ return;
+ }
+
+ unsafe { self.do_apply_declaration(context, longhand_id, &declaration) }
+ }
+
+ #[inline]
+ unsafe fn do_apply_declaration(
+ &self,
+ context: &mut computed::Context,
+ longhand_id: LonghandId,
+ declaration: &PropertyDeclaration,
+ ) {
+ debug_assert!(!longhand_id.is_logical());
+ // We could (and used to) use a pattern match here, but that bloats this
+ // function to over 100K of compiled code!
+ //
+ // To improve i-cache behavior, we outline the individual functions and
+ // use virtual dispatch instead.
+ (CASCADE_PROPERTY[longhand_id as usize])(&declaration, context);
+ }
+
+ fn compute_zoom(&self, context: &mut computed::Context) {
+ context.builder.effective_zoom = context
+ .builder
+ .inherited_effective_zoom()
+ .compute_effective(context.builder.specified_zoom());
+ }
+
+ fn compute_writing_mode(&self, context: &mut computed::Context) {
+ context.builder.writing_mode = WritingMode::new(context.builder.get_inherited_box())
+ }
+
+ fn compute_visited_style_if_needed<E>(
+ &self,
+ context: &mut computed::Context,
+ element: Option<E>,
+ parent_style: Option<&ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ visited_rules: &StrongRuleNode,
+ guards: &StylesheetGuards,
+ ) where
+ E: TElement,
+ {
+ let is_link = context.builder.pseudo.is_none() && element.unwrap().is_link();
+
+ macro_rules! visited_parent {
+ ($parent:expr) => {
+ if is_link {
+ $parent
+ } else {
+ $parent.map(|p| p.visited_style().unwrap_or(p))
+ }
+ };
+ }
+
+ // We could call apply_declarations directly, but that'd cause
+ // another instantiation of this function which is not great.
+ let style = cascade_rules(
+ context.builder.stylist.unwrap(),
+ context.builder.pseudo,
+ visited_rules,
+ guards,
+ visited_parent!(parent_style),
+ visited_parent!(layout_parent_style),
+ self.first_line_reparenting,
+ CascadeMode::Visited {
+ unvisited_context: &*context,
+ },
+ // Cascade input flags don't matter for the visited style, they are
+ // in the main (unvisited) style.
+ Default::default(),
+ // The rule cache doesn't care about caching :visited
+ // styles, we cache the unvisited style instead. We still do
+ // need to set the caching dependencies properly if present
+ // though, so the cache conditions need to match.
+ None, // rule_cache
+ &mut *context.rule_cache_conditions.borrow_mut(),
+ element,
+ );
+ context.builder.visited_style = Some(style);
+ }
+
+ fn finished_applying_properties(&self, builder: &mut StyleBuilder) {
+ #[cfg(feature = "gecko")]
+ {
+ if let Some(bg) = builder.get_background_if_mutated() {
+ bg.fill_arrays();
+ }
+
+ if let Some(svg) = builder.get_svg_if_mutated() {
+ svg.fill_arrays();
+ }
+ }
+
+ if self
+ .author_specified
+ .contains_any(LonghandIdSet::border_background_properties())
+ {
+ builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_BORDER_BACKGROUND);
+ }
+
+ if self.author_specified.contains(LonghandId::FontFamily) {
+ builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_FAMILY);
+ }
+
+ if self.author_specified.contains(LonghandId::Color) {
+ builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_TEXT_COLOR);
+ }
+
+ if self.author_specified.contains(LonghandId::LetterSpacing) {
+ builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_LETTER_SPACING);
+ }
+
+ if self.author_specified.contains(LonghandId::WordSpacing) {
+ builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_WORD_SPACING);
+ }
+
+ if self
+ .author_specified
+ .contains(LonghandId::FontSynthesisWeight)
+ {
+ builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT);
+ }
+
+ if self
+ .author_specified
+ .contains(LonghandId::FontSynthesisStyle)
+ {
+ builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE);
+ }
+
+ #[cfg(feature = "servo")]
+ {
+ if let Some(font) = builder.get_font_if_mutated() {
+ font.compute_font_hash();
+ }
+ }
+ }
+
+ fn try_to_use_cached_reset_properties(
+ &self,
+ builder: &mut StyleBuilder<'b>,
+ cache: Option<&'b RuleCache>,
+ guards: &StylesheetGuards,
+ ) -> bool {
+ let style = match self.first_line_reparenting {
+ FirstLineReparenting::Yes { style_to_reparent } => style_to_reparent,
+ FirstLineReparenting::No => {
+ let Some(cache) = cache else { return false };
+ let Some(style) = cache.find(guards, builder) else {
+ return false;
+ };
+ style
+ },
+ };
+
+ builder.copy_reset_from(style);
+
+ // We're using the same reset style as another element, and we'll skip
+ // applying the relevant properties. So we need to do the relevant
+ // bookkeeping here to keep these bits correct.
+ //
+ // Note that the border/background properties are non-inherited, so we
+ // don't need to do anything else other than just copying the bits over.
+ //
+ // When using this optimization, we also need to copy whether the old
+ // style specified viewport units / used font-relative lengths, this one
+ // would as well. It matches the same rules, so it is the right thing
+ // to do anyways, even if it's only used on inherited properties.
+ let bits_to_copy = ComputedValueFlags::HAS_AUTHOR_SPECIFIED_BORDER_BACKGROUND |
+ ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS |
+ ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS |
+ ComputedValueFlags::USES_CONTAINER_UNITS |
+ ComputedValueFlags::USES_VIEWPORT_UNITS;
+ builder.add_flags(style.flags & bits_to_copy);
+
+ true
+ }
+
+ /// The initial font depends on the current lang group so we may need to
+ /// recompute it if the language changed.
+ #[inline]
+ #[cfg(feature = "gecko")]
+ fn recompute_initial_font_family_if_needed(&self, builder: &mut StyleBuilder) {
+ use crate::gecko_bindings::bindings;
+ use crate::values::computed::font::FontFamily;
+
+ let default_font_type = {
+ let font = builder.get_font();
+
+ if !font.mFont.family.is_initial {
+ return;
+ }
+
+ let default_font_type = unsafe {
+ bindings::Gecko_nsStyleFont_ComputeFallbackFontTypeForLanguage(
+ builder.device.document(),
+ font.mLanguage.mRawPtr,
+ )
+ };
+
+ let initial_generic = font.mFont.family.families.single_generic();
+ debug_assert!(
+ initial_generic.is_some(),
+ "Initial font should be just one generic font"
+ );
+ if initial_generic == Some(default_font_type) {
+ return;
+ }
+
+ default_font_type
+ };
+
+ // NOTE: Leaves is_initial untouched.
+ builder.mutate_font().mFont.family.families =
+ FontFamily::generic(default_font_type).families.clone();
+ }
+
+ /// Prioritize user fonts if needed by pref.
+ #[inline]
+ #[cfg(feature = "gecko")]
+ fn prioritize_user_fonts_if_needed(&self, builder: &mut StyleBuilder) {
+ use crate::gecko_bindings::bindings;
+
+ // Check the use_document_fonts setting for content, but for chrome
+ // documents they're treated as always enabled.
+ if static_prefs::pref!("browser.display.use_document_fonts") != 0 ||
+ builder.device.chrome_rules_enabled_for_document()
+ {
+ return;
+ }
+
+ let default_font_type = {
+ let font = builder.get_font();
+
+ if font.mFont.family.is_system_font {
+ return;
+ }
+
+ if !font.mFont.family.families.needs_user_font_prioritization() {
+ return;
+ }
+
+ unsafe {
+ bindings::Gecko_nsStyleFont_ComputeFallbackFontTypeForLanguage(
+ builder.device.document(),
+ font.mLanguage.mRawPtr,
+ )
+ }
+ };
+
+ let font = builder.mutate_font();
+ font.mFont
+ .family
+ .families
+ .prioritize_first_generic_or_prepend(default_font_type);
+ }
+
+ /// Some keyword sizes depend on the font family and language.
+ #[cfg(feature = "gecko")]
+ fn recompute_keyword_font_size_if_needed(&self, context: &mut computed::Context) {
+ use crate::values::computed::ToComputedValue;
+
+ if !self.seen.contains(LonghandId::XLang) && !self.seen.contains(LonghandId::FontFamily) {
+ return;
+ }
+
+ let new_size = {
+ let font = context.builder.get_font();
+ let info = font.clone_font_size().keyword_info;
+ let new_size = match info.kw {
+ specified::FontSizeKeyword::None => return,
+ _ => {
+ context.for_non_inherited_property = false;
+ specified::FontSize::Keyword(info).to_computed_value(context)
+ },
+ };
+
+ if font.mScriptUnconstrainedSize == new_size.computed_size {
+ return;
+ }
+
+ new_size
+ };
+
+ context.builder.mutate_font().set_font_size(new_size);
+ }
+
+ /// Some properties, plus setting font-size itself, may make us go out of
+ /// our minimum font-size range.
+ #[cfg(feature = "gecko")]
+ fn constrain_font_size_if_needed(&self, builder: &mut StyleBuilder) {
+ use crate::gecko_bindings::bindings;
+ use crate::values::generics::NonNegative;
+
+ let min_font_size = {
+ let font = builder.get_font();
+ let min_font_size = unsafe {
+ bindings::Gecko_nsStyleFont_ComputeMinSize(&**font, builder.device.document())
+ };
+
+ if font.mFont.size.0 >= min_font_size {
+ return;
+ }
+
+ NonNegative(min_font_size)
+ };
+
+ builder.mutate_font().mFont.size = min_font_size;
+ }
+
+ /// <svg:text> is not affected by text zoom, and it uses a preshint to disable it. We fix up
+ /// the struct when this happens by unzooming its contained font values, which will have been
+ /// zoomed in the parent.
+ ///
+ /// FIXME(emilio): Why doing this _before_ handling font-size? That sounds wrong.
+ #[cfg(feature = "gecko")]
+ fn unzoom_fonts_if_needed(&self, builder: &mut StyleBuilder) {
+ debug_assert!(self.seen.contains(LonghandId::XTextScale));
+
+ let parent_text_scale = builder.get_parent_font().clone__x_text_scale();
+ let text_scale = builder.get_font().clone__x_text_scale();
+ if parent_text_scale == text_scale {
+ return;
+ }
+ debug_assert_ne!(
+ parent_text_scale.text_zoom_enabled(),
+ text_scale.text_zoom_enabled(),
+ "There's only one value that disables it"
+ );
+ debug_assert!(
+ !text_scale.text_zoom_enabled(),
+ "We only ever disable text zoom (in svg:text), never enable it"
+ );
+ let device = builder.device;
+ builder.mutate_font().unzoom_fonts(device);
+ }
+
+ /// Special handling of font-size: math (used for MathML).
+ /// https://w3c.github.io/mathml-core/#the-math-script-level-property
+ /// TODO: Bug: 1548471: MathML Core also does not specify a script min size
+ /// should we unship that feature or standardize it?
+ #[cfg(feature = "gecko")]
+ fn recompute_math_font_size_if_needed(&self, context: &mut computed::Context) {
+ use crate::values::generics::NonNegative;
+
+ // Do not do anything if font-size: math or math-depth is not set.
+ if context.builder.get_font().clone_font_size().keyword_info.kw !=
+ specified::FontSizeKeyword::Math
+ {
+ return;
+ }
+
+ const SCALE_FACTOR_WHEN_INCREMENTING_MATH_DEPTH_BY_ONE: f32 = 0.71;
+
+ // Helper function that calculates the scale factor applied to font-size
+ // when math-depth goes from parent_math_depth to computed_math_depth.
+ // This function is essentially a modification of the MathML3's formula
+ // 0.71^(parent_math_depth - computed_math_depth) so that a scale factor
+ // of parent_script_percent_scale_down is applied when math-depth goes
+ // from 0 to 1 and parent_script_script_percent_scale_down is applied
+ // when math-depth goes from 0 to 2. This is also a straightforward
+ // implementation of the specification's algorithm:
+ // https://w3c.github.io/mathml-core/#the-math-script-level-property
+ fn scale_factor_for_math_depth_change(
+ parent_math_depth: i32,
+ computed_math_depth: i32,
+ parent_script_percent_scale_down: Option<f32>,
+ parent_script_script_percent_scale_down: Option<f32>,
+ ) -> f32 {
+ let mut a = parent_math_depth;
+ let mut b = computed_math_depth;
+ let c = SCALE_FACTOR_WHEN_INCREMENTING_MATH_DEPTH_BY_ONE;
+ let scale_between_0_and_1 = parent_script_percent_scale_down.unwrap_or_else(|| c);
+ let scale_between_0_and_2 =
+ parent_script_script_percent_scale_down.unwrap_or_else(|| c * c);
+ let mut s = 1.0;
+ let mut invert_scale_factor = false;
+ if a == b {
+ return s;
+ }
+ if b < a {
+ mem::swap(&mut a, &mut b);
+ invert_scale_factor = true;
+ }
+ let mut e = b - a;
+ if a <= 0 && b >= 2 {
+ s *= scale_between_0_and_2;
+ e -= 2;
+ } else if a == 1 {
+ s *= scale_between_0_and_2 / scale_between_0_and_1;
+ e -= 1;
+ } else if b == 1 {
+ s *= scale_between_0_and_1;
+ e -= 1;
+ }
+ s *= (c as f32).powi(e);
+ if invert_scale_factor {
+ 1.0 / s.max(f32::MIN_POSITIVE)
+ } else {
+ s
+ }
+ }
+
+ let (new_size, new_unconstrained_size) = {
+ let builder = &context.builder;
+ let font = builder.get_font();
+ let parent_font = builder.get_parent_font();
+
+ let delta = font.mMathDepth.saturating_sub(parent_font.mMathDepth);
+
+ if delta == 0 {
+ return;
+ }
+
+ let mut min = parent_font.mScriptMinSize;
+ if font.mXTextScale.text_zoom_enabled() {
+ min = builder.device.zoom_text(min);
+ }
+
+ // Calculate scale factor following MathML Core's algorithm.
+ let scale = {
+ // Script scale factors are independent of orientation.
+ let font_metrics = context.query_font_metrics(
+ FontBaseSize::InheritedStyle,
+ FontMetricsOrientation::Horizontal,
+ /* retrieve_math_scales = */ true,
+ );
+ scale_factor_for_math_depth_change(
+ parent_font.mMathDepth as i32,
+ font.mMathDepth as i32,
+ font_metrics.script_percent_scale_down,
+ font_metrics.script_script_percent_scale_down,
+ )
+ };
+
+ let parent_size = parent_font.mSize.0;
+ let parent_unconstrained_size = parent_font.mScriptUnconstrainedSize.0;
+ let new_size = parent_size.scale_by(scale);
+ let new_unconstrained_size = parent_unconstrained_size.scale_by(scale);
+
+ if scale <= 1. {
+ // The parent size can be smaller than scriptminsize, e.g. if it
+ // was specified explicitly. Don't scale in this case, but we
+ // don't want to set it to scriptminsize either since that will
+ // make it larger.
+ if parent_size <= min {
+ (parent_size, new_unconstrained_size)
+ } else {
+ (min.max(new_size), new_unconstrained_size)
+ }
+ } else {
+ // If the new unconstrained size is larger than the min size,
+ // this means we have escaped the grasp of scriptminsize and can
+ // revert to using the unconstrained size.
+ // However, if the new size is even larger (perhaps due to usage
+ // of em units), use that instead.
+ (
+ new_size.min(new_unconstrained_size.max(min)),
+ new_unconstrained_size,
+ )
+ }
+ };
+ let font = context.builder.mutate_font();
+ font.mFont.size = NonNegative(new_size);
+ font.mSize = NonNegative(new_size);
+ font.mScriptUnconstrainedSize = NonNegative(new_unconstrained_size);
+ }
+}
diff --git a/servo/components/style/properties/computed_value_flags.rs b/servo/components/style/properties/computed_value_flags.rs
new file mode 100644
index 0000000000..0952cb1799
--- /dev/null
+++ b/servo/components/style/properties/computed_value_flags.rs
@@ -0,0 +1,194 @@
+/* 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/. */
+
+//! Misc information about a given computed style.
+
+bitflags! {
+ /// Misc information about a given computed style.
+ ///
+ /// All flags are currently inherited for text, pseudo elements, and
+ /// anonymous boxes, see StyleBuilder::for_inheritance and its callsites.
+ /// If we ever want to add some flags that shouldn't inherit for them,
+ /// we might want to add a function to handle this.
+ #[repr(C)]
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
+ pub struct ComputedValueFlags: u32 {
+ /// Whether the style or any of the ancestors has a text-decoration-line
+ /// property that should get propagated to descendants.
+ ///
+ /// text-decoration-line is a reset property, but gets propagated in the
+ /// frame/box tree.
+ const HAS_TEXT_DECORATION_LINES = 1 << 0;
+
+ /// Whether line break inside should be suppressed.
+ ///
+ /// If this flag is set, the line should not be broken inside,
+ /// which means inlines act as if nowrap is set, <br> element is
+ /// suppressed, and blocks are inlinized.
+ ///
+ /// This bit is propagated to all children of line participants.
+ /// It is currently used by ruby to make its content unbreakable.
+ const SHOULD_SUPPRESS_LINEBREAK = 1 << 1;
+
+ /// A flag used to mark text that that has text-combine-upright.
+ ///
+ /// This is used from Gecko's layout engine.
+ const IS_TEXT_COMBINED = 1 << 2;
+
+ /// A flag used to mark styles under a relevant link that is also
+ /// visited.
+ const IS_RELEVANT_LINK_VISITED = 1 << 3;
+
+ /// A flag used to mark styles which are a pseudo-element or under one.
+ const IS_IN_PSEUDO_ELEMENT_SUBTREE = 1 << 4;
+
+ /// A flag used to mark styles which have contain:style or under one.
+ const SELF_OR_ANCESTOR_HAS_CONTAIN_STYLE = 1 << 5;
+
+ /// Whether this style's `display` property depends on our parent style.
+ ///
+ /// This is important because it may affect our optimizations to avoid
+ /// computing the style of pseudo-elements, given whether the
+ /// pseudo-element is generated depends on the `display` value.
+ const DISPLAY_DEPENDS_ON_INHERITED_STYLE = 1 << 6;
+
+ /// Whether this style's `content` depends on our parent style.
+ ///
+ /// Important because of the same reason.
+ const CONTENT_DEPENDS_ON_INHERITED_STYLE = 1 << 7;
+
+ /// Whether the child explicitly inherits any reset property.
+ const INHERITS_RESET_STYLE = 1 << 8;
+
+ /// Whether any value on our style is font-metric-dependent on our
+ /// primary font.
+ const DEPENDS_ON_SELF_FONT_METRICS = 1 << 9;
+
+ /// Whether any value on our style is font-metric-dependent on the
+ /// primary font of our parent.
+ const DEPENDS_ON_INHERITED_FONT_METRICS = 1 << 10;
+
+ /// Whether the style or any of the ancestors has a multicol style.
+ ///
+ /// Only used in Servo.
+ const CAN_BE_FRAGMENTED = 1 << 11;
+
+ /// Whether this style is the style of the document element.
+ const IS_ROOT_ELEMENT_STYLE = 1 << 12;
+
+ /// Whether this element is inside an `opacity: 0` subtree.
+ const IS_IN_OPACITY_ZERO_SUBTREE = 1 << 13;
+
+ /// Whether there are author-specified rules for border-* properties
+ /// (except border-image-*), background-color, or background-image.
+ ///
+ /// TODO(emilio): Maybe do include border-image, see:
+ ///
+ /// https://github.com/w3c/csswg-drafts/issues/4777#issuecomment-604424845
+ const HAS_AUTHOR_SPECIFIED_BORDER_BACKGROUND = 1 << 14;
+
+ /// Whether there are author-specified rules for `font-family`.
+ const HAS_AUTHOR_SPECIFIED_FONT_FAMILY = 1 << 16;
+
+ /// Whether there are author-specified rules for `font-synthesis-weight`.
+ const HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT = 1 << 17;
+
+ /// Whether there are author-specified rules for `font-synthesis-style`.
+ const HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE = 1 << 18;
+
+ // (There's also font-synthesis-small-caps and font-synthesis-position,
+ // but we don't currently need to keep track of those.)
+
+ /// Whether there are author-specified rules for `letter-spacing`.
+ const HAS_AUTHOR_SPECIFIED_LETTER_SPACING = 1 << 19;
+
+ /// Whether there are author-specified rules for `word-spacing`.
+ const HAS_AUTHOR_SPECIFIED_WORD_SPACING = 1 << 20;
+
+ /// Whether the style depends on viewport units.
+ const USES_VIEWPORT_UNITS = 1 << 21;
+
+ /// Whether the style depends on viewport units on container queries.
+ ///
+ /// This needs to be a separate flag from `USES_VIEWPORT_UNITS` because
+ /// it causes us to re-match the style (rather than re-cascascading it,
+ /// which is enough for other uses of viewport units).
+ const USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES = 1 << 22;
+
+ /// A flag used to mark styles which have `container-type` of `size` or
+ /// `inline-size`, or under one.
+ const SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE = 1 << 23;
+
+ /// Whether the style evaluated any relative selector.
+ const CONSIDERED_RELATIVE_SELECTOR = 1 << 24;
+
+ /// Whether the style evaluated the matched element to be an anchor of
+ /// a relative selector.
+ const ANCHORS_RELATIVE_SELECTOR = 1 << 25;
+
+ /// Whether the style uses container query units, in which case the style depends on the
+ /// container's size and we can't reuse it across cousins (without double-checking the
+ /// container at least).
+ const USES_CONTAINER_UNITS = 1 << 26;
+
+ /// Whether there are author-specific rules for text `color`.
+ const HAS_AUTHOR_SPECIFIED_TEXT_COLOR = 1 << 27;
+ }
+}
+
+impl Default for ComputedValueFlags {
+ #[inline]
+ fn default() -> Self {
+ Self::empty()
+ }
+}
+
+impl ComputedValueFlags {
+ /// Flags that are unconditionally propagated to descendants.
+ #[inline]
+ fn inherited_flags() -> Self {
+ Self::IS_RELEVANT_LINK_VISITED |
+ Self::CAN_BE_FRAGMENTED |
+ Self::IS_IN_PSEUDO_ELEMENT_SUBTREE |
+ Self::HAS_TEXT_DECORATION_LINES |
+ Self::IS_IN_OPACITY_ZERO_SUBTREE |
+ Self::SELF_OR_ANCESTOR_HAS_CONTAIN_STYLE |
+ Self::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE
+ }
+
+ /// Flags that may be propagated to descendants.
+ #[inline]
+ fn maybe_inherited_flags() -> Self {
+ Self::inherited_flags() | Self::SHOULD_SUPPRESS_LINEBREAK
+ }
+
+ /// Flags that are an input to the cascade.
+ #[inline]
+ fn cascade_input_flags() -> Self {
+ Self::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES |
+ Self::CONSIDERED_RELATIVE_SELECTOR |
+ Self::ANCHORS_RELATIVE_SELECTOR
+ }
+
+ /// Returns the flags that are always propagated to descendants.
+ ///
+ /// See StyleAdjuster::set_bits and StyleBuilder.
+ #[inline]
+ pub fn inherited(self) -> Self {
+ self & Self::inherited_flags()
+ }
+
+ /// Flags that are conditionally propagated to descendants, just to handle
+ /// properly style invalidation.
+ #[inline]
+ pub fn maybe_inherited(self) -> Self {
+ self & Self::maybe_inherited_flags()
+ }
+
+ /// Flags that are an input to the cascade.
+ #[inline]
+ pub fn for_cascade_inputs(self) -> Self {
+ self & Self::cascade_input_flags()
+ }
+}
diff --git a/servo/components/style/properties/counted_unknown_properties.py b/servo/components/style/properties/counted_unknown_properties.py
new file mode 100644
index 0000000000..b1b800812d
--- /dev/null
+++ b/servo/components/style/properties/counted_unknown_properties.py
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+COUNTED_UNKNOWN_PROPERTIES = [
+ "-webkit-font-smoothing",
+ "-webkit-tap-highlight-color",
+ "speak",
+ "text-size-adjust",
+ "-webkit-font-feature-settings",
+ "-webkit-user-drag",
+ "orphans",
+ "widows",
+ "-webkit-user-modify",
+ "-webkit-margin-before",
+ "-webkit-margin-after",
+ "-webkit-margin-start",
+ "-webkit-column-break-inside",
+ "-webkit-padding-start",
+ "-webkit-margin-end",
+ "-webkit-box-reflect",
+ "-webkit-print-color-adjust",
+ "-webkit-mask-box-image",
+ "-webkit-line-break",
+ "alignment-baseline",
+ "-webkit-writing-mode",
+ "baseline-shift",
+ "-webkit-hyphenate-character",
+ "-webkit-highlight",
+ "background-repeat-x",
+ "-webkit-padding-end",
+ "background-repeat-y",
+ "-webkit-text-emphasis-color",
+ "-webkit-margin-top-collapse",
+ "-webkit-rtl-ordering",
+ "-webkit-padding-before",
+ "-webkit-text-decorations-in-effect",
+ "-webkit-border-vertical-spacing",
+ "-webkit-locale",
+ "-webkit-padding-after",
+ "-webkit-border-horizontal-spacing",
+ "color-rendering",
+ "-webkit-column-break-before",
+ "-webkit-transform-origin-x",
+ "-webkit-transform-origin-y",
+ "-webkit-text-emphasis-position",
+ "buffered-rendering",
+ "-webkit-text-orientation",
+ "-webkit-text-combine",
+ "-webkit-text-emphasis-style",
+ "-webkit-text-emphasis",
+ "-webkit-mask-box-image-width",
+ "-webkit-mask-box-image-source",
+ "-webkit-mask-box-image-outset",
+ "-webkit-mask-box-image-slice",
+ "-webkit-mask-box-image-repeat",
+ "-webkit-margin-after-collapse",
+ "-webkit-border-before-color",
+ "-webkit-border-before-width",
+ "-webkit-perspective-origin-x",
+ "-webkit-perspective-origin-y",
+ "-webkit-margin-before-collapse",
+ "-webkit-border-before-style",
+ "-webkit-margin-bottom-collapse",
+ "-webkit-ruby-position",
+ "-webkit-column-break-after",
+ "-webkit-margin-collapse",
+ "-webkit-border-before",
+ "-webkit-border-end",
+ "-webkit-border-after",
+ "-webkit-border-start",
+ "-webkit-min-logical-width",
+ "-webkit-logical-height",
+ "-webkit-transform-origin-z",
+ "-webkit-font-size-delta",
+ "-webkit-logical-width",
+ "-webkit-max-logical-width",
+ "-webkit-min-logical-height",
+ "-webkit-max-logical-height",
+ "-webkit-border-end-color",
+ "-webkit-border-end-width",
+ "-webkit-border-start-color",
+ "-webkit-border-start-width",
+ "-webkit-border-after-color",
+ "-webkit-border-after-width",
+ "-webkit-border-end-style",
+ "-webkit-border-after-style",
+ "-webkit-border-start-style",
+ "-webkit-mask-repeat-x",
+ "-webkit-mask-repeat-y",
+ "user-zoom",
+ "min-zoom",
+ "-webkit-box-decoration-break",
+ "orientation",
+ "max-zoom",
+ "-webkit-app-region",
+ "-webkit-column-rule",
+ "-webkit-column-span",
+ "-webkit-column-gap",
+ "-webkit-shape-outside",
+ "-webkit-column-rule-width",
+ "-webkit-column-count",
+ "-webkit-opacity",
+ "-webkit-column-width",
+ "-webkit-shape-image-threshold",
+ "-webkit-column-rule-style",
+ "-webkit-columns",
+ "-webkit-column-rule-color",
+ "-webkit-shape-margin",
+]
diff --git a/servo/components/style/properties/data.py b/servo/components/style/properties/data.py
new file mode 100644
index 0000000000..093f1cb75f
--- /dev/null
+++ b/servo/components/style/properties/data.py
@@ -0,0 +1,1083 @@
+# 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
+
+# It is important that the order of these physical / logical variants matches
+# the order of the enum variants in logical_geometry.rs
+PHYSICAL_SIDES = ["top", "right", "bottom", "left"]
+PHYSICAL_CORNERS = ["top-left", "top-right", "bottom-right", "bottom-left"]
+PHYSICAL_AXES = ["y", "x"]
+PHYSICAL_SIZES = ["height", "width"]
+LOGICAL_SIDES = ["block-start", "block-end", "inline-start", "inline-end"]
+LOGICAL_CORNERS = ["start-start", "start-end", "end-start", "end-end"]
+LOGICAL_SIZES = ["block-size", "inline-size"]
+LOGICAL_AXES = ["block", "inline"]
+
+# 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()
+
+PRIORITARY_PROPERTIES = set(
+ [
+ # The writing-mode group has the most priority of all property groups, as
+ # sizes like font-size can depend on it.
+ "writing-mode",
+ "direction",
+ "text-orientation",
+ # The fonts and colors group has the second priority, as all other lengths
+ # and colors depend on them.
+ #
+ # There are some interdependencies between these, but we fix them up in
+ # Cascade::fixup_font_stuff.
+ # Needed to properly compute the zoomed font-size.
+ "-x-text-scale",
+ # Needed to do font-size computation in a language-dependent way.
+ "-x-lang",
+ # Needed for ruby to respect language-dependent min-font-size
+ # preferences properly, see bug 1165538.
+ "-moz-min-font-size-ratio",
+ # font-size depends on math-depth's computed value.
+ "math-depth",
+ # Needed to compute the first available font and its used size,
+ # in order to compute font-relative units correctly.
+ "font-size",
+ "font-size-adjust",
+ "font-weight",
+ "font-stretch",
+ "font-style",
+ "font-family",
+ # color-scheme affects how system colors resolve.
+ "color-scheme",
+ # forced-color-adjust affects whether colors are adjusted.
+ "forced-color-adjust",
+ # Zoom affects all absolute lengths.
+ "zoom",
+ # Line height lengths depend on this.
+ "line-height",
+ ]
+)
+
+VISITED_DEPENDENT_PROPERTIES = set(
+ [
+ "column-rule-color",
+ "text-emphasis-color",
+ "-webkit-text-fill-color",
+ "-webkit-text-stroke-color",
+ "text-decoration-color",
+ "fill",
+ "stroke",
+ "caret-color",
+ "background-color",
+ "border-top-color",
+ "border-right-color",
+ "border-bottom-color",
+ "border-left-color",
+ "border-block-start-color",
+ "border-inline-end-color",
+ "border-block-end-color",
+ "border-inline-start-color",
+ "outline-color",
+ "color",
+ ]
+)
+
+# 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 is_visited_dependent(self):
+ return self.name in VISITED_DEPENDENT_PROPERTIES
+
+ def is_prioritary(self):
+ return self.name in PRIORITARY_PROPERTIES
+
+ 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",
+ affects=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.affects = affects
+ self.flags += self.affects_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.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
+
+ def affects_flags(self):
+ # Layout is the stronger hint. This property animation affects layout
+ # or frame construction. `display` or `width` are examples that should
+ # use this.
+ if self.affects == "layout":
+ return ["AFFECTS_LAYOUT"]
+ # This property doesn't affect layout, but affects overflow.
+ # `transform` and co. are examples of this.
+ if self.affects == "overflow":
+ return ["AFFECTS_OVERFLOW"]
+ # This property affects the rendered output but doesn't affect layout.
+ # `opacity`, `color`, or `z-index` are examples of this.
+ if self.affects == "paint":
+ return ["AFFECTS_PAINT"]
+ # This property doesn't affect rendering in any way.
+ # `user-select` is an example of this.
+ assert self.affects == "", (
+ "Property "
+ + self.name
+ + ': affects must be specified and be one of ["layout", "overflow", "paint", ""], see Longhand.affects_flags for documentation'
+ )
+ return []
+
+ @staticmethod
+ def type():
+ return "longhand"
+
+ # For a given logical property, return the kind of mapping we need to
+ # perform, and which logical value we represent, in a tuple.
+ def logical_mapping_data(self, data):
+ if not self.logical:
+ return []
+ # Sizes and axes are basically the same for mapping, we just need
+ # slightly different replacements (block-size -> height, etc rather
+ # than -x/-y) below.
+ for [ty, logical_items, physical_items] in [
+ ["Side", LOGICAL_SIDES, PHYSICAL_SIDES],
+ ["Corner", LOGICAL_CORNERS, PHYSICAL_CORNERS],
+ ["Axis", LOGICAL_SIZES, PHYSICAL_SIZES],
+ ["Axis", LOGICAL_AXES, PHYSICAL_AXES],
+ ]:
+ candidate = [s for s in logical_items if s in self.name]
+ if candidate:
+ assert len(candidate) == 1
+ return [ty, candidate[0], logical_items, physical_items]
+ assert False, "Don't know how to deal with " + self.name
+
+ def logical_mapping_kind(self, data):
+ assert self.logical
+ [kind, item, _, _] = self.logical_mapping_data(data)
+ return "LogicalMappingKind::{}(Logical{}::{})".format(
+ kind, kind, to_camel_case(item.replace("-size", ""))
+ )
+
+ # 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 []
+ [_, logical_side, _, physical_items] = self.logical_mapping_data(data)
+ return [
+ data.longhands_by_name[to_phys(self.name, logical_side, physical_side)]
+ for physical_side in physical_items
+ ]
+
+ 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",
+ "AnimationComposition",
+ "AnimationDirection",
+ "AnimationFillMode",
+ "AnimationPlayState",
+ "AspectRatio",
+ "BaselineSource",
+ "BreakBetween",
+ "BreakWithin",
+ "BackgroundRepeat",
+ "BorderImageRepeat",
+ "BorderStyle",
+ "table::CaptionSide",
+ "Clear",
+ "ColumnCount",
+ "Contain",
+ "ContentVisibility",
+ "ContainerType",
+ "Display",
+ "FillRule",
+ "Float",
+ "FontLanguageOverride",
+ "FontSizeAdjust",
+ "FontStretch",
+ "FontStyle",
+ "FontSynthesis",
+ "FontVariantEastAsian",
+ "FontVariantLigatures",
+ "FontVariantNumeric",
+ "FontWeight",
+ "GreaterThanOrEqualToOneNumber",
+ "GridAutoFlow",
+ "ImageRendering",
+ "InitialLetter",
+ "Integer",
+ "JustifyContent",
+ "JustifyItems",
+ "JustifySelf",
+ "LineBreak",
+ "LineClamp",
+ "MasonryAutoFlow",
+ "ui::MozTheme",
+ "BoolInteger",
+ "text::MozControlCharacterVisibility",
+ "MathDepth",
+ "MozScriptMinSize",
+ "MozScriptSizeMultiplier",
+ "TransformBox",
+ "TextDecorationSkipInk",
+ "NonNegativeNumber",
+ "OffsetRotate",
+ "Opacity",
+ "OutlineStyle",
+ "Overflow",
+ "OverflowAnchor",
+ "OverflowClipBox",
+ "OverflowWrap",
+ "OverscrollBehavior",
+ "PageOrientation",
+ "Percentage",
+ "PrintColorAdjust",
+ "ForcedColorAdjust",
+ "Resize",
+ "RubyPosition",
+ "SVGOpacity",
+ "SVGPaintOrder",
+ "ScrollbarGutter",
+ "ScrollSnapAlign",
+ "ScrollSnapAxis",
+ "ScrollSnapStop",
+ "ScrollSnapStrictness",
+ "ScrollSnapType",
+ "TextAlign",
+ "TextAlignLast",
+ "TextDecorationLine",
+ "TextEmphasisPosition",
+ "TextJustify",
+ "TextTransform",
+ "TextUnderlinePosition",
+ "TouchAction",
+ "TransformStyle",
+ "UserSelect",
+ "WordBreak",
+ "XSpan",
+ "XTextScale",
+ "ZIndex",
+ "Zoom",
+ }
+ 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
+
+ animatable = property(get_animatable)
+
+ @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.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 []
+ self.document_dependent = self.gecko_name in ["Font", "Visibility", "Text"]
+
+
+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-collapse")
+ props.remove("text-wrap-mode")
+ props.remove("text-wrap-style")
+ 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://svgwg.org/svg2-draft/propidx.html
+ @staticmethod
+ def svg_text_properties():
+ props = set(
+ [
+ "fill",
+ "fill-opacity",
+ "fill-rule",
+ "paint-order",
+ "stroke",
+ "stroke-dasharray",
+ "stroke-dashoffset",
+ "stroke-linecap",
+ "stroke-linejoin",
+ "stroke-miterlimit",
+ "stroke-opacity",
+ "stroke-width",
+ "text-rendering",
+ "vector-effect",
+ ]
+ )
+ return props
+
+ @staticmethod
+ def webkit_text_properties():
+ props = set(
+ [
+ # Kinda like css-text?
+ "-webkit-text-stroke-width",
+ "-webkit-text-fill-color",
+ "-webkit-text-stroke-color",
+ ]
+ )
+ return props
+
+ # 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",
+ "vertical-align",
+ # Will become shorthand of vertical-align (Bug 1830771)
+ "baseline-source",
+ "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")
+ )
+ props = props.union(PropertyRestrictions.svg_text_properties())
+ props = props.union(PropertyRestrictions.webkit_text_properties())
+
+ _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",
+ "vertical-align",
+ # Will become shorthand of vertical-align (Bug 1830771)
+ "baseline-source",
+ "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")
+ )
+ props = props.union(PropertyRestrictions.svg_text_properties())
+ props = props.union(PropertyRestrictions.webkit_text_properties())
+
+ # 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("text-overflow")
+ props.add("text-align")
+ props.add("text-justify")
+ for p in PropertyRestrictions.shorthand(data, "text-wrap"):
+ props.add(p)
+ for p in PropertyRestrictions.shorthand(data, "white-space"):
+ props.add(p)
+ # ::placeholder can't be SVG text
+ props -= PropertyRestrictions.svg_text_properties()
+
+ return props
+
+ # https://drafts.csswg.org/css-pseudo/#marker-pseudo
+ @staticmethod
+ def marker(data):
+ return set(
+ [
+ "color",
+ "text-combine-upright",
+ "text-transform",
+ "unicode-bidi",
+ "direction",
+ "content",
+ "line-height",
+ "-moz-osx-font-smoothing",
+ ]
+ + PropertyRestrictions.shorthand(data, "text-wrap")
+ + PropertyRestrictions.shorthand(data, "white-space")
+ + 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",
+ "text-combine-upright",
+ "ruby-position",
+ # XXX Should these really apply to cue?
+ "-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, "text-wrap")
+ + PropertyRestrictions.shorthand(data, "white-space")
+ + PropertyRestrictions.shorthand(data, "background")
+ + PropertyRestrictions.shorthand(data, "outline")
+ + PropertyRestrictions.shorthand(data, "font")
+ + PropertyRestrictions.shorthand(data, "font-synthesis")
+ )
+
+
+class CountedUnknownProperty:
+ def __init__(self, name):
+ self.name = name
+ self.ident = to_rust_ident(name)
+ self.camel_case = to_camel_case(self.ident)
diff --git a/servo/components/style/properties/declaration_block.rs b/servo/components/style/properties/declaration_block.rs
new file mode 100644
index 0000000000..81d7148e62
--- /dev/null
+++ b/servo/components/style/properties/declaration_block.rs
@@ -0,0 +1,1642 @@
+/* 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/. */
+
+//! A property declaration block.
+
+#![deny(missing_docs)]
+
+use super::{
+ property_counts, AllShorthand, ComputedValues, LogicalGroupSet, LonghandIdSet,
+ LonghandIdSetIterator, NonCustomPropertyIdSet, PropertyDeclaration, PropertyDeclarationId,
+ PropertyId, ShorthandId, SourcePropertyDeclaration, SourcePropertyDeclarationDrain,
+ SubpropertiesVec,
+};
+use crate::context::QuirksMode;
+use crate::custom_properties;
+use crate::error_reporting::{ContextualParseError, ParseErrorReporter};
+use crate::parser::ParserContext;
+use crate::properties::{
+ animated_properties::{AnimationValue, AnimationValueMap},
+ StyleBuilder,
+};
+use crate::rule_cache::RuleCacheConditions;
+use crate::selector_map::PrecomputedHashSet;
+use crate::selector_parser::SelectorImpl;
+use crate::shared_lock::Locked;
+use crate::str::{CssString, CssStringWriter};
+use crate::stylesheets::container_rule::ContainerSizeQuery;
+use crate::stylesheets::{CssRuleType, Origin, UrlExtraData};
+use crate::stylist::Stylist;
+use crate::values::computed::Context;
+use cssparser::{
+ parse_important, AtRuleParser, CowRcStr, DeclarationParser, Delimiter, ParseErrorKind, Parser,
+ ParserInput, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser,
+};
+use itertools::Itertools;
+use selectors::SelectorList;
+use servo_arc::Arc;
+use smallbitvec::{self, SmallBitVec};
+use smallvec::SmallVec;
+use std::fmt::{self, Write};
+use std::iter::{DoubleEndedIterator, Zip};
+use std::slice::Iter;
+use style_traits::{CssWriter, ParseError, ParsingMode, StyleParseErrorKind, ToCss};
+use thin_vec::ThinVec;
+
+/// A set of property declarations including animations and transitions.
+#[derive(Default)]
+pub struct AnimationDeclarations {
+ /// Declarations for animations.
+ pub animations: Option<Arc<Locked<PropertyDeclarationBlock>>>,
+ /// Declarations for transitions.
+ pub transitions: Option<Arc<Locked<PropertyDeclarationBlock>>>,
+}
+
+impl AnimationDeclarations {
+ /// Whether or not this `AnimationDeclarations` is empty.
+ pub fn is_empty(&self) -> bool {
+ self.animations.is_none() && self.transitions.is_none()
+ }
+}
+
+/// An enum describes how a declaration should update
+/// the declaration block.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum DeclarationUpdate {
+ /// The given declaration doesn't update anything.
+ None,
+ /// The given declaration is new, and should be append directly.
+ Append,
+ /// The given declaration can be updated in-place at the given position.
+ UpdateInPlace { pos: usize },
+ /// The given declaration cannot be updated in-place, and an existing
+ /// one needs to be removed at the given position.
+ AppendAndRemove { pos: usize },
+}
+
+/// A struct describes how a declaration block should be updated by
+/// a `SourcePropertyDeclaration`.
+#[derive(Default)]
+pub struct SourcePropertyDeclarationUpdate {
+ updates: SubpropertiesVec<DeclarationUpdate>,
+ new_count: usize,
+ any_removal: bool,
+}
+
+/// A declaration [importance][importance].
+///
+/// [importance]: https://drafts.csswg.org/css-cascade/#importance
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+pub enum Importance {
+ /// Indicates a declaration without `!important`.
+ Normal,
+
+ /// Indicates a declaration with `!important`.
+ Important,
+}
+
+impl Default for Importance {
+ fn default() -> Self {
+ Self::Normal
+ }
+}
+
+impl Importance {
+ /// Return whether this is an important declaration.
+ pub fn important(self) -> bool {
+ match self {
+ Self::Normal => false,
+ Self::Important => true,
+ }
+ }
+}
+
+/// A set of properties.
+#[derive(Clone, Debug, ToShmem, Default, MallocSizeOf)]
+pub struct PropertyDeclarationIdSet {
+ longhands: LonghandIdSet,
+ custom: PrecomputedHashSet<custom_properties::Name>,
+}
+
+impl PropertyDeclarationIdSet {
+ /// Add the given property to the set.
+ pub fn insert(&mut self, id: PropertyDeclarationId) -> bool {
+ match id {
+ PropertyDeclarationId::Longhand(id) => {
+ if self.longhands.contains(id) {
+ return false;
+ }
+ self.longhands.insert(id);
+ return true;
+ },
+ PropertyDeclarationId::Custom(name) => self.custom.insert((*name).clone()),
+ }
+ }
+
+ /// Return whether the given property is in the set.
+ pub fn contains(&self, id: PropertyDeclarationId) -> bool {
+ match id {
+ PropertyDeclarationId::Longhand(id) => self.longhands.contains(id),
+ PropertyDeclarationId::Custom(name) => self.custom.contains(name),
+ }
+ }
+
+ /// Remove the given property from the set.
+ pub fn remove(&mut self, id: PropertyDeclarationId) {
+ match id {
+ PropertyDeclarationId::Longhand(id) => self.longhands.remove(id),
+ PropertyDeclarationId::Custom(name) => {
+ self.custom.remove(name);
+ },
+ }
+ }
+
+ /// Remove all properties from the set.
+ pub fn clear(&mut self) {
+ self.longhands.clear();
+ self.custom.clear();
+ }
+
+ /// Returns whether the set is empty.
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.longhands.is_empty() && self.custom.is_empty()
+ }
+ /// Returns whether this set contains any reset longhand.
+ #[inline]
+ pub fn contains_any_reset(&self) -> bool {
+ self.longhands.contains_any_reset()
+ }
+
+ /// Returns whether this set contains all longhands in the specified set.
+ #[inline]
+ pub fn contains_all_longhands(&self, longhands: &LonghandIdSet) -> bool {
+ self.longhands.contains_all(longhands)
+ }
+
+ /// Returns whether this set contains all properties in the specified set.
+ #[inline]
+ pub fn contains_all(&self, properties: &PropertyDeclarationIdSet) -> bool {
+ if !self.longhands.contains_all(&properties.longhands) {
+ return false;
+ }
+ if properties.custom.len() > self.custom.len() {
+ return false;
+ }
+ properties
+ .custom
+ .iter()
+ .all(|item| self.custom.contains(item))
+ }
+
+ /// Iterate over the current property declaration id set.
+ pub fn iter(&self) -> PropertyDeclarationIdSetIterator {
+ PropertyDeclarationIdSetIterator {
+ longhands: self.longhands.iter(),
+ custom: self.custom.iter(),
+ }
+ }
+}
+
+/// An iterator over a set of longhand ids.
+pub struct PropertyDeclarationIdSetIterator<'a> {
+ longhands: LonghandIdSetIterator<'a>,
+ custom: std::collections::hash_set::Iter<'a, custom_properties::Name>,
+}
+
+impl<'a> Iterator for PropertyDeclarationIdSetIterator<'a> {
+ type Item = PropertyDeclarationId<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ // LonghandIdSetIterator's implementation always returns None
+ // after it did it once, so the code below will then continue
+ // to iterate over the custom properties.
+ match self.longhands.next() {
+ Some(id) => Some(PropertyDeclarationId::Longhand(id)),
+ None => match self.custom.next() {
+ Some(a) => Some(PropertyDeclarationId::Custom(a)),
+ None => None,
+ },
+ }
+ }
+}
+
+/// Overridden declarations are skipped.
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(Clone, ToShmem, Default)]
+pub struct PropertyDeclarationBlock {
+ /// The group of declarations, along with their importance.
+ ///
+ /// Only deduplicated declarations appear here.
+ declarations: ThinVec<PropertyDeclaration>,
+
+ /// The "important" flag for each declaration in `declarations`.
+ declarations_importance: SmallBitVec,
+
+ /// The set of properties that are present in the block.
+ property_ids: PropertyDeclarationIdSet,
+}
+
+/// Iterator over `(PropertyDeclaration, Importance)` pairs.
+pub struct DeclarationImportanceIterator<'a> {
+ iter: Zip<Iter<'a, PropertyDeclaration>, smallbitvec::Iter<'a>>,
+}
+
+impl<'a> Default for DeclarationImportanceIterator<'a> {
+ fn default() -> Self {
+ Self {
+ iter: [].iter().zip(smallbitvec::Iter::default()),
+ }
+ }
+}
+
+impl<'a> DeclarationImportanceIterator<'a> {
+ /// Constructor.
+ fn new(declarations: &'a [PropertyDeclaration], important: &'a SmallBitVec) -> Self {
+ DeclarationImportanceIterator {
+ iter: declarations.iter().zip(important.iter()),
+ }
+ }
+}
+
+impl<'a> Iterator for DeclarationImportanceIterator<'a> {
+ type Item = (&'a PropertyDeclaration, Importance);
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ self.iter.next().map(|(decl, important)| {
+ (
+ decl,
+ if important {
+ Importance::Important
+ } else {
+ Importance::Normal
+ },
+ )
+ })
+ }
+
+ #[inline]
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.iter.size_hint()
+ }
+}
+
+impl<'a> DoubleEndedIterator for DeclarationImportanceIterator<'a> {
+ #[inline(always)]
+ fn next_back(&mut self) -> Option<Self::Item> {
+ self.iter.next_back().map(|(decl, important)| {
+ (
+ decl,
+ if important {
+ Importance::Important
+ } else {
+ Importance::Normal
+ },
+ )
+ })
+ }
+}
+
+/// Iterator for AnimationValue to be generated from PropertyDeclarationBlock.
+pub struct AnimationValueIterator<'a, 'cx, 'cx_a: 'cx> {
+ iter: DeclarationImportanceIterator<'a>,
+ context: &'cx mut Context<'cx_a>,
+ default_values: &'a ComputedValues,
+}
+
+impl<'a, 'cx, 'cx_a: 'cx> AnimationValueIterator<'a, 'cx, 'cx_a> {
+ fn new(
+ declarations: &'a PropertyDeclarationBlock,
+ context: &'cx mut Context<'cx_a>,
+ default_values: &'a ComputedValues,
+ ) -> AnimationValueIterator<'a, 'cx, 'cx_a> {
+ AnimationValueIterator {
+ iter: declarations.declaration_importance_iter(),
+ context,
+ default_values,
+ }
+ }
+}
+
+impl<'a, 'cx, 'cx_a: 'cx> Iterator for AnimationValueIterator<'a, 'cx, 'cx_a> {
+ type Item = AnimationValue;
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ let (decl, importance) = self.iter.next()?;
+
+ if importance.important() {
+ continue;
+ }
+
+ let animation =
+ AnimationValue::from_declaration(decl, &mut self.context, self.default_values);
+
+ if let Some(anim) = animation {
+ return Some(anim);
+ }
+ }
+ }
+}
+
+impl fmt::Debug for PropertyDeclarationBlock {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.declarations.fmt(f)
+ }
+}
+
+impl PropertyDeclarationBlock {
+ /// Returns the number of declarations in the block.
+ #[inline]
+ pub fn len(&self) -> usize {
+ self.declarations.len()
+ }
+
+ /// Returns whether the block is empty.
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.declarations.is_empty()
+ }
+
+ /// Create an empty block
+ #[inline]
+ pub fn new() -> Self {
+ PropertyDeclarationBlock {
+ declarations: ThinVec::new(),
+ declarations_importance: SmallBitVec::new(),
+ property_ids: PropertyDeclarationIdSet::default(),
+ }
+ }
+
+ /// Create a block with a single declaration
+ pub fn with_one(declaration: PropertyDeclaration, importance: Importance) -> Self {
+ let mut property_ids = PropertyDeclarationIdSet::default();
+ property_ids.insert(declaration.id());
+ let mut declarations = ThinVec::with_capacity(1);
+ declarations.push(declaration);
+ PropertyDeclarationBlock {
+ declarations,
+ declarations_importance: SmallBitVec::from_elem(1, importance.important()),
+ property_ids,
+ }
+ }
+
+ /// The declarations in this block
+ #[inline]
+ pub fn declarations(&self) -> &[PropertyDeclaration] {
+ &self.declarations
+ }
+
+ /// The `important` flags for declarations in this block
+ #[inline]
+ pub fn declarations_importance(&self) -> &SmallBitVec {
+ &self.declarations_importance
+ }
+
+ /// Iterate over `(PropertyDeclaration, Importance)` pairs
+ #[inline]
+ pub fn declaration_importance_iter(&self) -> DeclarationImportanceIterator {
+ DeclarationImportanceIterator::new(&self.declarations, &self.declarations_importance)
+ }
+
+ /// Iterate over `PropertyDeclaration` for Importance::Normal
+ #[inline]
+ pub fn normal_declaration_iter<'a>(
+ &'a self,
+ ) -> impl DoubleEndedIterator<Item = &'a PropertyDeclaration> {
+ self.declaration_importance_iter()
+ .filter(|(_, importance)| !importance.important())
+ .map(|(declaration, _)| declaration)
+ }
+
+ /// Return an iterator of (AnimatableLonghand, AnimationValue).
+ #[inline]
+ pub fn to_animation_value_iter<'a, 'cx, 'cx_a: 'cx>(
+ &'a self,
+ context: &'cx mut Context<'cx_a>,
+ default_values: &'a ComputedValues,
+ ) -> AnimationValueIterator<'a, 'cx, 'cx_a> {
+ AnimationValueIterator::new(self, context, default_values)
+ }
+
+ /// Returns whether this block contains any declaration with `!important`.
+ ///
+ /// This is based on the `declarations_importance` bit-vector,
+ /// which should be maintained whenever `declarations` is changed.
+ #[inline]
+ pub fn any_important(&self) -> bool {
+ !self.declarations_importance.all_false()
+ }
+
+ /// Returns whether this block contains any declaration without `!important`.
+ ///
+ /// This is based on the `declarations_importance` bit-vector,
+ /// which should be maintained whenever `declarations` is changed.
+ #[inline]
+ pub fn any_normal(&self) -> bool {
+ !self.declarations_importance.all_true()
+ }
+
+ /// Returns a `PropertyDeclarationIdSet` representing the properties that are changed in
+ /// this block.
+ #[inline]
+ pub fn property_ids(&self) -> &PropertyDeclarationIdSet {
+ &self.property_ids
+ }
+
+ /// Returns whether this block contains a declaration of a given property id.
+ #[inline]
+ pub fn contains(&self, id: PropertyDeclarationId) -> bool {
+ self.property_ids.contains(id)
+ }
+
+ /// Returns whether this block contains any reset longhand.
+ #[inline]
+ pub fn contains_any_reset(&self) -> bool {
+ self.property_ids.contains_any_reset()
+ }
+
+ /// Get a declaration for a given property.
+ ///
+ /// NOTE: This is linear time in the case of custom properties or in the
+ /// case the longhand is actually in the declaration block.
+ #[inline]
+ pub fn get(
+ &self,
+ property: PropertyDeclarationId,
+ ) -> Option<(&PropertyDeclaration, Importance)> {
+ if !self.contains(property) {
+ return None;
+ }
+ self.declaration_importance_iter()
+ .find(|(declaration, _)| declaration.id() == property)
+ }
+
+ /// Tries to serialize a given shorthand from the declarations in this
+ /// block.
+ pub fn shorthand_to_css(
+ &self,
+ shorthand: ShorthandId,
+ dest: &mut CssStringWriter,
+ ) -> fmt::Result {
+ // Step 1.2.1 of
+ // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertyvalue
+ let mut list = SmallVec::<[&_; 10]>::new();
+ let mut important_count = 0;
+
+ // Step 1.2.2
+ for longhand in shorthand.longhands() {
+ // Step 1.2.2.1
+ let declaration = self.get(PropertyDeclarationId::Longhand(longhand));
+
+ // Step 1.2.2.2 & 1.2.2.3
+ match declaration {
+ Some((declaration, importance)) => {
+ list.push(declaration);
+ if importance.important() {
+ important_count += 1;
+ }
+ },
+ None => return Ok(()),
+ }
+ }
+
+ // If there is one or more longhand with important, and one or more
+ // without important, we don't serialize it as a shorthand.
+ if important_count > 0 && important_count != list.len() {
+ return Ok(());
+ }
+
+ // Step 1.2.3
+ // We don't print !important when serializing individual properties,
+ // so we treat this as a normal-importance property
+ match shorthand.get_shorthand_appendable_value(&list) {
+ Some(appendable_value) => append_declaration_value(dest, appendable_value),
+ None => return Ok(()),
+ }
+ }
+
+ /// Find the value of the given property in this block and serialize it
+ ///
+ /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertyvalue>
+ pub fn property_value_to_css(
+ &self,
+ property: &PropertyId,
+ dest: &mut CssStringWriter,
+ ) -> fmt::Result {
+ // Step 1.1: done when parsing a string to PropertyId
+
+ // Step 1.2
+ let longhand_or_custom = match property.as_shorthand() {
+ Ok(shorthand) => return self.shorthand_to_css(shorthand, dest),
+ Err(longhand_or_custom) => longhand_or_custom,
+ };
+
+ if let Some((value, _importance)) = self.get(longhand_or_custom) {
+ // Step 2
+ value.to_css(dest)
+ } else {
+ // Step 3
+ Ok(())
+ }
+ }
+
+ /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertypriority>
+ pub fn property_priority(&self, property: &PropertyId) -> Importance {
+ // Step 1: done when parsing a string to PropertyId
+
+ // Step 2
+ match property.as_shorthand() {
+ Ok(shorthand) => {
+ // Step 2.1 & 2.2 & 2.3
+ if shorthand.longhands().all(|l| {
+ self.get(PropertyDeclarationId::Longhand(l))
+ .map_or(false, |(_, importance)| importance.important())
+ }) {
+ Importance::Important
+ } else {
+ Importance::Normal
+ }
+ },
+ Err(longhand_or_custom) => {
+ // Step 3
+ self.get(longhand_or_custom)
+ .map_or(Importance::Normal, |(_, importance)| importance)
+ },
+ }
+ }
+
+ /// Adds or overrides the declaration for a given property in this block.
+ ///
+ /// See the documentation of `push` to see what impact `source` has when the
+ /// property is already there.
+ pub fn extend(
+ &mut self,
+ mut drain: SourcePropertyDeclarationDrain,
+ importance: Importance,
+ ) -> bool {
+ let all_shorthand_len = match drain.all_shorthand {
+ AllShorthand::NotSet => 0,
+ AllShorthand::CSSWideKeyword(_) | AllShorthand::WithVariables(_) => {
+ property_counts::ALL_SHORTHAND_EXPANDED
+ },
+ };
+ let push_calls_count = drain.declarations.len() + all_shorthand_len;
+
+ // With deduplication the actual length increase may be less than this.
+ self.declarations.reserve(push_calls_count);
+
+ let mut changed = false;
+ for decl in &mut drain.declarations {
+ changed |= self.push(decl, importance);
+ }
+ drain
+ .all_shorthand
+ .declarations()
+ .fold(changed, |changed, decl| {
+ changed | self.push(decl, importance)
+ })
+ }
+
+ /// Adds or overrides the declaration for a given property in this block.
+ ///
+ /// Returns whether the declaration has changed.
+ ///
+ /// This is only used for parsing and internal use.
+ pub fn push(&mut self, declaration: PropertyDeclaration, importance: Importance) -> bool {
+ let id = declaration.id();
+ if !self.property_ids.insert(id) {
+ let mut index_to_remove = None;
+ for (i, slot) in self.declarations.iter_mut().enumerate() {
+ if slot.id() != id {
+ continue;
+ }
+
+ let important = self.declarations_importance[i];
+
+ // For declarations from parsing, non-important declarations
+ // shouldn't override existing important one.
+ if important && !importance.important() {
+ return false;
+ }
+
+ index_to_remove = Some(i);
+ break;
+ }
+
+ if let Some(index) = index_to_remove {
+ self.declarations.remove(index);
+ self.declarations_importance.remove(index);
+ self.declarations.push(declaration);
+ self.declarations_importance.push(importance.important());
+ return true;
+ }
+ }
+
+ self.declarations.push(declaration);
+ self.declarations_importance.push(importance.important());
+ true
+ }
+
+ /// Prepares updating this declaration block with the given
+ /// `SourcePropertyDeclaration` and importance, and returns whether
+ /// there is something to update.
+ pub fn prepare_for_update(
+ &self,
+ source_declarations: &SourcePropertyDeclaration,
+ importance: Importance,
+ updates: &mut SourcePropertyDeclarationUpdate,
+ ) -> bool {
+ debug_assert!(updates.updates.is_empty());
+ // Check whether we are updating for an all shorthand change.
+ if !matches!(source_declarations.all_shorthand, AllShorthand::NotSet) {
+ debug_assert!(source_declarations.declarations.is_empty());
+ return source_declarations
+ .all_shorthand
+ .declarations()
+ .any(|decl| {
+ !self.contains(decl.id()) ||
+ self.declarations
+ .iter()
+ .enumerate()
+ .find(|&(_, ref d)| d.id() == decl.id())
+ .map_or(true, |(i, d)| {
+ let important = self.declarations_importance[i];
+ *d != decl || important != importance.important()
+ })
+ });
+ }
+ // Fill `updates` with update information.
+ let mut any_update = false;
+ let new_count = &mut updates.new_count;
+ let any_removal = &mut updates.any_removal;
+ let updates = &mut updates.updates;
+ updates.extend(
+ source_declarations
+ .declarations
+ .iter()
+ .map(|declaration| {
+ if !self.contains(declaration.id()) {
+ return DeclarationUpdate::Append;
+ }
+ let longhand_id = declaration.id().as_longhand();
+ if let Some(longhand_id) = longhand_id {
+ if let Some(logical_group) = longhand_id.logical_group() {
+ let mut needs_append = false;
+ for (pos, decl) in self.declarations.iter().enumerate().rev() {
+ let id = match decl.id().as_longhand() {
+ Some(id) => id,
+ None => continue,
+ };
+ if id == longhand_id {
+ if needs_append {
+ return DeclarationUpdate::AppendAndRemove { pos };
+ }
+ let important = self.declarations_importance[pos];
+ if decl == declaration && important == importance.important() {
+ return DeclarationUpdate::None;
+ }
+ return DeclarationUpdate::UpdateInPlace { pos };
+ }
+ if !needs_append &&
+ id.logical_group() == Some(logical_group) &&
+ id.is_logical() != longhand_id.is_logical()
+ {
+ needs_append = true;
+ }
+ }
+ unreachable!("Longhand should be found in loop above");
+ }
+ }
+ self.declarations
+ .iter()
+ .enumerate()
+ .find(|&(_, ref decl)| decl.id() == declaration.id())
+ .map_or(DeclarationUpdate::Append, |(pos, decl)| {
+ let important = self.declarations_importance[pos];
+ if decl == declaration && important == importance.important() {
+ DeclarationUpdate::None
+ } else {
+ DeclarationUpdate::UpdateInPlace { pos }
+ }
+ })
+ })
+ .inspect(|update| {
+ if matches!(update, DeclarationUpdate::None) {
+ return;
+ }
+ any_update = true;
+ match update {
+ DeclarationUpdate::Append => {
+ *new_count += 1;
+ },
+ DeclarationUpdate::AppendAndRemove { .. } => {
+ *any_removal = true;
+ },
+ _ => {},
+ }
+ }),
+ );
+ any_update
+ }
+
+ /// Update this declaration block with the given data.
+ pub fn update(
+ &mut self,
+ drain: SourcePropertyDeclarationDrain,
+ importance: Importance,
+ updates: &mut SourcePropertyDeclarationUpdate,
+ ) {
+ let important = importance.important();
+ if !matches!(drain.all_shorthand, AllShorthand::NotSet) {
+ debug_assert!(updates.updates.is_empty());
+ for decl in drain.all_shorthand.declarations() {
+ let id = decl.id();
+ if self.property_ids.insert(id) {
+ self.declarations.push(decl);
+ self.declarations_importance.push(important);
+ } else {
+ let (idx, slot) = self
+ .declarations
+ .iter_mut()
+ .enumerate()
+ .find(|&(_, ref d)| d.id() == decl.id())
+ .unwrap();
+ *slot = decl;
+ self.declarations_importance.set(idx, important);
+ }
+ }
+ return;
+ }
+
+ self.declarations.reserve(updates.new_count);
+ if updates.any_removal {
+ // Prepare for removal and fixing update positions.
+ struct UpdateOrRemoval<'a> {
+ item: &'a mut DeclarationUpdate,
+ pos: usize,
+ remove: bool,
+ }
+ let mut updates_and_removals: SubpropertiesVec<UpdateOrRemoval> = updates
+ .updates
+ .iter_mut()
+ .filter_map(|item| {
+ let (pos, remove) = match *item {
+ DeclarationUpdate::UpdateInPlace { pos } => (pos, false),
+ DeclarationUpdate::AppendAndRemove { pos } => (pos, true),
+ _ => return None,
+ };
+ Some(UpdateOrRemoval { item, pos, remove })
+ })
+ .collect();
+ // Execute removals. It's important to do it in reverse index order,
+ // so that removing doesn't invalidate following positions.
+ updates_and_removals.sort_unstable_by_key(|update| update.pos);
+ updates_and_removals
+ .iter()
+ .rev()
+ .filter(|update| update.remove)
+ .for_each(|update| {
+ self.declarations.remove(update.pos);
+ self.declarations_importance.remove(update.pos);
+ });
+ // Fixup pos field for updates.
+ let mut removed_count = 0;
+ for update in updates_and_removals.iter_mut() {
+ if update.remove {
+ removed_count += 1;
+ continue;
+ }
+ debug_assert_eq!(
+ *update.item,
+ DeclarationUpdate::UpdateInPlace { pos: update.pos }
+ );
+ *update.item = DeclarationUpdate::UpdateInPlace {
+ pos: update.pos - removed_count,
+ };
+ }
+ }
+ // Execute updates and appends.
+ for (decl, update) in drain.declarations.zip_eq(updates.updates.iter()) {
+ match *update {
+ DeclarationUpdate::None => {},
+ DeclarationUpdate::Append | DeclarationUpdate::AppendAndRemove { .. } => {
+ self.property_ids.insert(decl.id());
+ self.declarations.push(decl);
+ self.declarations_importance.push(important);
+ },
+ DeclarationUpdate::UpdateInPlace { pos } => {
+ self.declarations[pos] = decl;
+ self.declarations_importance.set(pos, important);
+ },
+ }
+ }
+ updates.updates.clear();
+ }
+
+ /// Returns the first declaration that would be removed by removing
+ /// `property`.
+ #[inline]
+ pub fn first_declaration_to_remove(&self, property: &PropertyId) -> Option<usize> {
+ if let Err(longhand_or_custom) = property.as_shorthand() {
+ if !self.contains(longhand_or_custom) {
+ return None;
+ }
+ }
+
+ self.declarations
+ .iter()
+ .position(|declaration| declaration.id().is_or_is_longhand_of(property))
+ }
+
+ /// Removes a given declaration at a given index.
+ #[inline]
+ fn remove_declaration_at(&mut self, i: usize) {
+ self.property_ids.remove(self.declarations[i].id());
+ self.declarations_importance.remove(i);
+ self.declarations.remove(i);
+ }
+
+ /// Clears all the declarations from this block.
+ #[inline]
+ pub fn clear(&mut self) {
+ self.declarations_importance.clear();
+ self.declarations.clear();
+ self.property_ids.clear();
+ }
+
+ /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-removeproperty>
+ ///
+ /// `first_declaration` needs to be the result of
+ /// `first_declaration_to_remove`.
+ #[inline]
+ pub fn remove_property(&mut self, property: &PropertyId, first_declaration: usize) {
+ debug_assert_eq!(
+ Some(first_declaration),
+ self.first_declaration_to_remove(property)
+ );
+ debug_assert!(self.declarations[first_declaration]
+ .id()
+ .is_or_is_longhand_of(property));
+
+ self.remove_declaration_at(first_declaration);
+
+ let shorthand = match property.as_shorthand() {
+ Ok(s) => s,
+ Err(_longhand_or_custom) => return,
+ };
+
+ let mut i = first_declaration;
+ let mut len = self.len();
+ while i < len {
+ if !self.declarations[i].id().is_longhand_of(shorthand) {
+ i += 1;
+ continue;
+ }
+
+ self.remove_declaration_at(i);
+ len -= 1;
+ }
+ }
+
+ /// Take a declaration block known to contain a single property and serialize it.
+ pub fn single_value_to_css(
+ &self,
+ property: &PropertyId,
+ dest: &mut CssStringWriter,
+ computed_values: Option<&ComputedValues>,
+ stylist: &Stylist,
+ ) -> fmt::Result {
+ if let Ok(shorthand) = property.as_shorthand() {
+ return self.shorthand_to_css(shorthand, dest);
+ }
+
+ // FIXME(emilio): Should this assert, or assert that the declaration is
+ // the property we expect?
+ let declaration = match self.declarations.get(0) {
+ Some(d) => d,
+ None => return Err(fmt::Error),
+ };
+
+ let mut rule_cache_conditions = RuleCacheConditions::default();
+ let mut context = Context::new(
+ StyleBuilder::new(
+ stylist.device(),
+ Some(stylist),
+ computed_values,
+ None,
+ None,
+ false,
+ ),
+ stylist.quirks_mode(),
+ &mut rule_cache_conditions,
+ ContainerSizeQuery::none(),
+ );
+
+ if let Some(cv) = computed_values {
+ context.builder.custom_properties = cv.custom_properties.clone();
+ };
+
+ match (declaration, computed_values) {
+ // If we have a longhand declaration with variables, those variables
+ // will be stored as unparsed values.
+ //
+ // As a temporary measure to produce sensible results in Gecko's
+ // getKeyframes() implementation for CSS animations, if
+ // |computed_values| is supplied, we use it to expand such variable
+ // declarations. This will be fixed properly in Gecko bug 1391537.
+ (&PropertyDeclaration::WithVariables(ref declaration), Some(_)) => declaration
+ .value
+ .substitute_variables(
+ declaration.id,
+ &context.builder.custom_properties,
+ stylist,
+ &context,
+ &mut Default::default(),
+ )
+ .to_css(dest),
+ (ref d, _) => d.to_css(dest),
+ }
+ }
+
+ /// Convert AnimationValueMap to PropertyDeclarationBlock.
+ pub fn from_animation_value_map(animation_value_map: &AnimationValueMap) -> Self {
+ let len = animation_value_map.len();
+ let mut declarations = ThinVec::with_capacity(len);
+ let mut property_ids = PropertyDeclarationIdSet::default();
+
+ for (property, animation_value) in animation_value_map.iter() {
+ property_ids.insert(property.as_borrowed());
+ declarations.push(animation_value.uncompute());
+ }
+
+ PropertyDeclarationBlock {
+ declarations,
+ property_ids,
+ declarations_importance: SmallBitVec::from_elem(len, false),
+ }
+ }
+
+ /// Returns true if the declaration block has a CSSWideKeyword for the given
+ /// property.
+ pub fn has_css_wide_keyword(&self, property: &PropertyId) -> bool {
+ if let Err(longhand_or_custom) = property.as_shorthand() {
+ if !self.property_ids.contains(longhand_or_custom) {
+ return false;
+ }
+ }
+ self.declarations.iter().any(|decl| {
+ decl.id().is_or_is_longhand_of(property) && decl.get_css_wide_keyword().is_some()
+ })
+ }
+
+ /// Like the method on ToCss, but without the type parameter to avoid
+ /// accidentally monomorphizing this large function multiple times for
+ /// different writers.
+ ///
+ /// https://drafts.csswg.org/cssom/#serialize-a-css-declaration-block
+ pub fn to_css(&self, dest: &mut CssStringWriter) -> fmt::Result {
+ let mut is_first_serialization = true; // trailing serializations should have a prepended space
+
+ // Step 1 -> dest = result list
+
+ // Step 2
+ //
+ // NOTE(emilio): We reuse this set for both longhands and shorthands
+ // with subtly different meaning. For longhands, only longhands that
+ // have actually been serialized (either by themselves, or as part of a
+ // shorthand) appear here. For shorthands, all the shorthands that we've
+ // attempted to serialize appear here.
+ let mut already_serialized = NonCustomPropertyIdSet::new();
+
+ // Step 3
+ 'declaration_loop: for (declaration, importance) in self.declaration_importance_iter() {
+ // Step 3.1
+ let property = declaration.id();
+ let longhand_id = match property {
+ PropertyDeclarationId::Longhand(id) => id,
+ PropertyDeclarationId::Custom(..) => {
+ // Given the invariants that there are no duplicate
+ // properties in a declaration block, and that custom
+ // properties can't be part of a shorthand, we can just care
+ // about them here.
+ append_serialization(
+ dest,
+ &property,
+ AppendableValue::Declaration(declaration),
+ importance,
+ &mut is_first_serialization,
+ )?;
+ continue;
+ },
+ };
+
+ // Step 3.2
+ if already_serialized.contains(longhand_id.into()) {
+ continue;
+ }
+
+ // Steps 3.3 & 3.4
+ for shorthand in longhand_id.shorthands() {
+ // We already attempted to serialize this shorthand before.
+ if already_serialized.contains(shorthand.into()) {
+ continue;
+ }
+ already_serialized.insert(shorthand.into());
+
+ if shorthand.is_legacy_shorthand() {
+ continue;
+ }
+
+ // Step 3.3.1:
+ // Let longhands be an array consisting of all CSS
+ // declarations in declaration block’s declarations that
+ // that are not in already serialized and have a property
+ // name that maps to one of the shorthand properties in
+ // shorthands.
+ let longhands = {
+ // TODO(emilio): This could just index in an array if we
+ // remove pref-controlled longhands.
+ let mut ids = LonghandIdSet::new();
+ for longhand in shorthand.longhands() {
+ ids.insert(longhand);
+ }
+ ids
+ };
+
+ // Step 3.4.2
+ // If all properties that map to shorthand are not present
+ // in longhands, continue with the steps labeled shorthand
+ // loop.
+ if !self.property_ids.contains_all_longhands(&longhands) {
+ continue;
+ }
+
+ // Step 3.4.3:
+ // Let current longhands be an empty array.
+ let mut current_longhands = SmallVec::<[&_; 10]>::new();
+ let mut logical_groups = LogicalGroupSet::new();
+ let mut saw_one = false;
+ let mut logical_mismatch = false;
+ let mut seen = LonghandIdSet::new();
+ let mut important_count = 0;
+
+ // Step 3.4.4:
+ // Append all CSS declarations in longhands that have a
+ // property name that maps to shorthand to current longhands.
+ for (declaration, importance) in self.declaration_importance_iter() {
+ let longhand = match declaration.id() {
+ PropertyDeclarationId::Longhand(id) => id,
+ PropertyDeclarationId::Custom(..) => continue,
+ };
+
+ if longhands.contains(longhand) {
+ saw_one = true;
+ if importance.important() {
+ important_count += 1;
+ }
+ current_longhands.push(declaration);
+ if shorthand != ShorthandId::All {
+ // All is special because it contains both physical
+ // and logical longhands.
+ if let Some(g) = longhand.logical_group() {
+ logical_groups.insert(g);
+ }
+ seen.insert(longhand);
+ if seen == longhands {
+ break;
+ }
+ }
+ } else if saw_one {
+ if let Some(g) = longhand.logical_group() {
+ if logical_groups.contains(g) {
+ logical_mismatch = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // 3.4.5:
+ // If there is one or more CSS declarations in current
+ // longhands have their important flag set and one or more
+ // with it unset, continue with the steps labeled shorthand
+ // loop.
+ let is_important = important_count > 0;
+ if is_important && important_count != current_longhands.len() {
+ continue;
+ }
+
+ // 3.4.6:
+ // If there’s any declaration in declaration block in between
+ // the first and the last longhand in current longhands which
+ // belongs to the same logical property group, but has a
+ // different mapping logic as any of the longhands in current
+ // longhands, and is not in current longhands, continue with
+ // the steps labeled shorthand loop.
+ if logical_mismatch {
+ continue;
+ }
+
+ let importance = if is_important {
+ Importance::Important
+ } else {
+ Importance::Normal
+ };
+
+ // 3.4.7:
+ // Let value be the result of invoking serialize a CSS value
+ // of current longhands.
+ let appendable_value =
+ match shorthand.get_shorthand_appendable_value(&current_longhands) {
+ None => continue,
+ Some(appendable_value) => appendable_value,
+ };
+
+ // We avoid re-serializing if we're already an
+ // AppendableValue::Css.
+ let mut v = CssString::new();
+ let value = match appendable_value {
+ AppendableValue::Css(css) => {
+ debug_assert!(!css.is_empty());
+ appendable_value
+ },
+ other => {
+ append_declaration_value(&mut v, other)?;
+
+ // 3.4.8:
+ // If value is the empty string, continue with the
+ // steps labeled shorthand loop.
+ if v.is_empty() {
+ continue;
+ }
+
+ AppendableValue::Css({
+ // Safety: serialization only generates valid utf-8.
+ #[cfg(feature = "gecko")]
+ unsafe {
+ v.as_str_unchecked()
+ }
+ #[cfg(feature = "servo")]
+ &v
+ })
+ },
+ };
+
+ // 3.4.9:
+ // Let serialized declaration be the result of invoking
+ // serialize a CSS declaration with property name shorthand,
+ // value value, and the important flag set if the CSS
+ // declarations in current longhands have their important
+ // flag set.
+ //
+ // 3.4.10:
+ // Append serialized declaration to list.
+ append_serialization(
+ dest,
+ &shorthand,
+ value,
+ importance,
+ &mut is_first_serialization,
+ )?;
+
+ // 3.4.11:
+ // Append the property names of all items of current
+ // longhands to already serialized.
+ for current_longhand in &current_longhands {
+ let longhand_id = match current_longhand.id() {
+ PropertyDeclarationId::Longhand(id) => id,
+ PropertyDeclarationId::Custom(..) => unreachable!(),
+ };
+
+ // Substep 9
+ already_serialized.insert(longhand_id.into());
+ }
+
+ // 3.4.12:
+ // Continue with the steps labeled declaration loop.
+ continue 'declaration_loop;
+ }
+
+ // Steps 3.5, 3.6 & 3.7:
+ // Let value be the result of invoking serialize a CSS value of
+ // declaration.
+ //
+ // Let serialized declaration be the result of invoking
+ // serialize a CSS declaration with property name property,
+ // value value, and the important flag set if declaration has
+ // its important flag set.
+ //
+ // Append serialized declaration to list.
+ append_serialization(
+ dest,
+ &property,
+ AppendableValue::Declaration(declaration),
+ importance,
+ &mut is_first_serialization,
+ )?;
+
+ // Step 3.8:
+ // Append property to already serialized.
+ already_serialized.insert(longhand_id.into());
+ }
+
+ // Step 4
+ Ok(())
+ }
+}
+
+/// A convenient enum to represent different kinds of stuff that can represent a
+/// _value_ in the serialization of a property declaration.
+pub enum AppendableValue<'a, 'b: 'a> {
+ /// A given declaration, of which we'll serialize just the value.
+ Declaration(&'a PropertyDeclaration),
+ /// A set of declarations for a given shorthand.
+ ///
+ /// FIXME: This needs more docs, where are the shorthands expanded? We print
+ /// the property name before-hand, don't we?
+ DeclarationsForShorthand(ShorthandId, &'a [&'b PropertyDeclaration]),
+ /// A raw CSS string, coming for example from a property with CSS variables,
+ /// or when storing a serialized shorthand value before appending directly.
+ Css(&'a str),
+}
+
+/// Potentially appends whitespace after the first (property: value;) pair.
+fn handle_first_serialization<W>(dest: &mut W, is_first_serialization: &mut bool) -> fmt::Result
+where
+ W: Write,
+{
+ if !*is_first_serialization {
+ dest.write_char(' ')
+ } else {
+ *is_first_serialization = false;
+ Ok(())
+ }
+}
+
+/// Append a given kind of appendable value to a serialization.
+pub fn append_declaration_value<'a, 'b: 'a>(
+ dest: &mut CssStringWriter,
+ appendable_value: AppendableValue<'a, 'b>,
+) -> fmt::Result {
+ match appendable_value {
+ AppendableValue::Css(css) => dest.write_str(css),
+ AppendableValue::Declaration(decl) => decl.to_css(dest),
+ AppendableValue::DeclarationsForShorthand(shorthand, decls) => {
+ shorthand.longhands_to_css(decls, dest)
+ },
+ }
+}
+
+/// Append a given property and value pair to a serialization.
+pub fn append_serialization<'a, 'b: 'a, N>(
+ dest: &mut CssStringWriter,
+ property_name: &N,
+ appendable_value: AppendableValue<'a, 'b>,
+ importance: Importance,
+ is_first_serialization: &mut bool,
+) -> fmt::Result
+where
+ N: ToCss,
+{
+ handle_first_serialization(dest, is_first_serialization)?;
+
+ property_name.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str(": ")?;
+
+ append_declaration_value(dest, appendable_value)?;
+
+ if importance.important() {
+ dest.write_str(" !important")?;
+ }
+
+ dest.write_char(';')
+}
+
+/// A helper to parse the style attribute of an element, in order for this to be
+/// shared between Servo and Gecko.
+///
+/// Inline because we call this cross-crate.
+#[inline]
+pub fn parse_style_attribute(
+ input: &str,
+ url_data: &UrlExtraData,
+ error_reporter: Option<&dyn ParseErrorReporter>,
+ quirks_mode: QuirksMode,
+ rule_type: CssRuleType,
+) -> PropertyDeclarationBlock {
+ let context = ParserContext::new(
+ Origin::Author,
+ url_data,
+ Some(rule_type),
+ ParsingMode::DEFAULT,
+ quirks_mode,
+ /* namespaces = */ Default::default(),
+ error_reporter,
+ None,
+ );
+
+ let mut input = ParserInput::new(input);
+ parse_property_declaration_list(&context, &mut Parser::new(&mut input), &[])
+}
+
+/// Parse a given property declaration. Can result in multiple
+/// `PropertyDeclaration`s when expanding a shorthand, for example.
+///
+/// This does not attempt to parse !important at all.
+#[inline]
+pub fn parse_one_declaration_into(
+ declarations: &mut SourcePropertyDeclaration,
+ id: PropertyId,
+ input: &str,
+ origin: Origin,
+ url_data: &UrlExtraData,
+ error_reporter: Option<&dyn ParseErrorReporter>,
+ parsing_mode: ParsingMode,
+ quirks_mode: QuirksMode,
+ rule_type: CssRuleType,
+) -> Result<(), ()> {
+ let context = ParserContext::new(
+ origin,
+ url_data,
+ Some(rule_type),
+ parsing_mode,
+ quirks_mode,
+ /* namespaces = */ Default::default(),
+ error_reporter,
+ None,
+ );
+
+ let property_id_for_error_reporting = if context.error_reporting_enabled() {
+ Some(id.clone())
+ } else {
+ None
+ };
+
+ let mut input = ParserInput::new(input);
+ let mut parser = Parser::new(&mut input);
+ let start_position = parser.position();
+ parser
+ .parse_entirely(|parser| {
+ PropertyDeclaration::parse_into(declarations, id, &context, parser)
+ })
+ .map_err(|err| {
+ if context.error_reporting_enabled() {
+ report_one_css_error(
+ &context,
+ None,
+ &[],
+ err,
+ parser.slice_from(start_position),
+ property_id_for_error_reporting,
+ )
+ }
+ })
+}
+
+/// A struct to parse property declarations.
+struct PropertyDeclarationParser<'a, 'b: 'a, 'i> {
+ context: &'a ParserContext<'b>,
+ state: &'a mut DeclarationParserState<'i>,
+}
+
+/// The state needed to parse a declaration block.
+///
+/// It stores declarations in output_block.
+#[derive(Default)]
+pub struct DeclarationParserState<'i> {
+ /// The output block where results are stored.
+ output_block: PropertyDeclarationBlock,
+ /// Declarations from the last declaration parsed. (note that a shorthand might expand to
+ /// multiple declarations).
+ declarations: SourcePropertyDeclaration,
+ /// The importance from the last declaration parsed.
+ importance: Importance,
+ /// A list of errors that have happened so far. Not all of them might be reported.
+ errors: SmallParseErrorVec<'i>,
+ /// The last parsed property id, if any.
+ last_parsed_property_id: Option<PropertyId>,
+}
+
+impl<'i> DeclarationParserState<'i> {
+ /// Returns whether any parsed declarations have been parsed so far.
+ pub fn has_parsed_declarations(&self) -> bool {
+ !self.output_block.is_empty()
+ }
+
+ /// Takes the parsed declarations.
+ pub fn take_declarations(&mut self) -> PropertyDeclarationBlock {
+ std::mem::take(&mut self.output_block)
+ }
+
+ /// Parse a single declaration value.
+ pub fn parse_value<'t>(
+ &mut self,
+ context: &ParserContext,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ let id = match PropertyId::parse(&name, context) {
+ Ok(id) => id,
+ Err(..) => {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnknownProperty(name)));
+ },
+ };
+ if context.error_reporting_enabled() {
+ self.last_parsed_property_id = Some(id.clone());
+ }
+ input.parse_until_before(Delimiter::Bang, |input| {
+ PropertyDeclaration::parse_into(&mut self.declarations, id, context, input)
+ })?;
+ self.importance = match input.try_parse(parse_important) {
+ Ok(()) => Importance::Important,
+ Err(_) => Importance::Normal,
+ };
+ // In case there is still unparsed text in the declaration, we should roll back.
+ input.expect_exhausted()?;
+ self.output_block
+ .extend(self.declarations.drain(), self.importance);
+ // We've successfully parsed a declaration, so forget about
+ // `last_parsed_property_id`. It'd be wrong to associate any
+ // following error with this property.
+ self.last_parsed_property_id = None;
+ Ok(())
+ }
+
+ /// Reports any CSS errors that have ocurred if needed.
+ #[inline]
+ pub fn report_errors_if_needed(
+ &mut self,
+ context: &ParserContext,
+ selectors: &[SelectorList<SelectorImpl>],
+ ) {
+ if self.errors.is_empty() {
+ return;
+ }
+ self.do_report_css_errors(context, selectors);
+ }
+
+ #[cold]
+ fn do_report_css_errors(
+ &mut self,
+ context: &ParserContext,
+ selectors: &[SelectorList<SelectorImpl>],
+ ) {
+ for (error, slice, property) in self.errors.drain(..) {
+ report_one_css_error(
+ context,
+ Some(&self.output_block),
+ selectors,
+ error,
+ slice,
+ property,
+ )
+ }
+ }
+
+ /// Resets the declaration parser state, and reports the error if needed.
+ #[inline]
+ pub fn did_error(&mut self, context: &ParserContext, error: ParseError<'i>, slice: &'i str) {
+ self.declarations.clear();
+ if !context.error_reporting_enabled() {
+ return;
+ }
+ let property = self.last_parsed_property_id.take();
+ self.errors.push((error, slice, property));
+ }
+}
+
+/// Default methods reject all at rules.
+impl<'a, 'b, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'b, 'i> {
+ type Prelude = ();
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+/// Default methods reject all rules.
+impl<'a, 'b, 'i> QualifiedRuleParser<'i> for PropertyDeclarationParser<'a, 'b, 'i> {
+ type Prelude = ();
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+/// Based on NonMozillaVendorIdentifier from Gecko's CSS parser.
+fn is_non_mozilla_vendor_identifier(name: &str) -> bool {
+ (name.starts_with("-") && !name.starts_with("-moz-")) || name.starts_with("_")
+}
+
+impl<'a, 'b, 'i> DeclarationParser<'i> for PropertyDeclarationParser<'a, 'b, 'i> {
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_value<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ self.state.parse_value(self.context, name, input)
+ }
+}
+
+impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>>
+ for PropertyDeclarationParser<'a, 'b, 'i>
+{
+ fn parse_declarations(&self) -> bool {
+ true
+ }
+ // TODO(emilio): Nesting.
+ fn parse_qualified(&self) -> bool {
+ false
+ }
+}
+
+type SmallParseErrorVec<'i> = SmallVec<[(ParseError<'i>, &'i str, Option<PropertyId>); 2]>;
+
+fn alias_of_known_property(name: &str) -> Option<PropertyId> {
+ let mut prefixed = String::with_capacity(name.len() + 5);
+ prefixed.push_str("-moz-");
+ prefixed.push_str(name);
+ PropertyId::parse_enabled_for_all_content(&prefixed).ok()
+}
+
+#[cold]
+fn report_one_css_error<'i>(
+ context: &ParserContext,
+ block: Option<&PropertyDeclarationBlock>,
+ selectors: &[SelectorList<SelectorImpl>],
+ mut error: ParseError<'i>,
+ slice: &str,
+ property: Option<PropertyId>,
+) {
+ debug_assert!(context.error_reporting_enabled());
+
+ fn all_properties_in_block(block: &PropertyDeclarationBlock, property: &PropertyId) -> bool {
+ match property.as_shorthand() {
+ Ok(id) => id
+ .longhands()
+ .all(|longhand| block.contains(PropertyDeclarationId::Longhand(longhand))),
+ Err(longhand_or_custom) => block.contains(longhand_or_custom),
+ }
+ }
+
+ if let ParseErrorKind::Custom(StyleParseErrorKind::UnknownProperty(ref name)) = error.kind {
+ if is_non_mozilla_vendor_identifier(name) {
+ // If the unrecognized property looks like a vendor-specific property,
+ // silently ignore it instead of polluting the error output.
+ return;
+ }
+ if let Some(alias) = alias_of_known_property(name) {
+ // This is an unknown property, but its -moz-* version is known.
+ // We don't want to report error if the -moz-* version is already
+ // specified.
+ if let Some(block) = block {
+ if all_properties_in_block(block, &alias) {
+ return;
+ }
+ }
+ }
+ }
+
+ if let Some(ref property) = property {
+ if let Some(block) = block {
+ if all_properties_in_block(block, property) {
+ return;
+ }
+ }
+ error = match *property {
+ PropertyId::Custom(ref c) => {
+ StyleParseErrorKind::new_invalid(format!("--{}", c), error)
+ },
+ _ => StyleParseErrorKind::new_invalid(property.non_custom_id().unwrap().name(), error),
+ };
+ }
+
+ let location = error.location;
+ let error = ContextualParseError::UnsupportedPropertyDeclaration(slice, error, selectors);
+ context.log_css_error(location, error);
+}
+
+/// Parse a list of property declarations and return a property declaration
+/// block.
+pub fn parse_property_declaration_list(
+ context: &ParserContext,
+ input: &mut Parser,
+ selectors: &[SelectorList<SelectorImpl>],
+) -> PropertyDeclarationBlock {
+ let mut state = DeclarationParserState::default();
+ let mut parser = PropertyDeclarationParser {
+ context,
+ state: &mut state,
+ };
+ let mut iter = RuleBodyParser::new(input, &mut parser);
+ while let Some(declaration) = iter.next() {
+ match declaration {
+ Ok(()) => {},
+ Err((error, slice)) => iter.parser.state.did_error(context, error, slice),
+ }
+ }
+ parser.state.report_errors_if_needed(context, selectors);
+ state.output_block
+}
diff --git a/servo/components/style/properties/gecko.mako.rs b/servo/components/style/properties/gecko.mako.rs
new file mode 100644
index 0000000000..f5ae0cade3
--- /dev/null
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -0,0 +1,1806 @@
+/* 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/. */
+
+// `data` comes from components/style/properties.mako.rs; see build.rs for more details.
+
+<%!
+ from data import to_camel_case, to_camel_case_lower
+ from data import Keyword
+%>
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+use crate::Atom;
+use app_units::Au;
+use crate::computed_value_flags::*;
+use crate::custom_properties::ComputedCustomProperties;
+use crate::gecko_bindings::bindings;
+% for style_struct in data.style_structs:
+use crate::gecko_bindings::bindings::Gecko_Construct_Default_${style_struct.gecko_ffi_name};
+use crate::gecko_bindings::bindings::Gecko_CopyConstruct_${style_struct.gecko_ffi_name};
+use crate::gecko_bindings::bindings::Gecko_Destroy_${style_struct.gecko_ffi_name};
+% endfor
+use crate::gecko_bindings::bindings::Gecko_CopyCounterStyle;
+use crate::gecko_bindings::bindings::Gecko_EnsureImageLayersLength;
+use crate::gecko_bindings::bindings::Gecko_nsStyleFont_SetLang;
+use crate::gecko_bindings::bindings::Gecko_nsStyleFont_CopyLangFrom;
+use crate::gecko_bindings::structs;
+use crate::gecko_bindings::structs::mozilla::PseudoStyleType;
+use crate::gecko::data::PerDocumentStyleData;
+use crate::logical_geometry::WritingMode;
+use crate::media_queries::Device;
+use crate::properties::longhands;
+use crate::rule_tree::StrongRuleNode;
+use crate::selector_parser::PseudoElement;
+use servo_arc::{Arc, UniqueArc};
+use std::mem::{forget, MaybeUninit, ManuallyDrop};
+use std::{cmp, ops, ptr};
+use crate::values;
+use crate::values::computed::{BorderStyle, Percentage, Time, Zoom};
+use crate::values::computed::font::FontSize;
+use crate::values::generics::column::ColumnCount;
+
+
+pub mod style_structs {
+ % for style_struct in data.style_structs:
+ pub use super::${style_struct.gecko_struct_name} as ${style_struct.name};
+
+ unsafe impl Send for ${style_struct.name} {}
+ unsafe impl Sync for ${style_struct.name} {}
+ % endfor
+}
+
+/// FIXME(emilio): This is completely duplicated with the other properties code.
+pub type ComputedValuesInner = structs::ServoComputedData;
+
+#[repr(C)]
+pub struct ComputedValues(structs::mozilla::ComputedStyle);
+
+impl ComputedValues {
+ #[inline]
+ pub (crate) fn as_gecko_computed_style(&self) -> &structs::ComputedStyle {
+ &self.0
+ }
+
+ pub fn new(
+ pseudo: Option<<&PseudoElement>,
+ custom_properties: ComputedCustomProperties,
+ writing_mode: WritingMode,
+ effective_zoom: Zoom,
+ flags: ComputedValueFlags,
+ rules: Option<StrongRuleNode>,
+ visited_style: Option<Arc<ComputedValues>>,
+ % for style_struct in data.style_structs:
+ ${style_struct.ident}: Arc<style_structs::${style_struct.name}>,
+ % endfor
+ ) -> Arc<Self> {
+ ComputedValuesInner::new(
+ custom_properties,
+ writing_mode,
+ effective_zoom,
+ flags,
+ rules,
+ visited_style,
+ % for style_struct in data.style_structs:
+ ${style_struct.ident},
+ % endfor
+ ).to_outer(pseudo)
+ }
+
+ pub fn default_values(doc: &structs::Document) -> Arc<Self> {
+ ComputedValuesInner::new(
+ ComputedCustomProperties::default(),
+ WritingMode::empty(), // FIXME(bz): This seems dubious
+ Zoom::ONE,
+ ComputedValueFlags::empty(),
+ /* rules = */ None,
+ /* visited_style = */ None,
+ % for style_struct in data.style_structs:
+ style_structs::${style_struct.name}::default(doc),
+ % endfor
+ ).to_outer(None)
+ }
+
+ /// Converts the computed values to an Arc<> from a reference.
+ pub fn to_arc(&self) -> Arc<Self> {
+ // SAFETY: We're guaranteed to be allocated as an Arc<> since the
+ // functions above are the only ones that create ComputedValues
+ // instances in Gecko (and that must be the case since ComputedValues'
+ // member is private).
+ unsafe { Arc::from_raw_addrefed(self) }
+ }
+
+ #[inline]
+ pub fn is_pseudo_style(&self) -> bool {
+ self.0.mPseudoType != PseudoStyleType::NotPseudo
+ }
+
+ #[inline]
+ pub fn pseudo(&self) -> Option<PseudoElement> {
+ if !self.is_pseudo_style() {
+ return None;
+ }
+ PseudoElement::from_pseudo_type(self.0.mPseudoType, None)
+ }
+
+ #[inline]
+ pub fn is_first_line_style(&self) -> bool {
+ self.pseudo() == Some(PseudoElement::FirstLine)
+ }
+
+ /// Returns true if the display property is changed from 'none' to others.
+ pub fn is_display_property_changed_from_none(
+ &self,
+ old_values: Option<<&ComputedValues>
+ ) -> bool {
+ use crate::properties::longhands::display::computed_value::T as Display;
+
+ old_values.map_or(false, |old| {
+ let old_display_style = old.get_box().clone_display();
+ let new_display_style = self.get_box().clone_display();
+ old_display_style == Display::None &&
+ new_display_style != Display::None
+ })
+ }
+
+}
+
+impl Drop for ComputedValues {
+ fn drop(&mut self) {
+ // XXX this still relies on the destructor of ComputedValuesInner to run on the rust side,
+ // that's pretty wild.
+ unsafe {
+ bindings::Gecko_ComputedStyle_Destroy(&mut self.0);
+ }
+ }
+}
+
+unsafe impl Sync for ComputedValues {}
+unsafe impl Send for ComputedValues {}
+
+impl Clone for ComputedValues {
+ fn clone(&self) -> Self {
+ unreachable!()
+ }
+}
+
+impl Clone for ComputedValuesInner {
+ fn clone(&self) -> Self {
+ ComputedValuesInner {
+ % for style_struct in data.style_structs:
+ ${style_struct.gecko_name}: Arc::into_raw(unsafe { Arc::from_raw_addrefed(self.${style_struct.name_lower}_ptr()) }) as *const _,
+ % endfor
+ custom_properties: self.custom_properties.clone(),
+ writing_mode: self.writing_mode.clone(),
+ flags: self.flags.clone(),
+ effective_zoom: self.effective_zoom,
+ rules: self.rules.clone(),
+ visited_style: if self.visited_style.is_null() {
+ ptr::null()
+ } else {
+ Arc::into_raw(unsafe { Arc::from_raw_addrefed(self.visited_style_ptr()) }) as *const _
+ },
+ }
+ }
+}
+
+
+impl Drop for ComputedValuesInner {
+ fn drop(&mut self) {
+ % for style_struct in data.style_structs:
+ let _ = unsafe { Arc::from_raw(self.${style_struct.name_lower}_ptr()) };
+ % endfor
+ if !self.visited_style.is_null() {
+ let _ = unsafe { Arc::from_raw(self.visited_style_ptr()) };
+ }
+ }
+}
+
+impl ComputedValuesInner {
+ pub fn new(
+ custom_properties: ComputedCustomProperties,
+ writing_mode: WritingMode,
+ effective_zoom: Zoom,
+ flags: ComputedValueFlags,
+ rules: Option<StrongRuleNode>,
+ visited_style: Option<Arc<ComputedValues>>,
+ % for style_struct in data.style_structs:
+ ${style_struct.ident}: Arc<style_structs::${style_struct.name}>,
+ % endfor
+ ) -> Self {
+ Self {
+ custom_properties,
+ writing_mode,
+ rules,
+ visited_style: visited_style.map_or(ptr::null(), |p| Arc::into_raw(p)) as *const _,
+ flags,
+ effective_zoom,
+ % for style_struct in data.style_structs:
+ ${style_struct.gecko_name}: Arc::into_raw(${style_struct.ident}) as *const _,
+ % endfor
+ }
+ }
+
+ fn to_outer(self, pseudo: Option<<&PseudoElement>) -> Arc<ComputedValues> {
+ let pseudo_ty = match pseudo {
+ Some(p) => p.pseudo_type(),
+ None => structs::PseudoStyleType::NotPseudo,
+ };
+ unsafe {
+ let mut arc = UniqueArc::<ComputedValues>::new_uninit();
+ bindings::Gecko_ComputedStyle_Init(
+ arc.as_mut_ptr() as *mut _,
+ &self,
+ pseudo_ty,
+ );
+ // We're simulating move semantics by having C++ do a memcpy and
+ // then forgetting it on this end.
+ forget(self);
+ UniqueArc::assume_init(arc).shareable()
+ }
+ }
+}
+
+impl ops::Deref for ComputedValues {
+ type Target = ComputedValuesInner;
+ #[inline]
+ fn deref(&self) -> &ComputedValuesInner {
+ &self.0.mSource
+ }
+}
+
+impl ops::DerefMut for ComputedValues {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut ComputedValuesInner {
+ &mut self.0.mSource
+ }
+}
+
+impl ComputedValuesInner {
+ /// Returns true if the value of the `content` property would make a
+ /// pseudo-element not rendered.
+ #[inline]
+ pub fn ineffective_content_property(&self) -> bool {
+ self.get_counters().ineffective_content_property()
+ }
+
+ #[inline]
+ fn visited_style_ptr(&self) -> *const ComputedValues {
+ self.visited_style as *const _
+ }
+
+ /// Returns the visited style, if any.
+ pub fn visited_style(&self) -> Option<<&ComputedValues> {
+ unsafe { self.visited_style_ptr().as_ref() }
+ }
+
+ % for style_struct in data.style_structs:
+ #[inline]
+ fn ${style_struct.name_lower}_ptr(&self) -> *const style_structs::${style_struct.name} {
+ // This is sound because the wrapper we create is repr(transparent).
+ self.${style_struct.gecko_name} as *const _
+ }
+
+ #[inline]
+ pub fn clone_${style_struct.name_lower}(&self) -> Arc<style_structs::${style_struct.name}> {
+ unsafe { Arc::from_raw_addrefed(self.${style_struct.name_lower}_ptr()) }
+ }
+ #[inline]
+ pub fn get_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} {
+ unsafe { &*self.${style_struct.name_lower}_ptr() }
+ }
+
+ #[inline]
+ pub fn mutate_${style_struct.name_lower}(&mut self) -> &mut style_structs::${style_struct.name} {
+ unsafe {
+ let mut arc = Arc::from_raw(self.${style_struct.name_lower}_ptr());
+ let ptr = Arc::make_mut(&mut arc) as *mut _;
+ // Sound for the same reason _ptr() is sound.
+ self.${style_struct.gecko_name} = Arc::into_raw(arc) as *const _;
+ &mut *ptr
+ }
+ }
+ % endfor
+}
+
+<%def name="impl_simple_setter(ident, gecko_ffi_name)">
+ #[allow(non_snake_case)]
+ pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
+ ${set_gecko_property(gecko_ffi_name, "From::from(v)")}
+ }
+</%def>
+
+<%def name="impl_simple_clone(ident, gecko_ffi_name)">
+ #[allow(non_snake_case)]
+ pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
+ From::from(self.${gecko_ffi_name}.clone())
+ }
+</%def>
+
+<%def name="impl_simple_copy(ident, gecko_ffi_name, *kwargs)">
+ #[allow(non_snake_case)]
+ pub fn copy_${ident}_from(&mut self, other: &Self) {
+ self.${gecko_ffi_name} = other.${gecko_ffi_name}.clone();
+ }
+
+ #[allow(non_snake_case)]
+ pub fn reset_${ident}(&mut self, other: &Self) {
+ self.copy_${ident}_from(other)
+ }
+</%def>
+
+<%!
+def get_gecko_property(ffi_name, self_param = "self"):
+ return "%s.%s" % (self_param, ffi_name)
+
+def set_gecko_property(ffi_name, expr):
+ return "self.%s = %s;" % (ffi_name, expr)
+%>
+
+<%def name="impl_keyword_setter(ident, gecko_ffi_name, keyword, cast_type='u8')">
+ #[allow(non_snake_case)]
+ pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
+ use crate::properties::longhands::${ident}::computed_value::T as Keyword;
+ // FIXME(bholley): Align binary representations and ditch |match| for cast + static_asserts
+ let result = match v {
+ % for value in keyword.values_for('gecko'):
+ Keyword::${to_camel_case(value)} =>
+ structs::${keyword.gecko_constant(value)} ${keyword.maybe_cast(cast_type)},
+ % endfor
+ };
+ ${set_gecko_property(gecko_ffi_name, "result")}
+ }
+</%def>
+
+<%def name="impl_keyword_clone(ident, gecko_ffi_name, keyword, cast_type='u8')">
+ #[allow(non_snake_case)]
+ pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
+ use crate::properties::longhands::${ident}::computed_value::T as Keyword;
+ // FIXME(bholley): Align binary representations and ditch |match| for cast + static_asserts
+
+ // Some constant macros in the gecko are defined as negative integer(e.g. font-stretch).
+ // And they are convert to signed integer in Rust bindings. We need to cast then
+ // as signed type when we have both signed/unsigned integer in order to use them
+ // as match's arms.
+ // Also, to use same implementation here we use casted constant if we have only singed values.
+ % if keyword.gecko_enum_prefix is None:
+ % for value in keyword.values_for('gecko'):
+ const ${keyword.casted_constant_name(value, cast_type)} : ${cast_type} =
+ structs::${keyword.gecko_constant(value)} as ${cast_type};
+ % endfor
+
+ match ${get_gecko_property(gecko_ffi_name)} as ${cast_type} {
+ % for value in keyword.values_for('gecko'):
+ ${keyword.casted_constant_name(value, cast_type)} => Keyword::${to_camel_case(value)},
+ % endfor
+ % if keyword.gecko_inexhaustive:
+ _ => panic!("Found unexpected value in style struct for ${ident} property"),
+ % endif
+ }
+ % else:
+ match ${get_gecko_property(gecko_ffi_name)} {
+ % for value in keyword.values_for('gecko'):
+ structs::${keyword.gecko_constant(value)} => Keyword::${to_camel_case(value)},
+ % endfor
+ % if keyword.gecko_inexhaustive:
+ _ => panic!("Found unexpected value in style struct for ${ident} property"),
+ % endif
+ }
+ % endif
+ }
+</%def>
+
+<%def name="impl_keyword(ident, gecko_ffi_name, keyword, cast_type='u8', **kwargs)">
+<%call expr="impl_keyword_setter(ident, gecko_ffi_name, keyword, cast_type, **kwargs)"></%call>
+<%call expr="impl_simple_copy(ident, gecko_ffi_name, **kwargs)"></%call>
+<%call expr="impl_keyword_clone(ident, gecko_ffi_name, keyword, cast_type)"></%call>
+</%def>
+
+<%def name="impl_simple(ident, gecko_ffi_name)">
+<%call expr="impl_simple_setter(ident, gecko_ffi_name)"></%call>
+<%call expr="impl_simple_copy(ident, gecko_ffi_name)"></%call>
+<%call expr="impl_simple_clone(ident, gecko_ffi_name)"></%call>
+</%def>
+
+<%def name="impl_border_width(ident, gecko_ffi_name, inherit_from)">
+ #[allow(non_snake_case)]
+ pub fn set_${ident}(&mut self, v: Au) {
+ let value = v.0;
+ self.${inherit_from} = value;
+ self.${gecko_ffi_name} = value;
+ }
+
+ #[allow(non_snake_case)]
+ pub fn copy_${ident}_from(&mut self, other: &Self) {
+ self.${inherit_from} = other.${inherit_from};
+ // NOTE: This is needed to easily handle the `unset` and `initial`
+ // keywords, which are implemented calling this function.
+ //
+ // In practice, this means that we may have an incorrect value here, but
+ // we'll adjust that properly in the style fixup phase.
+ //
+ // FIXME(emilio): We could clean this up a bit special-casing the reset_
+ // function below.
+ self.${gecko_ffi_name} = other.${inherit_from};
+ }
+
+ #[allow(non_snake_case)]
+ pub fn reset_${ident}(&mut self, other: &Self) {
+ self.copy_${ident}_from(other)
+ }
+
+ #[allow(non_snake_case)]
+ pub fn clone_${ident}(&self) -> Au {
+ Au(self.${gecko_ffi_name})
+ }
+</%def>
+
+<%def name="impl_split_style_coord(ident, gecko_ffi_name, index)">
+ #[allow(non_snake_case)]
+ pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
+ self.${gecko_ffi_name}.${index} = v;
+ }
+ #[allow(non_snake_case)]
+ pub fn copy_${ident}_from(&mut self, other: &Self) {
+ self.${gecko_ffi_name}.${index} =
+ other.${gecko_ffi_name}.${index}.clone();
+ }
+ #[allow(non_snake_case)]
+ pub fn reset_${ident}(&mut self, other: &Self) {
+ self.copy_${ident}_from(other)
+ }
+
+ #[allow(non_snake_case)]
+ pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
+ self.${gecko_ffi_name}.${index}.clone()
+ }
+</%def>
+
+<%def name="copy_sides_style_coord(ident)">
+ <% gecko_ffi_name = "m" + to_camel_case(ident) %>
+ #[allow(non_snake_case)]
+ pub fn copy_${ident}_from(&mut self, other: &Self) {
+ % for side in SIDES:
+ self.${gecko_ffi_name}.data_at_mut(${side.index})
+ .copy_from(&other.${gecko_ffi_name}.data_at(${side.index}));
+ % endfor
+ ${ caller.body() }
+ }
+
+ #[allow(non_snake_case)]
+ pub fn reset_${ident}(&mut self, other: &Self) {
+ self.copy_${ident}_from(other)
+ }
+</%def>
+
+<%def name="impl_corner_style_coord(ident, gecko_ffi_name, corner)">
+ #[allow(non_snake_case)]
+ pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
+ self.${gecko_ffi_name}.${corner} = v;
+ }
+ #[allow(non_snake_case)]
+ pub fn copy_${ident}_from(&mut self, other: &Self) {
+ self.${gecko_ffi_name}.${corner} =
+ other.${gecko_ffi_name}.${corner}.clone();
+ }
+ #[allow(non_snake_case)]
+ pub fn reset_${ident}(&mut self, other: &Self) {
+ self.copy_${ident}_from(other)
+ }
+ #[allow(non_snake_case)]
+ pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
+ self.${gecko_ffi_name}.${corner}.clone()
+ }
+</%def>
+
+<%def name="impl_style_struct(style_struct)">
+/// A wrapper for ${style_struct.gecko_ffi_name}, to be able to manually construct / destruct /
+/// clone it.
+#[repr(transparent)]
+pub struct ${style_struct.gecko_struct_name}(ManuallyDrop<structs::${style_struct.gecko_ffi_name}>);
+
+impl ops::Deref for ${style_struct.gecko_struct_name} {
+ type Target = structs::${style_struct.gecko_ffi_name};
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl ops::DerefMut for ${style_struct.gecko_struct_name} {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut <Self as ops::Deref>::Target {
+ &mut self.0
+ }
+}
+
+impl ${style_struct.gecko_struct_name} {
+ #[allow(dead_code, unused_variables)]
+ pub fn default(document: &structs::Document) -> Arc<Self> {
+% if style_struct.document_dependent:
+ unsafe {
+ let mut result = UniqueArc::<Self>::new_uninit();
+ Gecko_Construct_Default_${style_struct.gecko_ffi_name}(
+ result.as_mut_ptr() as *mut _,
+ document,
+ );
+ UniqueArc::assume_init(result).shareable()
+ }
+% else:
+ lazy_static! {
+ static ref DEFAULT: Arc<${style_struct.gecko_struct_name}> = unsafe {
+ let mut result = UniqueArc::<${style_struct.gecko_struct_name}>::new_uninit();
+ Gecko_Construct_Default_${style_struct.gecko_ffi_name}(
+ result.as_mut_ptr() as *mut _,
+ std::ptr::null(),
+ );
+ let arc = UniqueArc::assume_init(result).shareable();
+ arc.mark_as_intentionally_leaked();
+ arc
+ };
+ };
+ DEFAULT.clone()
+% endif
+ }
+}
+
+impl Drop for ${style_struct.gecko_struct_name} {
+ fn drop(&mut self) {
+ unsafe {
+ Gecko_Destroy_${style_struct.gecko_ffi_name}(&mut **self);
+ }
+ }
+}
+impl Clone for ${style_struct.gecko_struct_name} {
+ fn clone(&self) -> Self {
+ unsafe {
+ let mut result = MaybeUninit::<Self>::uninit();
+ // FIXME(bug 1595895): Zero the memory to keep valgrind happy, but
+ // these looks like Valgrind false-positives at a quick glance.
+ ptr::write_bytes::<Self>(result.as_mut_ptr(), 0, 1);
+ Gecko_CopyConstruct_${style_struct.gecko_ffi_name}(result.as_mut_ptr() as *mut _, &**self);
+ result.assume_init()
+ }
+ }
+}
+</%def>
+
+<%def name="impl_simple_type_with_conversion(ident, gecko_ffi_name)">
+ #[allow(non_snake_case)]
+ pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
+ self.${gecko_ffi_name} = From::from(v)
+ }
+
+ <% impl_simple_copy(ident, gecko_ffi_name) %>
+
+ #[allow(non_snake_case)]
+ pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
+ From::from(self.${gecko_ffi_name})
+ }
+</%def>
+
+<%def name="impl_font_settings(ident, gecko_type, tag_type, value_type, gecko_value_type)">
+ <%
+ gecko_ffi_name = to_camel_case_lower(ident)
+ %>
+
+ pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
+ let iter = v.0.iter().map(|other| structs::${gecko_type} {
+ mTag: other.tag.0,
+ mValue: other.value as ${gecko_value_type},
+ });
+ self.mFont.${gecko_ffi_name}.assign_from_iter_pod(iter);
+ }
+
+ pub fn copy_${ident}_from(&mut self, other: &Self) {
+ let iter = other.mFont.${gecko_ffi_name}.iter().map(|s| *s);
+ self.mFont.${gecko_ffi_name}.assign_from_iter_pod(iter);
+ }
+
+ pub fn reset_${ident}(&mut self, other: &Self) {
+ self.copy_${ident}_from(other)
+ }
+
+ pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
+ use crate::values::generics::font::{FontSettings, FontTag, ${tag_type}};
+
+ FontSettings(
+ self.mFont.${gecko_ffi_name}.iter().map(|gecko_font_setting| {
+ ${tag_type} {
+ tag: FontTag(gecko_font_setting.mTag),
+ value: gecko_font_setting.mValue as ${value_type},
+ }
+ }).collect::<Vec<_>>().into_boxed_slice()
+ )
+ }
+</%def>
+
+<%def name="impl_trait(style_struct_name, skip_longhands='')">
+<%
+ style_struct = next(x for x in data.style_structs if x.name == style_struct_name)
+ longhands = [x for x in style_struct.longhands
+ if not (skip_longhands == "*" or x.name in skip_longhands.split())]
+
+ def longhand_method(longhand):
+ args = dict(ident=longhand.ident, gecko_ffi_name=longhand.gecko_ffi_name)
+
+ if longhand.logical:
+ return
+ # get the method and pass additional keyword or type-specific arguments
+ if longhand.keyword:
+ method = impl_keyword
+ args.update(keyword=longhand.keyword)
+ if "font" in longhand.ident:
+ args.update(cast_type=longhand.cast_type)
+ else:
+ method = impl_simple
+
+ method(**args)
+%>
+impl ${style_struct.gecko_struct_name} {
+ /*
+ * Manually-Implemented Methods.
+ */
+ ${caller.body().strip()}
+
+ /*
+ * Auto-Generated Methods.
+ */
+ <%
+ for longhand in longhands:
+ longhand_method(longhand)
+ %>
+}
+</%def>
+
+<%!
+class Side(object):
+ def __init__(self, name, index):
+ self.name = name
+ self.ident = name.lower()
+ self.index = index
+
+SIDES = [Side("Top", 0), Side("Right", 1), Side("Bottom", 2), Side("Left", 3)]
+CORNERS = ["top_left", "top_right", "bottom_right", "bottom_left"]
+%>
+
+#[allow(dead_code)]
+fn static_assert() {
+ // Note: using the above technique with an enum hits a rust bug when |structs| is in a different crate.
+ % for side in SIDES:
+ { const DETAIL: u32 = [0][(structs::Side::eSide${side.name} as usize != ${side.index}) as usize]; let _ = DETAIL; }
+ % endfor
+}
+
+
+<% skip_border_longhands = " ".join(["border-{0}-{1}".format(x.ident, y)
+ for x in SIDES
+ for y in ["style", "width"]] +
+ ["border-{0}-radius".format(x.replace("_", "-"))
+ for x in CORNERS]) %>
+
+<%self:impl_trait style_struct_name="Border"
+ skip_longhands="${skip_border_longhands} border-image-repeat">
+ % for side in SIDES:
+ pub fn set_border_${side.ident}_style(&mut self, v: BorderStyle) {
+ self.mBorderStyle[${side.index}] = v;
+
+ // This is needed because the initial mComputedBorder value is set to
+ // zero.
+ //
+ // In order to compute stuff, we start from the initial struct, and keep
+ // going down the tree applying properties.
+ //
+ // That means, effectively, that when we set border-style to something
+ // non-hidden, we should use the initial border instead.
+ //
+ // Servo stores the initial border-width in the initial struct, and then
+ // adjusts as needed in the fixup phase. This means that the initial
+ // struct is technically not valid without fixups, and that you lose
+ // pretty much any sharing of the initial struct, which is kind of
+ // unfortunate.
+ //
+ // Gecko has two fields for this, one that stores the "specified"
+ // border, and other that stores the actual computed one. That means
+ // that when we set border-style, border-width may change and we need to
+ // sync back to the specified one. This is what this function does.
+ //
+ // Note that this doesn't impose any dependency in the order of
+ // computation of the properties. This is only relevant if border-style
+ // is specified, but border-width isn't. If border-width is specified at
+ // some point, the two mBorder and mComputedBorder fields would be the
+ // same already.
+ //
+ // Once we're here, we know that we'll run style fixups, so it's fine to
+ // just copy the specified border here, we'll adjust it if it's
+ // incorrect later.
+ self.mComputedBorder.${side.ident} = self.mBorder.${side.ident};
+ }
+
+ pub fn copy_border_${side.ident}_style_from(&mut self, other: &Self) {
+ self.set_border_${side.ident}_style(other.mBorderStyle[${side.index}]);
+ }
+
+ pub fn reset_border_${side.ident}_style(&mut self, other: &Self) {
+ self.copy_border_${side.ident}_style_from(other);
+ }
+
+ #[inline]
+ pub fn clone_border_${side.ident}_style(&self) -> BorderStyle {
+ self.mBorderStyle[${side.index}]
+ }
+
+ ${impl_border_width("border_%s_width" % side.ident, "mComputedBorder.%s" % side.ident, "mBorder.%s" % side.ident)}
+
+ pub fn border_${side.ident}_has_nonzero_width(&self) -> bool {
+ self.mComputedBorder.${side.ident} != 0
+ }
+ % endfor
+
+ % for corner in CORNERS:
+ <% impl_corner_style_coord("border_%s_radius" % corner,
+ "mBorderRadius",
+ corner) %>
+ % endfor
+
+ <%
+ border_image_repeat_keywords = ["Stretch", "Repeat", "Round", "Space"]
+ %>
+
+ pub fn set_border_image_repeat(&mut self, v: longhands::border_image_repeat::computed_value::T) {
+ use crate::values::specified::border::BorderImageRepeatKeyword;
+ use crate::gecko_bindings::structs::StyleBorderImageRepeat;
+
+ % for i, side in enumerate(["H", "V"]):
+ self.mBorderImageRepeat${side} = match v.${i} {
+ % for keyword in border_image_repeat_keywords:
+ BorderImageRepeatKeyword::${keyword} => StyleBorderImageRepeat::${keyword},
+ % endfor
+ };
+ % endfor
+ }
+
+ pub fn copy_border_image_repeat_from(&mut self, other: &Self) {
+ self.mBorderImageRepeatH = other.mBorderImageRepeatH;
+ self.mBorderImageRepeatV = other.mBorderImageRepeatV;
+ }
+
+ pub fn reset_border_image_repeat(&mut self, other: &Self) {
+ self.copy_border_image_repeat_from(other)
+ }
+
+ pub fn clone_border_image_repeat(&self) -> longhands::border_image_repeat::computed_value::T {
+ use crate::values::specified::border::BorderImageRepeatKeyword;
+ use crate::gecko_bindings::structs::StyleBorderImageRepeat;
+
+ % for side in ["H", "V"]:
+ let servo_${side.lower()} = match self.mBorderImageRepeat${side} {
+ % for keyword in border_image_repeat_keywords:
+ StyleBorderImageRepeat::${keyword} => BorderImageRepeatKeyword::${keyword},
+ % endfor
+ };
+ % endfor
+ longhands::border_image_repeat::computed_value::T(servo_h, servo_v)
+ }
+</%self:impl_trait>
+
+<% skip_scroll_margin_longhands = " ".join(["scroll-margin-%s" % x.ident for x in SIDES]) %>
+<% skip_margin_longhands = " ".join(["margin-%s" % x.ident for x in SIDES]) %>
+<%self:impl_trait style_struct_name="Margin"
+ skip_longhands="${skip_margin_longhands}
+ ${skip_scroll_margin_longhands}">
+ % for side in SIDES:
+ <% impl_split_style_coord("margin_%s" % side.ident,
+ "mMargin",
+ side.index) %>
+ <% impl_split_style_coord("scroll_margin_%s" % side.ident,
+ "mScrollMargin",
+ side.index) %>
+ % endfor
+</%self:impl_trait>
+
+<% skip_scroll_padding_longhands = " ".join(["scroll-padding-%s" % x.ident for x in SIDES]) %>
+<% skip_padding_longhands = " ".join(["padding-%s" % x.ident for x in SIDES]) %>
+<%self:impl_trait style_struct_name="Padding"
+ skip_longhands="${skip_padding_longhands}
+ ${skip_scroll_padding_longhands}">
+
+ % for side in SIDES:
+ <% impl_split_style_coord("padding_%s" % side.ident,
+ "mPadding",
+ side.index) %>
+ <% impl_split_style_coord("scroll_padding_%s" % side.ident, "mScrollPadding", side.index) %>
+ % endfor
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="Page">
+</%self:impl_trait>
+
+<% skip_position_longhands = " ".join(x.ident for x in SIDES) %>
+<%self:impl_trait style_struct_name="Position"
+ skip_longhands="${skip_position_longhands}
+ masonry-auto-flow">
+ % for side in SIDES:
+ <% impl_split_style_coord(side.ident, "mOffset", side.index) %>
+ % endfor
+ pub fn set_computed_justify_items(&mut self, v: values::specified::JustifyItems) {
+ debug_assert_ne!(v.0, crate::values::specified::align::AlignFlags::LEGACY);
+ self.mJustifyItems.computed = v;
+ }
+
+ ${impl_simple_type_with_conversion("masonry_auto_flow", "mMasonryAutoFlow")}
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="Outline"
+ skip_longhands="outline-style outline-width">
+
+ pub fn set_outline_style(&mut self, v: longhands::outline_style::computed_value::T) {
+ self.mOutlineStyle = v;
+ // NB: This is needed to correctly handling the initial value of
+ // outline-width when outline-style changes, see the
+ // update_border_${side.ident} comment for more details.
+ self.mActualOutlineWidth = self.mOutlineWidth;
+ }
+
+ pub fn copy_outline_style_from(&mut self, other: &Self) {
+ self.set_outline_style(other.mOutlineStyle);
+ }
+
+ pub fn reset_outline_style(&mut self, other: &Self) {
+ self.copy_outline_style_from(other)
+ }
+
+ pub fn clone_outline_style(&self) -> longhands::outline_style::computed_value::T {
+ self.mOutlineStyle.clone()
+ }
+
+ ${impl_border_width("outline_width", "mActualOutlineWidth", "mOutlineWidth")}
+
+ pub fn outline_has_nonzero_width(&self) -> bool {
+ self.mActualOutlineWidth != 0
+ }
+</%self:impl_trait>
+
+<% skip_font_longhands = """font-family font-size font-size-adjust font-weight
+ font-style font-stretch -x-lang
+ font-variant-alternates font-variant-east-asian
+ font-variant-ligatures font-variant-numeric
+ font-language-override font-feature-settings
+ font-variation-settings -moz-min-font-size-ratio""" %>
+<%self:impl_trait style_struct_name="Font"
+ skip_longhands="${skip_font_longhands}">
+
+ // Negative numbers are invalid at parse time, but <integer> is still an
+ // i32.
+ <% impl_font_settings("font_feature_settings", "gfxFontFeature", "FeatureTagValue", "i32", "u32") %>
+ <% impl_font_settings("font_variation_settings", "gfxFontVariation", "VariationValue", "f32", "f32") %>
+
+ pub fn unzoom_fonts(&mut self, device: &Device) {
+ use crate::values::generics::NonNegative;
+ self.mSize = NonNegative(device.unzoom_text(self.mSize.0));
+ self.mScriptUnconstrainedSize = NonNegative(device.unzoom_text(self.mScriptUnconstrainedSize.0));
+ self.mFont.size = NonNegative(device.unzoom_text(self.mFont.size.0));
+ }
+
+ pub fn copy_font_size_from(&mut self, other: &Self) {
+ self.mScriptUnconstrainedSize = other.mScriptUnconstrainedSize;
+
+ self.mSize = other.mScriptUnconstrainedSize;
+ // NOTE: Intentionally not copying from mFont.size. The cascade process
+ // recomputes the used size as needed.
+ self.mFont.size = other.mSize;
+ self.mFontSizeKeyword = other.mFontSizeKeyword;
+
+ // TODO(emilio): Should we really copy over these two?
+ self.mFontSizeFactor = other.mFontSizeFactor;
+ self.mFontSizeOffset = other.mFontSizeOffset;
+ }
+
+ pub fn reset_font_size(&mut self, other: &Self) {
+ self.copy_font_size_from(other)
+ }
+
+ pub fn set_font_size(&mut self, v: FontSize) {
+ let computed_size = v.computed_size;
+ self.mScriptUnconstrainedSize = computed_size;
+
+ // These two may be changed from Cascade::fixup_font_stuff.
+ self.mSize = computed_size;
+ // NOTE: Intentionally not copying from used_size. The cascade process
+ // recomputes the used size as needed.
+ self.mFont.size = computed_size;
+
+ self.mFontSizeKeyword = v.keyword_info.kw;
+ self.mFontSizeFactor = v.keyword_info.factor;
+ self.mFontSizeOffset = v.keyword_info.offset;
+ }
+
+ pub fn clone_font_size(&self) -> FontSize {
+ use crate::values::specified::font::KeywordInfo;
+
+ FontSize {
+ computed_size: self.mSize,
+ used_size: self.mFont.size,
+ keyword_info: KeywordInfo {
+ kw: self.mFontSizeKeyword,
+ factor: self.mFontSizeFactor,
+ offset: self.mFontSizeOffset,
+ }
+ }
+ }
+
+ ${impl_simple('font_weight', 'mFont.weight')}
+ ${impl_simple('font_stretch', 'mFont.stretch')}
+ ${impl_simple('font_style', 'mFont.style')}
+
+ ${impl_simple("font_variant_alternates", "mFont.variantAlternates")}
+
+ ${impl_simple("font_size_adjust", "mFont.sizeAdjust")}
+
+ ${impl_simple("font_family", "mFont.family")}
+
+ #[allow(non_snake_case)]
+ pub fn set__x_lang(&mut self, v: longhands::_x_lang::computed_value::T) {
+ let ptr = v.0.as_ptr();
+ forget(v);
+ unsafe {
+ Gecko_nsStyleFont_SetLang(&mut **self, ptr);
+ }
+ }
+
+ #[allow(non_snake_case)]
+ pub fn copy__x_lang_from(&mut self, other: &Self) {
+ unsafe {
+ Gecko_nsStyleFont_CopyLangFrom(&mut **self, &**other);
+ }
+ }
+
+ #[allow(non_snake_case)]
+ pub fn reset__x_lang(&mut self, other: &Self) {
+ self.copy__x_lang_from(other)
+ }
+
+ #[allow(non_snake_case)]
+ pub fn clone__x_lang(&self) -> longhands::_x_lang::computed_value::T {
+ longhands::_x_lang::computed_value::T(unsafe {
+ Atom::from_raw(self.mLanguage.mRawPtr)
+ })
+ }
+
+
+ ${impl_simple_type_with_conversion("font_language_override", "mFont.languageOverride")}
+ ${impl_simple_type_with_conversion("font_variant_ligatures", "mFont.variantLigatures")}
+ ${impl_simple_type_with_conversion("font_variant_east_asian", "mFont.variantEastAsian")}
+ ${impl_simple_type_with_conversion("font_variant_numeric", "mFont.variantNumeric")}
+
+ #[allow(non_snake_case)]
+ pub fn clone__moz_min_font_size_ratio(
+ &self,
+ ) -> longhands::_moz_min_font_size_ratio::computed_value::T {
+ Percentage(self.mMinFontSizeRatio as f32 / 100.)
+ }
+
+ #[allow(non_snake_case)]
+ pub fn set__moz_min_font_size_ratio(&mut self, v: longhands::_moz_min_font_size_ratio::computed_value::T) {
+ let scaled = v.0 * 100.;
+ let percentage = if scaled > 255. {
+ 255.
+ } else if scaled < 0. {
+ 0.
+ } else {
+ scaled
+ };
+
+ self.mMinFontSizeRatio = percentage as u8;
+ }
+
+ ${impl_simple_copy('_moz_min_font_size_ratio', 'mMinFontSizeRatio')}
+</%self:impl_trait>
+
+<%def name="impl_coordinated_property_copy(type, ident, gecko_ffi_name)">
+ #[allow(non_snake_case)]
+ pub fn copy_${type}_${ident}_from(&mut self, other: &Self) {
+ self.m${to_camel_case(type)}s.ensure_len(other.m${to_camel_case(type)}s.len());
+
+ let count = other.m${to_camel_case(type)}${gecko_ffi_name}Count;
+ self.m${to_camel_case(type)}${gecko_ffi_name}Count = count;
+
+ let iter = self.m${to_camel_case(type)}s.iter_mut().take(count as usize).zip(
+ other.m${to_camel_case(type)}s.iter()
+ );
+
+ for (ours, others) in iter {
+ ours.m${gecko_ffi_name} = others.m${gecko_ffi_name}.clone();
+ }
+ }
+ #[allow(non_snake_case)]
+ pub fn reset_${type}_${ident}(&mut self, other: &Self) {
+ self.copy_${type}_${ident}_from(other)
+ }
+</%def>
+
+<%def name="impl_coordinated_property_count(type, ident, gecko_ffi_name)">
+ #[allow(non_snake_case)]
+ pub fn ${type}_${ident}_count(&self) -> usize {
+ self.m${to_camel_case(type)}${gecko_ffi_name}Count as usize
+ }
+</%def>
+
+<%def name="impl_coordinated_property(type, ident, gecko_ffi_name)">
+ #[allow(non_snake_case)]
+ pub fn set_${type}_${ident}<I>(&mut self, v: I)
+ where
+ I: IntoIterator<Item = longhands::${type}_${ident}::computed_value::single_value::T>,
+ I::IntoIter: ExactSizeIterator + Clone
+ {
+ let v = v.into_iter();
+ debug_assert_ne!(v.len(), 0);
+ let input_len = v.len();
+ self.m${to_camel_case(type)}s.ensure_len(input_len);
+
+ self.m${to_camel_case(type)}${gecko_ffi_name}Count = input_len as u32;
+ for (gecko, servo) in self.m${to_camel_case(type)}s.iter_mut().take(input_len as usize).zip(v) {
+ gecko.m${gecko_ffi_name} = servo;
+ }
+ }
+ #[allow(non_snake_case)]
+ pub fn ${type}_${ident}_at(&self, index: usize)
+ -> longhands::${type}_${ident}::computed_value::SingleComputedValue {
+ self.m${to_camel_case(type)}s[index % self.${type}_${ident}_count()].m${gecko_ffi_name}.clone()
+ }
+ ${impl_coordinated_property_copy(type, ident, gecko_ffi_name)}
+ ${impl_coordinated_property_count(type, ident, gecko_ffi_name)}
+</%def>
+
+<% skip_box_longhands= """display contain""" %>
+<%self:impl_trait style_struct_name="Box" skip_longhands="${skip_box_longhands}">
+ #[inline]
+ pub fn set_display(&mut self, v: longhands::display::computed_value::T) {
+ self.mDisplay = v;
+ self.mOriginalDisplay = v;
+ }
+
+ #[inline]
+ pub fn copy_display_from(&mut self, other: &Self) {
+ self.set_display(other.mDisplay);
+ }
+
+ #[inline]
+ pub fn reset_display(&mut self, other: &Self) {
+ self.copy_display_from(other)
+ }
+
+ #[inline]
+ pub fn set_adjusted_display(
+ &mut self,
+ v: longhands::display::computed_value::T,
+ _is_item_or_root: bool
+ ) {
+ self.mDisplay = v;
+ }
+
+ #[inline]
+ pub fn clone_display(&self) -> longhands::display::computed_value::T {
+ self.mDisplay
+ }
+
+ #[inline]
+ pub fn set_contain(&mut self, v: longhands::contain::computed_value::T) {
+ self.mContain = v;
+ self.mEffectiveContainment = v;
+ }
+
+ #[inline]
+ pub fn copy_contain_from(&mut self, other: &Self) {
+ self.set_contain(other.mContain);
+ }
+
+ #[inline]
+ pub fn reset_contain(&mut self, other: &Self) {
+ self.copy_contain_from(other)
+ }
+
+ #[inline]
+ pub fn clone_contain(&self) -> longhands::contain::computed_value::T {
+ self.mContain
+ }
+
+ #[inline]
+ pub fn set_effective_containment(
+ &mut self,
+ v: longhands::contain::computed_value::T
+ ) {
+ self.mEffectiveContainment = v;
+ }
+
+ #[inline]
+ pub fn clone_effective_containment(&self) -> longhands::contain::computed_value::T {
+ self.mEffectiveContainment
+ }
+</%self:impl_trait>
+
+<%def name="simple_image_array_property(name, shorthand, field_name)">
+ <%
+ image_layers_field = "mImage" if shorthand == "background" else "mMask"
+ copy_simple_image_array_property(name, shorthand, image_layers_field, field_name)
+ %>
+
+ pub fn set_${shorthand}_${name}<I>(&mut self, v: I)
+ where I: IntoIterator<Item=longhands::${shorthand}_${name}::computed_value::single_value::T>,
+ I::IntoIter: ExactSizeIterator
+ {
+ use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType;
+ let v = v.into_iter();
+
+ unsafe {
+ Gecko_EnsureImageLayersLength(&mut self.${image_layers_field}, v.len(),
+ LayerType::${shorthand.title()});
+ }
+
+ self.${image_layers_field}.${field_name}Count = v.len() as u32;
+ for (servo, geckolayer) in v.zip(self.${image_layers_field}.mLayers.iter_mut()) {
+ geckolayer.${field_name} = {
+ ${caller.body()}
+ };
+ }
+ }
+</%def>
+
+<%def name="copy_simple_image_array_property(name, shorthand, layers_field_name, field_name)">
+ pub fn copy_${shorthand}_${name}_from(&mut self, other: &Self) {
+ use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType;
+
+ let count = other.${layers_field_name}.${field_name}Count;
+ unsafe {
+ Gecko_EnsureImageLayersLength(&mut self.${layers_field_name},
+ count as usize,
+ LayerType::${shorthand.title()});
+ }
+ // FIXME(emilio): This may be bogus in the same way as bug 1426246.
+ for (layer, other) in self.${layers_field_name}.mLayers.iter_mut()
+ .zip(other.${layers_field_name}.mLayers.iter())
+ .take(count as usize) {
+ layer.${field_name} = other.${field_name}.clone();
+ }
+ self.${layers_field_name}.${field_name}Count = count;
+ }
+
+ pub fn reset_${shorthand}_${name}(&mut self, other: &Self) {
+ self.copy_${shorthand}_${name}_from(other)
+ }
+</%def>
+
+<%def name="impl_simple_image_array_property(name, shorthand, layer_field_name, field_name, struct_name)">
+ <%
+ ident = "%s_%s" % (shorthand, name)
+ style_struct = next(x for x in data.style_structs if x.name == struct_name)
+ longhand = next(x for x in style_struct.longhands if x.ident == ident)
+ keyword = longhand.keyword
+ %>
+
+ <% copy_simple_image_array_property(name, shorthand, layer_field_name, field_name) %>
+
+ pub fn set_${ident}<I>(&mut self, v: I)
+ where
+ I: IntoIterator<Item=longhands::${ident}::computed_value::single_value::T>,
+ I::IntoIter: ExactSizeIterator,
+ {
+ use crate::properties::longhands::${ident}::single_value::computed_value::T as Keyword;
+ use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType;
+
+ let v = v.into_iter();
+
+ unsafe {
+ Gecko_EnsureImageLayersLength(&mut self.${layer_field_name}, v.len(),
+ LayerType::${shorthand.title()});
+ }
+
+ self.${layer_field_name}.${field_name}Count = v.len() as u32;
+ for (servo, geckolayer) in v.zip(self.${layer_field_name}.mLayers.iter_mut()) {
+ geckolayer.${field_name} = {
+ match servo {
+ % for value in keyword.values_for("gecko"):
+ Keyword::${to_camel_case(value)} =>
+ structs::${keyword.gecko_constant(value)} ${keyword.maybe_cast('u8')},
+ % endfor
+ }
+ };
+ }
+ }
+
+ pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
+ use crate::properties::longhands::${ident}::single_value::computed_value::T as Keyword;
+
+ % if keyword.needs_cast():
+ % for value in keyword.values_for('gecko'):
+ const ${keyword.casted_constant_name(value, "u8")} : u8 =
+ structs::${keyword.gecko_constant(value)} as u8;
+ % endfor
+ % endif
+
+ longhands::${ident}::computed_value::List(
+ self.${layer_field_name}.mLayers.iter()
+ .take(self.${layer_field_name}.${field_name}Count as usize)
+ .map(|ref layer| {
+ match layer.${field_name} {
+ % for value in longhand.keyword.values_for("gecko"):
+ % if keyword.needs_cast():
+ ${keyword.casted_constant_name(value, "u8")}
+ % else:
+ structs::${keyword.gecko_constant(value)}
+ % endif
+ => Keyword::${to_camel_case(value)},
+ % endfor
+ % if keyword.gecko_inexhaustive:
+ _ => panic!("Found unexpected value in style struct for ${ident} property"),
+ % endif
+ }
+ }).collect()
+ )
+ }
+</%def>
+
+<%def name="impl_common_image_layer_properties(shorthand)">
+ <%
+ if shorthand == "background":
+ image_layers_field = "mImage"
+ struct_name = "Background"
+ else:
+ image_layers_field = "mMask"
+ struct_name = "SVG"
+ %>
+
+ <%self:simple_image_array_property name="repeat" shorthand="${shorthand}" field_name="mRepeat">
+ use crate::values::specified::background::BackgroundRepeatKeyword;
+ use crate::gecko_bindings::structs::nsStyleImageLayers_Repeat;
+ use crate::gecko_bindings::structs::StyleImageLayerRepeat;
+
+ fn to_ns(repeat: BackgroundRepeatKeyword) -> StyleImageLayerRepeat {
+ match repeat {
+ BackgroundRepeatKeyword::Repeat => StyleImageLayerRepeat::Repeat,
+ BackgroundRepeatKeyword::Space => StyleImageLayerRepeat::Space,
+ BackgroundRepeatKeyword::Round => StyleImageLayerRepeat::Round,
+ BackgroundRepeatKeyword::NoRepeat => StyleImageLayerRepeat::NoRepeat,
+ }
+ }
+
+ let repeat_x = to_ns(servo.0);
+ let repeat_y = to_ns(servo.1);
+ nsStyleImageLayers_Repeat {
+ mXRepeat: repeat_x,
+ mYRepeat: repeat_y,
+ }
+ </%self:simple_image_array_property>
+
+ pub fn clone_${shorthand}_repeat(&self) -> longhands::${shorthand}_repeat::computed_value::T {
+ use crate::properties::longhands::${shorthand}_repeat::single_value::computed_value::T;
+ use crate::values::specified::background::BackgroundRepeatKeyword;
+ use crate::gecko_bindings::structs::StyleImageLayerRepeat;
+
+ fn to_servo(repeat: StyleImageLayerRepeat) -> BackgroundRepeatKeyword {
+ match repeat {
+ StyleImageLayerRepeat::Repeat => BackgroundRepeatKeyword::Repeat,
+ StyleImageLayerRepeat::Space => BackgroundRepeatKeyword::Space,
+ StyleImageLayerRepeat::Round => BackgroundRepeatKeyword::Round,
+ StyleImageLayerRepeat::NoRepeat => BackgroundRepeatKeyword::NoRepeat,
+ _ => panic!("Found unexpected value in style struct for ${shorthand}_repeat property"),
+ }
+ }
+
+ longhands::${shorthand}_repeat::computed_value::List(
+ self.${image_layers_field}.mLayers.iter()
+ .take(self.${image_layers_field}.mRepeatCount as usize)
+ .map(|ref layer| {
+ T(to_servo(layer.mRepeat.mXRepeat), to_servo(layer.mRepeat.mYRepeat))
+ }).collect()
+ )
+ }
+
+ <% impl_simple_image_array_property("clip", shorthand, image_layers_field, "mClip", struct_name) %>
+ <% impl_simple_image_array_property("origin", shorthand, image_layers_field, "mOrigin", struct_name) %>
+
+ % for (orientation, keyword) in [("x", "horizontal"), ("y", "vertical")]:
+ pub fn copy_${shorthand}_position_${orientation}_from(&mut self, other: &Self) {
+ use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType;
+
+ let count = other.${image_layers_field}.mPosition${orientation.upper()}Count;
+
+ unsafe {
+ Gecko_EnsureImageLayersLength(&mut self.${image_layers_field},
+ count as usize,
+ LayerType::${shorthand.capitalize()});
+ }
+
+ for (layer, other) in self.${image_layers_field}.mLayers.iter_mut()
+ .zip(other.${image_layers_field}.mLayers.iter())
+ .take(count as usize) {
+ layer.mPosition.${keyword} = other.mPosition.${keyword}.clone();
+ }
+ self.${image_layers_field}.mPosition${orientation.upper()}Count = count;
+ }
+
+ pub fn reset_${shorthand}_position_${orientation}(&mut self, other: &Self) {
+ self.copy_${shorthand}_position_${orientation}_from(other)
+ }
+
+ pub fn clone_${shorthand}_position_${orientation}(&self)
+ -> longhands::${shorthand}_position_${orientation}::computed_value::T {
+ longhands::${shorthand}_position_${orientation}::computed_value::List(
+ self.${image_layers_field}.mLayers.iter()
+ .take(self.${image_layers_field}.mPosition${orientation.upper()}Count as usize)
+ .map(|position| position.mPosition.${keyword}.clone())
+ .collect()
+ )
+ }
+
+ pub fn set_${shorthand}_position_${orientation[0]}<I>(&mut self,
+ v: I)
+ where I: IntoIterator<Item = longhands::${shorthand}_position_${orientation[0]}
+ ::computed_value::single_value::T>,
+ I::IntoIter: ExactSizeIterator
+ {
+ use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType;
+
+ let v = v.into_iter();
+
+ unsafe {
+ Gecko_EnsureImageLayersLength(&mut self.${image_layers_field}, v.len(),
+ LayerType::${shorthand.capitalize()});
+ }
+
+ self.${image_layers_field}.mPosition${orientation[0].upper()}Count = v.len() as u32;
+ for (servo, geckolayer) in v.zip(self.${image_layers_field}
+ .mLayers.iter_mut()) {
+ geckolayer.mPosition.${keyword} = servo;
+ }
+ }
+ % endfor
+
+ <%self:simple_image_array_property name="size" shorthand="${shorthand}" field_name="mSize">
+ servo
+ </%self:simple_image_array_property>
+
+ pub fn clone_${shorthand}_size(&self) -> longhands::${shorthand}_size::computed_value::T {
+ longhands::${shorthand}_size::computed_value::List(
+ self.${image_layers_field}.mLayers.iter().map(|layer| layer.mSize.clone()).collect()
+ )
+ }
+
+ pub fn copy_${shorthand}_image_from(&mut self, other: &Self) {
+ use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType;
+ unsafe {
+ let count = other.${image_layers_field}.mImageCount;
+ Gecko_EnsureImageLayersLength(&mut self.${image_layers_field},
+ count as usize,
+ LayerType::${shorthand.capitalize()});
+
+ for (layer, other) in self.${image_layers_field}.mLayers.iter_mut()
+ .zip(other.${image_layers_field}.mLayers.iter())
+ .take(count as usize) {
+ layer.mImage = other.mImage.clone();
+ }
+ self.${image_layers_field}.mImageCount = count;
+ }
+ }
+
+ pub fn reset_${shorthand}_image(&mut self, other: &Self) {
+ self.copy_${shorthand}_image_from(other)
+ }
+
+ #[allow(unused_variables)]
+ pub fn set_${shorthand}_image<I>(&mut self, images: I)
+ where I: IntoIterator<Item = longhands::${shorthand}_image::computed_value::single_value::T>,
+ I::IntoIter: ExactSizeIterator
+ {
+ use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType;
+
+ let images = images.into_iter();
+
+ unsafe {
+ Gecko_EnsureImageLayersLength(
+ &mut self.${image_layers_field},
+ images.len(),
+ LayerType::${shorthand.title()},
+ );
+ }
+
+ self.${image_layers_field}.mImageCount = images.len() as u32;
+ for (image, geckoimage) in images.zip(self.${image_layers_field}
+ .mLayers.iter_mut()) {
+ geckoimage.mImage = image;
+ }
+ }
+
+ pub fn clone_${shorthand}_image(&self) -> longhands::${shorthand}_image::computed_value::T {
+ longhands::${shorthand}_image::computed_value::List(
+ self.${image_layers_field}.mLayers.iter()
+ .take(self.${image_layers_field}.mImageCount as usize)
+ .map(|layer| layer.mImage.clone())
+ .collect()
+ )
+ }
+
+ <%
+ fill_fields = "mRepeat mClip mOrigin mPositionX mPositionY mImage mSize"
+ if shorthand == "background":
+ fill_fields += " mAttachment mBlendMode"
+ else:
+ # mSourceURI uses mImageCount
+ fill_fields += " mMaskMode mComposite"
+ %>
+ pub fn fill_arrays(&mut self) {
+ use crate::gecko_bindings::bindings::Gecko_FillAllImageLayers;
+ use std::cmp;
+ let mut max_len = 1;
+ % for member in fill_fields.split():
+ max_len = cmp::max(max_len, self.${image_layers_field}.${member}Count);
+ % endfor
+ unsafe {
+ // While we could do this manually, we'd need to also manually
+ // run all the copy constructors, so we just delegate to gecko
+ Gecko_FillAllImageLayers(&mut self.${image_layers_field}, max_len);
+ }
+ }
+</%def>
+
+// TODO: Gecko accepts lists in most background-related properties. We just use
+// the first element (which is the common case), but at some point we want to
+// add support for parsing these lists in servo and pushing to nsTArray's.
+<% skip_background_longhands = """background-repeat
+ background-image background-clip
+ background-origin background-attachment
+ background-size background-position
+ background-blend-mode
+ background-position-x
+ background-position-y""" %>
+<%self:impl_trait style_struct_name="Background"
+ skip_longhands="${skip_background_longhands}">
+
+ <% impl_common_image_layer_properties("background") %>
+ <% impl_simple_image_array_property("attachment", "background", "mImage", "mAttachment", "Background") %>
+ <% impl_simple_image_array_property("blend_mode", "background", "mImage", "mBlendMode", "Background") %>
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="List" skip_longhands="list-style-type">
+ pub fn set_list_style_type(&mut self, v: longhands::list_style_type::computed_value::T) {
+ use nsstring::{nsACString, nsCStr};
+ use self::longhands::list_style_type::computed_value::T;
+ match v {
+ T::None => unsafe {
+ bindings::Gecko_SetCounterStyleToNone(&mut self.mCounterStyle)
+ }
+ T::CounterStyle(s) => s.to_gecko_value(&mut self.mCounterStyle),
+ T::String(s) => unsafe {
+ bindings::Gecko_SetCounterStyleToString(
+ &mut self.mCounterStyle,
+ &nsCStr::from(&s) as &nsACString,
+ )
+ }
+ }
+ }
+
+ pub fn copy_list_style_type_from(&mut self, other: &Self) {
+ unsafe {
+ Gecko_CopyCounterStyle(&mut self.mCounterStyle, &other.mCounterStyle);
+ }
+ }
+
+ pub fn reset_list_style_type(&mut self, other: &Self) {
+ self.copy_list_style_type_from(other)
+ }
+
+ pub fn clone_list_style_type(&self) -> longhands::list_style_type::computed_value::T {
+ use self::longhands::list_style_type::computed_value::T;
+ use crate::values::Either;
+ use crate::values::generics::CounterStyle;
+ use crate::gecko_bindings::bindings;
+
+ let name = unsafe {
+ bindings::Gecko_CounterStyle_GetName(&self.mCounterStyle)
+ };
+ if !name.is_null() {
+ let name = unsafe { Atom::from_raw(name) };
+ if name == atom!("none") {
+ return T::None;
+ }
+ }
+ let result = CounterStyle::from_gecko_value(&self.mCounterStyle);
+ match result {
+ Either::First(counter_style) => T::CounterStyle(counter_style),
+ Either::Second(string) => T::String(string),
+ }
+ }
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="Table">
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="Effects">
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="InheritedBox">
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="InheritedTable"
+ skip_longhands="border-spacing">
+
+ pub fn set_border_spacing(&mut self, v: longhands::border_spacing::computed_value::T) {
+ self.mBorderSpacingCol = v.horizontal().0;
+ self.mBorderSpacingRow = v.vertical().0;
+ }
+
+ pub fn copy_border_spacing_from(&mut self, other: &Self) {
+ self.mBorderSpacingCol = other.mBorderSpacingCol;
+ self.mBorderSpacingRow = other.mBorderSpacingRow;
+ }
+
+ pub fn reset_border_spacing(&mut self, other: &Self) {
+ self.copy_border_spacing_from(other)
+ }
+
+ pub fn clone_border_spacing(&self) -> longhands::border_spacing::computed_value::T {
+ longhands::border_spacing::computed_value::T::new(
+ Au(self.mBorderSpacingCol).into(),
+ Au(self.mBorderSpacingRow).into()
+ )
+ }
+</%self:impl_trait>
+
+
+<%self:impl_trait style_struct_name="InheritedText">
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="Text" skip_longhands="initial-letter">
+ pub fn set_initial_letter(&mut self, v: longhands::initial_letter::computed_value::T) {
+ use crate::values::generics::text::InitialLetter;
+ match v {
+ InitialLetter::Normal => {
+ self.mInitialLetterSize = 0.;
+ self.mInitialLetterSink = 0;
+ },
+ InitialLetter::Specified(size, sink) => {
+ self.mInitialLetterSize = size;
+ if let Some(sink) = sink {
+ self.mInitialLetterSink = sink;
+ } else {
+ self.mInitialLetterSink = size.floor() as i32;
+ }
+ }
+ }
+ }
+
+ pub fn copy_initial_letter_from(&mut self, other: &Self) {
+ self.mInitialLetterSize = other.mInitialLetterSize;
+ self.mInitialLetterSink = other.mInitialLetterSink;
+ }
+
+ pub fn reset_initial_letter(&mut self, other: &Self) {
+ self.copy_initial_letter_from(other)
+ }
+
+ pub fn clone_initial_letter(&self) -> longhands::initial_letter::computed_value::T {
+ use crate::values::generics::text::InitialLetter;
+
+ if self.mInitialLetterSize == 0. && self.mInitialLetterSink == 0 {
+ InitialLetter::Normal
+ } else if self.mInitialLetterSize.floor() as i32 == self.mInitialLetterSink {
+ InitialLetter::Specified(self.mInitialLetterSize, None)
+ } else {
+ InitialLetter::Specified(self.mInitialLetterSize, Some(self.mInitialLetterSink))
+ }
+ }
+</%self:impl_trait>
+
+<% skip_svg_longhands = """
+mask-mode mask-repeat mask-clip mask-origin mask-composite mask-position-x mask-position-y mask-size mask-image
+"""
+%>
+<%self:impl_trait style_struct_name="SVG"
+ skip_longhands="${skip_svg_longhands}">
+ <% impl_common_image_layer_properties("mask") %>
+ <% impl_simple_image_array_property("mode", "mask", "mMask", "mMaskMode", "SVG") %>
+ <% impl_simple_image_array_property("composite", "mask", "mMask", "mComposite", "SVG") %>
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="InheritedSVG">
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="InheritedUI">
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="Column"
+ skip_longhands="column-count column-rule-width column-rule-style">
+
+ #[allow(unused_unsafe)]
+ pub fn set_column_count(&mut self, v: longhands::column_count::computed_value::T) {
+ use crate::gecko_bindings::structs::{nsStyleColumn_kColumnCountAuto, nsStyleColumn_kMaxColumnCount};
+
+ self.mColumnCount = match v {
+ ColumnCount::Integer(integer) => {
+ cmp::min(integer.0 as u32, unsafe { nsStyleColumn_kMaxColumnCount })
+ },
+ ColumnCount::Auto => nsStyleColumn_kColumnCountAuto
+ };
+ }
+
+ ${impl_simple_copy('column_count', 'mColumnCount')}
+
+ pub fn clone_column_count(&self) -> longhands::column_count::computed_value::T {
+ use crate::gecko_bindings::structs::{nsStyleColumn_kColumnCountAuto, nsStyleColumn_kMaxColumnCount};
+ if self.mColumnCount != nsStyleColumn_kColumnCountAuto {
+ debug_assert!(self.mColumnCount >= 1 &&
+ self.mColumnCount <= nsStyleColumn_kMaxColumnCount);
+ ColumnCount::Integer((self.mColumnCount as i32).into())
+ } else {
+ ColumnCount::Auto
+ }
+ }
+
+ pub fn set_column_rule_style(&mut self, v: longhands::column_rule_style::computed_value::T) {
+ self.mColumnRuleStyle = v;
+ // NB: This is needed to correctly handling the initial value of
+ // column-rule-width when colun-rule-style changes, see the
+ // update_border_${side.ident} comment for more details.
+ self.mActualColumnRuleWidth = self.mColumnRuleWidth;
+ }
+
+ pub fn copy_column_rule_style_from(&mut self, other: &Self) {
+ self.set_column_rule_style(other.mColumnRuleStyle);
+ }
+
+ pub fn reset_column_rule_style(&mut self, other: &Self) {
+ self.copy_column_rule_style_from(other)
+ }
+
+ pub fn clone_column_rule_style(&self) -> longhands::column_rule_style::computed_value::T {
+ self.mColumnRuleStyle.clone()
+ }
+
+ ${impl_border_width("column_rule_width", "mActualColumnRuleWidth", "mColumnRuleWidth")}
+
+ pub fn column_rule_has_nonzero_width(&self) -> bool {
+ self.mActualColumnRuleWidth != 0
+ }
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="Counters">
+ pub fn ineffective_content_property(&self) -> bool {
+ !self.mContent.is_items()
+ }
+</%self:impl_trait>
+
+<% skip_ui_longhands = """animation-name animation-delay animation-duration
+ animation-direction animation-fill-mode
+ animation-play-state animation-iteration-count
+ animation-timing-function animation-composition animation-timeline
+ transition-duration transition-delay
+ transition-timing-function transition-property
+ scroll-timeline-name scroll-timeline-axis
+ view-timeline-name view-timeline-axis view-timeline-inset""" %>
+
+<%self:impl_trait style_struct_name="UI" skip_longhands="${skip_ui_longhands}">
+ ${impl_coordinated_property('transition', 'delay', 'Delay')}
+ ${impl_coordinated_property('transition', 'duration', 'Duration')}
+ ${impl_coordinated_property('transition', 'timing_function', 'TimingFunction')}
+ ${impl_coordinated_property('transition', 'property', 'Property')}
+
+ pub fn transition_combined_duration_at(&self, index: usize) -> Time {
+ // https://drafts.csswg.org/css-transitions/#transition-combined-duration
+ Time::from_seconds(
+ self.transition_duration_at(index).seconds().max(0.0) +
+ self.transition_delay_at(index).seconds()
+ )
+ }
+
+ /// Returns whether there are any transitions specified.
+ pub fn specifies_transitions(&self) -> bool {
+ if self.mTransitionPropertyCount == 1 &&
+ self.transition_combined_duration_at(0).seconds() <= 0.0f32 {
+ return false;
+ }
+ self.mTransitionPropertyCount > 0
+ }
+
+ pub fn animations_equals(&self, other: &Self) -> bool {
+ return self.mAnimationNameCount == other.mAnimationNameCount
+ && self.mAnimationDelayCount == other.mAnimationDelayCount
+ && self.mAnimationDirectionCount == other.mAnimationDirectionCount
+ && self.mAnimationDurationCount == other.mAnimationDurationCount
+ && self.mAnimationFillModeCount == other.mAnimationFillModeCount
+ && self.mAnimationIterationCountCount == other.mAnimationIterationCountCount
+ && self.mAnimationPlayStateCount == other.mAnimationPlayStateCount
+ && self.mAnimationTimingFunctionCount == other.mAnimationTimingFunctionCount
+ && self.mAnimationCompositionCount == other.mAnimationCompositionCount
+ && self.mAnimationTimelineCount == other.mAnimationTimelineCount
+ && unsafe { bindings::Gecko_StyleAnimationsEquals(&self.mAnimations, &other.mAnimations) }
+ }
+
+ ${impl_coordinated_property('animation', 'name', 'Name')}
+ ${impl_coordinated_property('animation', 'delay', 'Delay')}
+ ${impl_coordinated_property('animation', 'duration', 'Duration')}
+ ${impl_coordinated_property('animation', 'direction', 'Direction')}
+ ${impl_coordinated_property('animation', 'fill_mode', 'FillMode')}
+ ${impl_coordinated_property('animation', 'play_state', 'PlayState')}
+ ${impl_coordinated_property('animation', 'composition', 'Composition')}
+ ${impl_coordinated_property('animation', 'iteration_count', 'IterationCount')}
+ ${impl_coordinated_property('animation', 'timeline', 'Timeline')}
+ ${impl_coordinated_property('animation', 'timing_function', 'TimingFunction')}
+
+ ${impl_coordinated_property('scroll_timeline', 'name', 'Name')}
+ ${impl_coordinated_property('scroll_timeline', 'axis', 'Axis')}
+
+ pub fn scroll_timelines_equals(&self, other: &Self) -> bool {
+ self.mScrollTimelineNameCount == other.mScrollTimelineNameCount
+ && self.mScrollTimelineAxisCount == other.mScrollTimelineAxisCount
+ && unsafe {
+ bindings::Gecko_StyleScrollTimelinesEquals(
+ &self.mScrollTimelines,
+ &other.mScrollTimelines,
+ )
+ }
+ }
+
+ ${impl_coordinated_property('view_timeline', 'name', 'Name')}
+ ${impl_coordinated_property('view_timeline', 'axis', 'Axis')}
+ ${impl_coordinated_property('view_timeline', 'inset', 'Inset')}
+
+ pub fn view_timelines_equals(&self, other: &Self) -> bool {
+ self.mViewTimelineNameCount == other.mViewTimelineNameCount
+ && self.mViewTimelineAxisCount == other.mViewTimelineAxisCount
+ && self.mViewTimelineInsetCount == other.mViewTimelineInsetCount
+ && unsafe {
+ bindings::Gecko_StyleViewTimelinesEquals(
+ &self.mViewTimelines,
+ &other.mViewTimelines,
+ )
+ }
+ }
+</%self:impl_trait>
+
+<%self:impl_trait style_struct_name="XUL">
+</%self:impl_trait>
+
+% for style_struct in data.style_structs:
+${impl_style_struct(style_struct)}
+% endfor
+
+/// Assert that the initial values set in Gecko style struct constructors
+/// match the values returned by `get_initial_value()` for each longhand.
+#[cfg(feature = "gecko")]
+#[inline]
+pub fn assert_initial_values_match(data: &PerDocumentStyleData) {
+ if cfg!(debug_assertions) {
+ let data = data.borrow();
+ let cv = data.stylist.device().default_computed_values();
+ <%
+ # Skip properties with initial values that change at computed
+ # value time, or whose initial value depends on the document
+ # / other prefs.
+ SKIPPED = [
+ "border-top-width",
+ "border-bottom-width",
+ "border-left-width",
+ "border-right-width",
+ "column-rule-width",
+ "font-family",
+ "font-size",
+ "outline-width",
+ "color",
+ ]
+ TO_TEST = [p for p in data.longhands if p.enabled_in != "" and not p.logical and not p.name in SKIPPED]
+ %>
+ % for property in TO_TEST:
+ assert_eq!(
+ cv.clone_${property.ident}(),
+ longhands::${property.ident}::get_initial_value(),
+ concat!(
+ "initial value in Gecko style struct for ",
+ stringify!(${property.ident}),
+ " must match longhands::",
+ stringify!(${property.ident}),
+ "::get_initial_value()"
+ )
+ );
+ % endfor
+ }
+}
diff --git a/servo/components/style/properties/helpers.mako.rs b/servo/components/style/properties/helpers.mako.rs
new file mode 100644
index 0000000000..968a97aa00
--- /dev/null
+++ b/servo/components/style/properties/helpers.mako.rs
@@ -0,0 +1,909 @@
+/* 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/. */
+
+<%!
+ from data import Keyword, to_rust_ident, to_phys, to_camel_case, SYSTEM_FONT_LONGHANDS
+ from data import (LOGICAL_CORNERS, PHYSICAL_CORNERS, LOGICAL_SIDES,
+ PHYSICAL_SIDES, LOGICAL_SIZES, LOGICAL_AXES)
+%>
+
+<%def name="predefined_type(name, type, initial_value, parse_method='parse',
+ vector=False, none_value=None, initial_specified_value=None,
+ allow_quirks='No', **kwargs)">
+ <%def name="predefined_type_inner(name, type, initial_value, parse_method)">
+ #[allow(unused_imports)]
+ use app_units::Au;
+ #[allow(unused_imports)]
+ use crate::values::specified::AllowQuirks;
+ #[allow(unused_imports)]
+ use crate::Zero;
+ #[allow(unused_imports)]
+ use smallvec::SmallVec;
+ pub use crate::values::specified::${type} as SpecifiedValue;
+ pub mod computed_value {
+ pub use crate::values::computed::${type} as T;
+ }
+ % if initial_value:
+ #[inline] pub fn get_initial_value() -> computed_value::T { ${initial_value} }
+ % endif
+ % if initial_specified_value:
+ #[inline] pub fn get_initial_specified_value() -> SpecifiedValue { ${initial_specified_value} }
+ % endif
+ #[allow(unused_variables)]
+ #[inline]
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<SpecifiedValue, ParseError<'i>> {
+ % if allow_quirks != "No":
+ specified::${type}::${parse_method}_quirky(context, input, AllowQuirks::${allow_quirks})
+ % elif parse_method != "parse":
+ specified::${type}::${parse_method}(context, input)
+ % else:
+ <specified::${type} as crate::parser::Parse>::parse(context, input)
+ % endif
+ }
+ </%def>
+ % if vector:
+ <%call
+ expr="vector_longhand(name, predefined_type=type, allow_empty=not initial_value, none_value=none_value, **kwargs)"
+ >
+ ${predefined_type_inner(name, type, initial_value, parse_method)}
+ % if caller:
+ ${caller.body()}
+ % endif
+ </%call>
+ % else:
+ <%call expr="longhand(name, predefined_type=type, **kwargs)">
+ ${predefined_type_inner(name, type, initial_value, parse_method)}
+ % if caller:
+ ${caller.body()}
+ % endif
+ </%call>
+ % endif
+</%def>
+
+// The setup here is roughly:
+//
+// * UnderlyingList is the list that is stored in the computed value. This may
+// be a shared ArcSlice if the property is inherited.
+// * UnderlyingOwnedList is the list that is used for animation.
+// * Specified values always use OwnedSlice, since it's more compact.
+// * computed_value::List is just a convenient alias that you can use for the
+// computed value list, since this is in the computed_value module.
+//
+// If simple_vector_bindings is true, then we don't use the complex iterator
+// machinery and set_foo_from, and just compute the value like any other
+// longhand.
+<%def name="vector_longhand(name, animation_value_type=None,
+ vector_animation_type=None, allow_empty=False,
+ none_value=None,
+ simple_vector_bindings=False,
+ separator='Comma',
+ **kwargs)">
+ <%call expr="longhand(name, animation_value_type=animation_value_type, vector=True,
+ simple_vector_bindings=simple_vector_bindings, **kwargs)">
+ #[allow(unused_imports)]
+ use smallvec::SmallVec;
+
+ pub mod single_value {
+ #[allow(unused_imports)]
+ use cssparser::{Parser, BasicParseError};
+ #[allow(unused_imports)]
+ use crate::parser::{Parse, ParserContext};
+ #[allow(unused_imports)]
+ use crate::properties::ShorthandId;
+ #[allow(unused_imports)]
+ use selectors::parser::SelectorParseErrorKind;
+ #[allow(unused_imports)]
+ use style_traits::{ParseError, StyleParseErrorKind};
+ #[allow(unused_imports)]
+ use crate::values::computed::{Context, ToComputedValue};
+ #[allow(unused_imports)]
+ use crate::values::{computed, specified};
+ ${caller.body()}
+ }
+
+ /// The definition of the computed value for ${name}.
+ pub mod computed_value {
+ #[allow(unused_imports)]
+ use crate::values::animated::ToAnimatedValue;
+ #[allow(unused_imports)]
+ use crate::values::resolved::ToResolvedValue;
+ pub use super::single_value::computed_value as single_value;
+ pub use self::single_value::T as SingleComputedValue;
+ % if not allow_empty:
+ use smallvec::SmallVec;
+ % endif
+ use crate::values::computed::ComputedVecIter;
+
+ <%
+ is_shared_list = allow_empty and \
+ data.longhands_by_name[name].style_struct.inherited
+ %>
+
+ // FIXME(emilio): Add an OwnedNonEmptySlice type, and figure out
+ // something for transition-name, which is the only remaining user
+ // of NotInitial.
+ pub type UnderlyingList<T> =
+ % if allow_empty:
+ % if data.longhands_by_name[name].style_struct.inherited:
+ crate::ArcSlice<T>;
+ % else:
+ crate::OwnedSlice<T>;
+ % endif
+ % else:
+ SmallVec<[T; 1]>;
+ % endif
+
+ pub type UnderlyingOwnedList<T> =
+ % if allow_empty:
+ crate::OwnedSlice<T>;
+ % else:
+ SmallVec<[T; 1]>;
+ % endif
+
+
+ /// The generic type defining the animated and resolved values for
+ /// this property.
+ ///
+ /// Making this type generic allows the compiler to figure out the
+ /// animated value for us, instead of having to implement it
+ /// manually for every type we care about.
+ #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToResolvedValue, ToCss)]
+ % if separator == "Comma":
+ #[css(comma)]
+ % endif
+ pub struct OwnedList<T>(
+ % if not allow_empty:
+ #[css(iterable)]
+ % else:
+ #[css(if_empty = "none", iterable)]
+ % endif
+ pub UnderlyingOwnedList<T>,
+ );
+
+ /// The computed value for this property.
+ % if not is_shared_list:
+ pub type ComputedList = OwnedList<single_value::T>;
+ pub use self::OwnedList as List;
+ % else:
+ pub use self::ComputedList as List;
+
+ #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss)]
+ % if separator == "Comma":
+ #[css(comma)]
+ % endif
+ pub struct ComputedList(
+ % if not allow_empty:
+ #[css(iterable)]
+ % else:
+ #[css(if_empty = "none", iterable)]
+ % endif
+ % if is_shared_list:
+ #[ignore_malloc_size_of = "Arc"]
+ % endif
+ pub UnderlyingList<single_value::T>,
+ );
+
+ type ResolvedList = OwnedList<<single_value::T as ToResolvedValue>::ResolvedValue>;
+ impl ToResolvedValue for ComputedList {
+ type ResolvedValue = ResolvedList;
+
+ fn to_resolved_value(self, context: &crate::values::resolved::Context) -> Self::ResolvedValue {
+ OwnedList(
+ self.0
+ .iter()
+ .cloned()
+ .map(|v| v.to_resolved_value(context))
+ .collect()
+ )
+ }
+
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ % if not is_shared_list:
+ use std::iter::FromIterator;
+ % endif
+ let iter =
+ resolved.0.into_iter().map(ToResolvedValue::from_resolved_value);
+ ComputedList(UnderlyingList::from_iter(iter))
+ }
+ }
+ % endif
+
+ % if simple_vector_bindings:
+ impl From<ComputedList> for UnderlyingList<single_value::T> {
+ #[inline]
+ fn from(l: ComputedList) -> Self {
+ l.0
+ }
+ }
+ impl From<UnderlyingList<single_value::T>> for ComputedList {
+ #[inline]
+ fn from(l: UnderlyingList<single_value::T>) -> Self {
+ List(l)
+ }
+ }
+ % endif
+
+ % if vector_animation_type:
+ % if not animation_value_type:
+ Sorry, this is stupid but needed for now.
+ % endif
+
+ use crate::values::animated::{Animate, ToAnimatedZero, Procedure, lists};
+ use crate::values::distance::{SquaredDistance, ComputeSquaredDistance};
+
+ // FIXME(emilio): For some reason rust thinks that this alias is
+ // unused, even though it's clearly used below?
+ #[allow(unused)]
+ type AnimatedList = OwnedList<<single_value::T as ToAnimatedValue>::AnimatedValue>;
+
+ % if is_shared_list:
+ impl ToAnimatedValue for ComputedList {
+ type AnimatedValue = AnimatedList;
+
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ OwnedList(
+ self.0.iter().map(|v| v.clone().to_animated_value()).collect()
+ )
+ }
+
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ let iter =
+ animated.0.into_iter().map(ToAnimatedValue::from_animated_value);
+ ComputedList(UnderlyingList::from_iter(iter))
+ }
+ }
+ % endif
+
+ impl ToAnimatedZero for AnimatedList {
+ fn to_animated_zero(&self) -> Result<Self, ()> { Err(()) }
+ }
+
+ impl Animate for AnimatedList {
+ fn animate(
+ &self,
+ other: &Self,
+ procedure: Procedure,
+ ) -> Result<Self, ()> {
+ Ok(OwnedList(
+ lists::${vector_animation_type}::animate(&self.0, &other.0, procedure)?
+ ))
+ }
+ }
+ impl ComputeSquaredDistance for AnimatedList {
+ fn compute_squared_distance(
+ &self,
+ other: &Self,
+ ) -> Result<SquaredDistance, ()> {
+ lists::${vector_animation_type}::squared_distance(&self.0, &other.0)
+ }
+ }
+ % endif
+
+ /// The computed value, effectively a list of single values.
+ pub use self::ComputedList as T;
+
+ pub type Iter<'a, 'cx, 'cx_a> = ComputedVecIter<'a, 'cx, 'cx_a, super::single_value::SpecifiedValue>;
+ }
+
+ /// The specified value of ${name}.
+ #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+ % if none_value:
+ #[value_info(other_values = "none")]
+ % endif
+ % if separator == "Comma":
+ #[css(comma)]
+ % endif
+ pub struct SpecifiedValue(
+ % if not allow_empty:
+ #[css(iterable)]
+ % else:
+ #[css(if_empty = "none", iterable)]
+ % endif
+ pub crate::OwnedSlice<single_value::SpecifiedValue>,
+ );
+
+ pub fn get_initial_value() -> computed_value::T {
+ % if allow_empty:
+ computed_value::List(Default::default())
+ % else:
+ let mut v = SmallVec::new();
+ v.push(single_value::get_initial_value());
+ computed_value::List(v)
+ % endif
+ }
+
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<SpecifiedValue, ParseError<'i>> {
+ use style_traits::Separator;
+
+ % if allow_empty or none_value:
+ if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+ % if allow_empty:
+ return Ok(SpecifiedValue(Default::default()))
+ % else:
+ return Ok(SpecifiedValue(crate::OwnedSlice::from(vec![${none_value}])))
+ % endif
+ }
+ % endif
+
+ let v = style_traits::${separator}::parse(input, |parser| {
+ single_value::parse(context, parser)
+ })?;
+ Ok(SpecifiedValue(v.into()))
+ }
+
+ pub use self::single_value::SpecifiedValue as SingleSpecifiedValue;
+
+ % if not simple_vector_bindings and engine == "gecko":
+ impl SpecifiedValue {
+ fn compute_iter<'a, 'cx, 'cx_a>(
+ &'a self,
+ context: &'cx Context<'cx_a>,
+ ) -> computed_value::Iter<'a, 'cx, 'cx_a> {
+ computed_value::Iter::new(context, &self.0)
+ }
+ }
+ % endif
+
+ impl ToComputedValue for SpecifiedValue {
+ type ComputedValue = computed_value::T;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> computed_value::T {
+ % if not is_shared_list:
+ use std::iter::FromIterator;
+ % endif
+ computed_value::List(computed_value::UnderlyingList::from_iter(
+ self.0.iter().map(|i| i.to_computed_value(context))
+ ))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &computed_value::T) -> Self {
+ let iter = computed.0.iter().map(ToComputedValue::from_computed_value);
+ SpecifiedValue(iter.collect())
+ }
+ }
+ </%call>
+</%def>
+<%def name="longhand(*args, **kwargs)">
+ <%
+ property = data.declare_longhand(*args, **kwargs)
+ if property is None:
+ return ""
+ %>
+ /// ${property.spec}
+ pub mod ${property.ident} {
+ #[allow(unused_imports)]
+ use cssparser::{Parser, BasicParseError, Token};
+ #[allow(unused_imports)]
+ use crate::parser::{Parse, ParserContext};
+ #[allow(unused_imports)]
+ use crate::properties::{UnparsedValue, ShorthandId};
+ #[allow(unused_imports)]
+ use crate::error_reporting::ParseErrorReporter;
+ #[allow(unused_imports)]
+ use crate::properties::longhands;
+ #[allow(unused_imports)]
+ use crate::properties::{LonghandId, LonghandIdSet};
+ #[allow(unused_imports)]
+ use crate::properties::{CSSWideKeyword, ComputedValues, PropertyDeclaration};
+ #[allow(unused_imports)]
+ use crate::properties::style_structs;
+ #[allow(unused_imports)]
+ use selectors::parser::SelectorParseErrorKind;
+ #[allow(unused_imports)]
+ use servo_arc::Arc;
+ #[allow(unused_imports)]
+ use style_traits::{ParseError, StyleParseErrorKind};
+ #[allow(unused_imports)]
+ use crate::values::computed::{Context, ToComputedValue};
+ #[allow(unused_imports)]
+ use crate::values::{computed, generics, specified};
+ #[allow(unused_imports)]
+ use crate::Atom;
+ ${caller.body()}
+ #[allow(unused_variables)]
+ pub unsafe fn cascade_property(
+ declaration: &PropertyDeclaration,
+ context: &mut computed::Context,
+ ) {
+ % if property.logical:
+ declaration.debug_crash("Should physicalize before entering here");
+ % else:
+ context.for_non_inherited_property = ${"false" if property.style_struct.inherited else "true"};
+ % if property.logical_group:
+ debug_assert_eq!(
+ declaration.id().as_longhand().unwrap().logical_group(),
+ LonghandId::${property.camel_case}.logical_group(),
+ );
+ % else:
+ debug_assert_eq!(
+ declaration.id().as_longhand().unwrap(),
+ LonghandId::${property.camel_case},
+ );
+ % endif
+ let specified_value = match *declaration {
+ PropertyDeclaration::CSSWideKeyword(ref wk) => {
+ match wk.keyword {
+ % if not property.style_struct.inherited:
+ CSSWideKeyword::Unset |
+ % endif
+ CSSWideKeyword::Initial => {
+ % if not property.style_struct.inherited:
+ declaration.debug_crash("Unexpected initial or unset for non-inherited property");
+ % else:
+ context.builder.reset_${property.ident}();
+ % endif
+ },
+ % if property.style_struct.inherited:
+ CSSWideKeyword::Unset |
+ % endif
+ CSSWideKeyword::Inherit => {
+ % if property.style_struct.inherited:
+ declaration.debug_crash("Unexpected inherit or unset for inherited property");
+ % else:
+ context.rule_cache_conditions.borrow_mut().set_uncacheable();
+ context.builder.inherit_${property.ident}();
+ % endif
+ }
+ CSSWideKeyword::RevertLayer |
+ CSSWideKeyword::Revert => {
+ declaration.debug_crash("Found revert/revert-layer not deal with");
+ },
+ }
+ return;
+ },
+ #[cfg(debug_assertions)]
+ PropertyDeclaration::WithVariables(..) => {
+ declaration.debug_crash("Found variables not substituted");
+ return;
+ },
+ _ => unsafe {
+ declaration.unchecked_value_as::<${property.specified_type()}>()
+ },
+ };
+
+ % if property.ident in SYSTEM_FONT_LONGHANDS and engine == "gecko":
+ if let Some(sf) = specified_value.get_system() {
+ longhands::system_font::resolve_system_font(sf, context);
+ }
+ % endif
+
+ % if property.is_vector and not property.simple_vector_bindings and engine == "gecko":
+ // In the case of a vector property we want to pass down an
+ // iterator so that this can be computed without allocation.
+ //
+ // However, computing requires a context, but the style struct
+ // being mutated is on the context. We temporarily remove it,
+ // mutate it, and then put it back. Vector longhands cannot
+ // touch their own style struct whilst computing, else this will
+ // panic.
+ let mut s =
+ context.builder.take_${data.current_style_struct.name_lower}();
+ {
+ let iter = specified_value.compute_iter(context);
+ s.set_${property.ident}(iter);
+ }
+ context.builder.put_${data.current_style_struct.name_lower}(s);
+ % else:
+ % if property.boxed:
+ let computed = (**specified_value).to_computed_value(context);
+ % else:
+ let computed = specified_value.to_computed_value(context);
+ % endif
+ context.builder.set_${property.ident}(computed)
+ % endif
+ % endif
+ }
+
+ pub fn parse_declared<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<PropertyDeclaration, ParseError<'i>> {
+ % if property.allow_quirks != "No":
+ parse_quirky(context, input, specified::AllowQuirks::${property.allow_quirks})
+ % else:
+ parse(context, input)
+ % endif
+ % if property.boxed:
+ .map(Box::new)
+ % endif
+ .map(PropertyDeclaration::${property.camel_case})
+ }
+ }
+</%def>
+
+<%def name="gecko_keyword_conversion(keyword, values=None, type='SpecifiedValue', cast_to=None)">
+ <%
+ if not values:
+ values = keyword.values_for(engine)
+ maybe_cast = "as %s" % cast_to if cast_to else ""
+ const_type = cast_to if cast_to else "u32"
+ %>
+ #[cfg(feature = "gecko")]
+ impl ${type} {
+ /// Obtain a specified value from a Gecko keyword value
+ ///
+ /// Intended for use with presentation attributes, not style structs
+ pub fn from_gecko_keyword(kw: u32) -> Self {
+ use crate::gecko_bindings::structs;
+ % for value in values:
+ // We can't match on enum values if we're matching on a u32
+ const ${to_rust_ident(value).upper()}: ${const_type}
+ = structs::${keyword.gecko_constant(value)} as ${const_type};
+ % endfor
+ match kw ${maybe_cast} {
+ % for value in values:
+ ${to_rust_ident(value).upper()} => ${type}::${to_camel_case(value)},
+ % endfor
+ _ => panic!("Found unexpected value in style struct for ${keyword.name} property"),
+ }
+ }
+ }
+</%def>
+
+<%def name="gecko_bitflags_conversion(bit_map, gecko_bit_prefix, type, kw_type='u8')">
+ #[cfg(feature = "gecko")]
+ impl ${type} {
+ /// Obtain a specified value from a Gecko keyword value
+ ///
+ /// Intended for use with presentation attributes, not style structs
+ pub fn from_gecko_keyword(kw: ${kw_type}) -> Self {
+ % for gecko_bit in bit_map.values():
+ use crate::gecko_bindings::structs::${gecko_bit_prefix}${gecko_bit};
+ % endfor
+
+ let mut bits = ${type}::empty();
+ % for servo_bit, gecko_bit in bit_map.items():
+ if kw & (${gecko_bit_prefix}${gecko_bit} as ${kw_type}) != 0 {
+ bits |= ${servo_bit};
+ }
+ % endfor
+ bits
+ }
+
+ pub fn to_gecko_keyword(self) -> ${kw_type} {
+ % for gecko_bit in bit_map.values():
+ use crate::gecko_bindings::structs::${gecko_bit_prefix}${gecko_bit};
+ % endfor
+
+ let mut bits: ${kw_type} = 0;
+ // FIXME: if we ensure that the Servo bitflags storage is the same
+ // as Gecko's one, we can just copy it.
+ % for servo_bit, gecko_bit in bit_map.items():
+ if self.contains(${servo_bit}) {
+ bits |= ${gecko_bit_prefix}${gecko_bit} as ${kw_type};
+ }
+ % endfor
+ bits
+ }
+ }
+</%def>
+
+<%def name="single_keyword(name, values, vector=False,
+ needs_conversion=False, **kwargs)">
+ <%
+ keyword_kwargs = {a: kwargs.pop(a, None) for a in [
+ 'gecko_constant_prefix',
+ 'gecko_enum_prefix',
+ 'extra_gecko_values',
+ 'extra_servo_2013_values',
+ 'extra_servo_2020_values',
+ 'gecko_aliases',
+ 'servo_2013_aliases',
+ 'servo_2020_aliases',
+ 'custom_consts',
+ 'gecko_inexhaustive',
+ 'gecko_strip_moz_prefix',
+ ]}
+ %>
+
+ <%def name="inner_body(keyword, needs_conversion=False)">
+ pub use self::computed_value::T as SpecifiedValue;
+ pub mod computed_value {
+ #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+ #[derive(Clone, Copy, Debug, Eq, FromPrimitive, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+ pub enum T {
+ % for variant in keyword.values_for(engine):
+ <%
+ aliases = []
+ for alias, v in keyword.aliases_for(engine).items():
+ if variant == v:
+ aliases.append(alias)
+ %>
+ % if aliases:
+ #[parse(aliases = "${','.join(sorted(aliases))}")]
+ % endif
+ ${to_camel_case(variant)},
+ % endfor
+ }
+ }
+ #[inline]
+ pub fn get_initial_value() -> computed_value::T {
+ computed_value::T::${to_camel_case(values.split()[0])}
+ }
+ #[inline]
+ pub fn get_initial_specified_value() -> SpecifiedValue {
+ SpecifiedValue::${to_camel_case(values.split()[0])}
+ }
+ #[inline]
+ pub fn parse<'i, 't>(_context: &ParserContext, input: &mut Parser<'i, 't>)
+ -> Result<SpecifiedValue, ParseError<'i>> {
+ SpecifiedValue::parse(input)
+ }
+
+ % if needs_conversion:
+ <%
+ conversion_values = keyword.values_for(engine) + list(keyword.aliases_for(engine).keys())
+ %>
+ ${gecko_keyword_conversion(keyword, values=conversion_values)}
+ % endif
+ </%def>
+ % if vector:
+ <%call expr="vector_longhand(name, keyword=Keyword(name, values, **keyword_kwargs), **kwargs)">
+ ${inner_body(Keyword(name, values, **keyword_kwargs))}
+ % if caller:
+ ${caller.body()}
+ % endif
+ </%call>
+ % else:
+ <%call expr="longhand(name, keyword=Keyword(name, values, **keyword_kwargs), **kwargs)">
+ ${inner_body(Keyword(name, values, **keyword_kwargs),
+ needs_conversion=needs_conversion)}
+ % if caller:
+ ${caller.body()}
+ % endif
+ </%call>
+ % endif
+</%def>
+
+<%def name="shorthand(name, sub_properties, derive_serialize=False,
+ derive_value_info=True, **kwargs)">
+<%
+ shorthand = data.declare_shorthand(name, sub_properties.split(), **kwargs)
+ # mako doesn't accept non-string value in parameters with <% %> form, so
+ # we have to workaround it this way.
+ if not isinstance(derive_value_info, bool):
+ derive_value_info = eval(derive_value_info)
+%>
+ % if shorthand:
+ /// ${shorthand.spec}
+ pub mod ${shorthand.ident} {
+ use cssparser::Parser;
+ use crate::parser::ParserContext;
+ use crate::properties::{PropertyDeclaration, SourcePropertyDeclaration, MaybeBoxed, longhands};
+ #[allow(unused_imports)]
+ use selectors::parser::SelectorParseErrorKind;
+ #[allow(unused_imports)]
+ use std::fmt::{self, Write};
+ #[allow(unused_imports)]
+ use style_traits::{ParseError, StyleParseErrorKind};
+ #[allow(unused_imports)]
+ use style_traits::{CssWriter, KeywordsCollectFn, SpecifiedValueInfo, ToCss};
+
+ % if derive_value_info:
+ #[derive(SpecifiedValueInfo)]
+ % endif
+ pub struct Longhands {
+ % for sub_property in shorthand.sub_properties:
+ pub ${sub_property.ident}:
+ % if sub_property.boxed:
+ Box<
+ % endif
+ longhands::${sub_property.ident}::SpecifiedValue
+ % if sub_property.boxed:
+ >
+ % endif
+ ,
+ % endfor
+ }
+
+ /// Represents a serializable set of all of the longhand properties that
+ /// correspond to a shorthand.
+ % if derive_serialize:
+ #[derive(ToCss)]
+ % endif
+ pub struct LonghandsToSerialize<'a> {
+ % for sub_property in shorthand.sub_properties:
+ pub ${sub_property.ident}:
+ % if sub_property.may_be_disabled_in(shorthand, engine):
+ Option<
+ % endif
+ &'a longhands::${sub_property.ident}::SpecifiedValue,
+ % if sub_property.may_be_disabled_in(shorthand, engine):
+ >,
+ % endif
+ % endfor
+ }
+
+ impl<'a> LonghandsToSerialize<'a> {
+ /// Tries to get a serializable set of longhands given a set of
+ /// property declarations.
+ pub fn from_iter(iter: impl Iterator<Item = &'a PropertyDeclaration>) -> Result<Self, ()> {
+ // Define all of the expected variables that correspond to the shorthand
+ % for sub_property in shorthand.sub_properties:
+ let mut ${sub_property.ident} =
+ None::< &'a longhands::${sub_property.ident}::SpecifiedValue>;
+ % endfor
+
+ // Attempt to assign the incoming declarations to the expected variables
+ for declaration in iter {
+ match *declaration {
+ % for sub_property in shorthand.sub_properties:
+ PropertyDeclaration::${sub_property.camel_case}(ref value) => {
+ ${sub_property.ident} = Some(value)
+ },
+ % endfor
+ _ => {}
+ };
+ }
+
+ // If any of the expected variables are missing, return an error
+ match (
+ % for sub_property in shorthand.sub_properties:
+ ${sub_property.ident},
+ % endfor
+ ) {
+
+ (
+ % for sub_property in shorthand.sub_properties:
+ % if sub_property.may_be_disabled_in(shorthand, engine):
+ ${sub_property.ident},
+ % else:
+ Some(${sub_property.ident}),
+ % endif
+ % endfor
+ ) =>
+ Ok(LonghandsToSerialize {
+ % for sub_property in shorthand.sub_properties:
+ ${sub_property.ident},
+ % endfor
+ }),
+ _ => Err(())
+ }
+ }
+ }
+
+ /// Parse the given shorthand and fill the result into the
+ /// `declarations` vector.
+ pub fn parse_into<'i, 't>(
+ declarations: &mut SourcePropertyDeclaration,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ #[allow(unused_imports)]
+ use crate::properties::{NonCustomPropertyId, LonghandId};
+ input.parse_entirely(|input| parse_value(context, input)).map(|longhands| {
+ % for sub_property in shorthand.sub_properties:
+ % if sub_property.may_be_disabled_in(shorthand, engine):
+ if NonCustomPropertyId::from(LonghandId::${sub_property.camel_case})
+ .allowed_in_ignoring_rule_type(context) {
+ % endif
+ declarations.push(PropertyDeclaration::${sub_property.camel_case}(
+ longhands.${sub_property.ident}
+ ));
+ % if sub_property.may_be_disabled_in(shorthand, engine):
+ }
+ % endif
+ % endfor
+ })
+ }
+
+ /// Try to serialize a given shorthand to a string.
+ pub fn to_css(declarations: &[&PropertyDeclaration], dest: &mut crate::str::CssStringWriter) -> fmt::Result {
+ match LonghandsToSerialize::from_iter(declarations.iter().cloned()) {
+ Ok(longhands) => longhands.to_css(&mut CssWriter::new(dest)),
+ Err(_) => Ok(())
+ }
+ }
+
+ ${caller.body()}
+ }
+ % endif
+</%def>
+
+// A shorthand of kind `<property-1> <property-2>?` where both properties have
+// the same type.
+<%def name="two_properties_shorthand(
+ name,
+ first_property,
+ second_property,
+ parser_function='crate::parser::Parse::parse',
+ **kwargs
+)">
+<%call expr="self.shorthand(name, sub_properties=' '.join([first_property, second_property]), **kwargs)">
+ #[allow(unused_imports)]
+ use crate::parser::Parse;
+ #[allow(unused_imports)]
+ use crate::values::specified;
+
+ fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let parse_one = |c: &ParserContext, input: &mut Parser<'i, 't>| -> Result<
+ crate::properties::longhands::${to_rust_ident(first_property)}::SpecifiedValue,
+ ParseError<'i>
+ > {
+ ${parser_function}(c, input)
+ };
+
+ let first = parse_one(context, input)?;
+ let second =
+ input.try_parse(|input| parse_one(context, input)).unwrap_or_else(|_| first.clone());
+ Ok(expanded! {
+ ${to_rust_ident(first_property)}: first,
+ ${to_rust_ident(second_property)}: second,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let first = &self.${to_rust_ident(first_property)};
+ let second = &self.${to_rust_ident(second_property)};
+
+ first.to_css(dest)?;
+ if first != second {
+ dest.write_char(' ')?;
+ second.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%call>
+</%def>
+
+<%def name="four_sides_shorthand(name, sub_property_pattern,
+ parser_function='crate::parser::Parse::parse',
+ allow_quirks='No', **kwargs)">
+ <% sub_properties=' '.join(sub_property_pattern % side for side in PHYSICAL_SIDES) %>
+ <%call expr="self.shorthand(name, sub_properties=sub_properties, **kwargs)">
+ #[allow(unused_imports)]
+ use crate::parser::Parse;
+ use crate::values::generics::rect::Rect;
+ #[allow(unused_imports)]
+ use crate::values::specified;
+
+ fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let rect = Rect::parse_with(context, input, |c, i| -> Result<
+ crate::properties::longhands::${to_rust_ident(sub_property_pattern % "top")}::SpecifiedValue,
+ ParseError<'i>
+ > {
+ % if allow_quirks != "No":
+ ${parser_function}_quirky(c, i, specified::AllowQuirks::${allow_quirks})
+ % else:
+ ${parser_function}(c, i)
+ % endif
+ })?;
+ Ok(expanded! {
+ % for index, side in enumerate(["top", "right", "bottom", "left"]):
+ ${to_rust_ident(sub_property_pattern % side)}: rect.${index},
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let rect = Rect::new(
+ % for side in ["top", "right", "bottom", "left"]:
+ &self.${to_rust_ident(sub_property_pattern % side)},
+ % endfor
+ );
+ rect.to_css(dest)
+ }
+ }
+ </%call>
+</%def>
diff --git a/servo/components/style/properties/helpers/animated_properties.mako.rs b/servo/components/style/properties/helpers/animated_properties.mako.rs
new file mode 100644
index 0000000000..290684cdab
--- /dev/null
+++ b/servo/components/style/properties/helpers/animated_properties.mako.rs
@@ -0,0 +1,785 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%
+ from data import to_idl_name, SYSTEM_FONT_LONGHANDS, to_camel_case
+ from itertools import groupby
+%>
+
+#[cfg(feature = "gecko")] use crate::gecko_bindings::structs::nsCSSPropertyID;
+use crate::properties::{
+ longhands::{
+ self, content_visibility::computed_value::T as ContentVisibility,
+ visibility::computed_value::T as Visibility,
+ },
+ CSSWideKeyword, NonCustomPropertyId, LonghandId, NonCustomPropertyIterator,
+ PropertyDeclaration, PropertyDeclarationId,
+};
+use std::ptr;
+use std::mem;
+use fxhash::FxHashMap;
+use super::ComputedValues;
+use crate::properties::OwnedPropertyDeclarationId;
+use crate::values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero};
+use crate::values::animated::effects::AnimatedFilter;
+#[cfg(feature = "gecko")] use crate::values::computed::TransitionProperty;
+use crate::values::computed::{ClipRect, Context};
+use crate::values::computed::ToComputedValue;
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::effects::Filter;
+use void::{self, Void};
+use crate::properties_and_values::value::CustomAnimatedValue;
+
+/// Convert nsCSSPropertyID to TransitionProperty
+#[cfg(feature = "gecko")]
+#[allow(non_upper_case_globals)]
+impl From<nsCSSPropertyID> for TransitionProperty {
+ fn from(property: nsCSSPropertyID) -> TransitionProperty {
+ TransitionProperty::NonCustom(NonCustomPropertyId::from_nscsspropertyid(property).unwrap())
+ }
+}
+
+/// A collection of AnimationValue that were composed on an element.
+/// This HashMap stores the values that are the last AnimationValue to be
+/// composed for each TransitionProperty.
+pub type AnimationValueMap = FxHashMap<OwnedPropertyDeclarationId, AnimationValue>;
+
+/// An enum to represent a single computed value belonging to an animated
+/// property in order to be interpolated with another one. When interpolating,
+/// both values need to belong to the same property.
+#[derive(Debug, MallocSizeOf)]
+#[repr(u16)]
+pub enum AnimationValue {
+ % for prop in data.longhands:
+ /// `${prop.name}`
+ % if prop.animatable and not prop.logical:
+ ${prop.camel_case}(${prop.animated_type()}),
+ % else:
+ ${prop.camel_case}(Void),
+ % endif
+ % endfor
+ /// A custom property.
+ Custom(CustomAnimatedValue),
+}
+
+<%
+ animated = []
+ unanimated = []
+ animated_with_logical = []
+ for prop in data.longhands:
+ if prop.animatable:
+ animated_with_logical.append(prop)
+ if prop.animatable and not prop.logical:
+ animated.append(prop)
+ else:
+ unanimated.append(prop)
+%>
+
+#[repr(C)]
+struct AnimationValueVariantRepr<T> {
+ tag: u16,
+ value: T
+}
+
+impl Clone for AnimationValue {
+ #[inline]
+ fn clone(&self) -> Self {
+ use self::AnimationValue::*;
+
+ <%
+ [copy, others] = [list(g) for _, g in groupby(animated, key=lambda x: not x.specified_is_copy())]
+ %>
+
+ let self_tag = unsafe { *(self as *const _ as *const u16) };
+ if self_tag <= LonghandId::${copy[-1].camel_case} as u16 {
+ #[derive(Clone, Copy)]
+ #[repr(u16)]
+ enum CopyVariants {
+ % for prop in copy:
+ _${prop.camel_case}(${prop.animated_type()}),
+ % endfor
+ }
+
+ unsafe {
+ let mut out = mem::MaybeUninit::uninit();
+ ptr::write(
+ out.as_mut_ptr() as *mut CopyVariants,
+ *(self as *const _ as *const CopyVariants),
+ );
+ return out.assume_init();
+ }
+ }
+
+ match *self {
+ % for ty, props in groupby(others, key=lambda x: x.animated_type()):
+ <% props = list(props) %>
+ ${" |\n".join("{}(ref value)".format(prop.camel_case) for prop in props)} => {
+ % if len(props) == 1:
+ ${props[0].camel_case}(value.clone())
+ % else:
+ unsafe {
+ let mut out = mem::MaybeUninit::uninit();
+ ptr::write(
+ out.as_mut_ptr() as *mut AnimationValueVariantRepr<${ty}>,
+ AnimationValueVariantRepr {
+ tag: *(self as *const _ as *const u16),
+ value: value.clone(),
+ },
+ );
+ out.assume_init()
+ }
+ % endif
+ }
+ % endfor
+ Custom(ref animated_value) => Custom(animated_value.clone()),
+ _ => unsafe { debug_unreachable!() }
+ }
+ }
+}
+
+impl PartialEq for AnimationValue {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ use self::AnimationValue::*;
+
+ unsafe {
+ let this_tag = *(self as *const _ as *const u16);
+ let other_tag = *(other as *const _ as *const u16);
+ if this_tag != other_tag {
+ return false;
+ }
+
+ match *self {
+ % for ty, props in groupby(animated, key=lambda x: x.animated_type()):
+ ${" |\n".join("{}(ref this)".format(prop.camel_case) for prop in props)} => {
+ let other_repr =
+ &*(other as *const _ as *const AnimationValueVariantRepr<${ty}>);
+ *this == other_repr.value
+ }
+ % endfor
+ ${" |\n".join("{}(void)".format(prop.camel_case) for prop in unanimated)} => {
+ void::unreachable(void)
+ },
+ AnimationValue::Custom(ref this) => {
+ let other_repr =
+ &*(other as *const _ as *const AnimationValueVariantRepr<CustomAnimatedValue>);
+ *this == other_repr.value
+ },
+ }
+ }
+ }
+}
+
+impl AnimationValue {
+ /// Returns the longhand id this animated value corresponds to.
+ #[inline]
+ pub fn id(&self) -> PropertyDeclarationId {
+ if let AnimationValue::Custom(animated_value) = self {
+ return PropertyDeclarationId::Custom(&animated_value.name);
+ }
+
+ let id = unsafe { *(self as *const _ as *const LonghandId) };
+ debug_assert_eq!(id, match *self {
+ % for prop in data.longhands:
+ % if prop.animatable and not prop.logical:
+ AnimationValue::${prop.camel_case}(..) => LonghandId::${prop.camel_case},
+ % else:
+ AnimationValue::${prop.camel_case}(void) => void::unreachable(void),
+ % endif
+ % endfor
+ AnimationValue::Custom(..) => unsafe { debug_unreachable!() },
+ });
+ PropertyDeclarationId::Longhand(id)
+ }
+
+ /// Returns whether this value is interpolable with another one.
+ pub fn interpolable_with(&self, other: &Self) -> bool {
+ self.animate(other, Procedure::Interpolate { progress: 0.5 }).is_ok()
+ }
+
+ /// "Uncompute" this animation value in order to be used inside the CSS
+ /// cascade.
+ pub fn uncompute(&self) -> PropertyDeclaration {
+ use crate::properties::longhands;
+ use self::AnimationValue::*;
+
+ use super::PropertyDeclarationVariantRepr;
+
+ match *self {
+ <% keyfunc = lambda x: (x.base_type(), x.specified_type(), x.boxed, x.is_animatable_with_computed_value) %>
+ % for (ty, specified, boxed, computed), props in groupby(animated, key=keyfunc):
+ <% props = list(props) %>
+ ${" |\n".join("{}(ref value)".format(prop.camel_case) for prop in props)} => {
+ % if not computed:
+ let ref value = ToAnimatedValue::from_animated_value(value.clone());
+ % endif
+ let value = ${ty}::from_computed_value(&value);
+ % if boxed:
+ let value = Box::new(value);
+ % endif
+ % if len(props) == 1:
+ PropertyDeclaration::${props[0].camel_case}(value)
+ % else:
+ unsafe {
+ let mut out = mem::MaybeUninit::uninit();
+ ptr::write(
+ out.as_mut_ptr() as *mut PropertyDeclarationVariantRepr<${specified}>,
+ PropertyDeclarationVariantRepr {
+ tag: *(self as *const _ as *const u16),
+ value,
+ },
+ );
+ out.assume_init()
+ }
+ % endif
+ }
+ % endfor
+ ${" |\n".join("{}(void)".format(prop.camel_case) for prop in unanimated)} => {
+ void::unreachable(void)
+ },
+ Custom(ref animated_value) => animated_value.to_declaration(),
+ }
+ }
+
+ /// Construct an AnimationValue from a property declaration.
+ pub fn from_declaration(
+ decl: &PropertyDeclaration,
+ context: &mut Context,
+ initial: &ComputedValues,
+ ) -> Option<Self> {
+ use super::PropertyDeclarationVariantRepr;
+
+ <%
+ keyfunc = lambda x: (
+ x.specified_type(),
+ x.animated_type(),
+ x.boxed,
+ not x.is_animatable_with_computed_value,
+ x.style_struct.inherited,
+ x.ident in SYSTEM_FONT_LONGHANDS and engine == "gecko",
+ )
+ %>
+
+ let animatable = match *decl {
+ % for (specified_ty, ty, boxed, to_animated, inherit, system), props in groupby(animated_with_logical, key=keyfunc):
+ ${" |\n".join("PropertyDeclaration::{}(ref value)".format(prop.camel_case) for prop in props)} => {
+ let decl_repr = unsafe {
+ &*(decl as *const _ as *const PropertyDeclarationVariantRepr<${specified_ty}>)
+ };
+ let longhand_id = unsafe {
+ *(&decl_repr.tag as *const u16 as *const LonghandId)
+ };
+ context.for_non_inherited_property = ${"false" if inherit else "true"};
+ % if system:
+ if let Some(sf) = value.get_system() {
+ longhands::system_font::resolve_system_font(sf, context)
+ }
+ % endif
+ % if boxed:
+ let value = (**value).to_computed_value(context);
+ % else:
+ let value = value.to_computed_value(context);
+ % endif
+ % if to_animated:
+ let value = value.to_animated_value();
+ % endif
+
+ unsafe {
+ let mut out = mem::MaybeUninit::uninit();
+ ptr::write(
+ out.as_mut_ptr() as *mut AnimationValueVariantRepr<${ty}>,
+ AnimationValueVariantRepr {
+ tag: longhand_id.to_physical(context.builder.writing_mode) as u16,
+ value,
+ },
+ );
+ out.assume_init()
+ }
+ }
+ % endfor
+ PropertyDeclaration::CSSWideKeyword(ref declaration) => {
+ match declaration.id.to_physical(context.builder.writing_mode) {
+ // We put all the animatable properties first in the hopes
+ // that it might increase match locality.
+ % for prop in data.longhands:
+ % if prop.animatable and not prop.logical:
+ LonghandId::${prop.camel_case} => {
+ // FIXME(emilio, bug 1533327): I think revert (and
+ // revert-layer) handling is not fine here, but what to
+ // do instead?
+ //
+ // Seems we'd need the computed value as if it was
+ // revert, somehow. Treating it as `unset` seems fine
+ // for now...
+ let style_struct = match declaration.keyword {
+ % if not prop.style_struct.inherited:
+ CSSWideKeyword::Revert |
+ CSSWideKeyword::RevertLayer |
+ CSSWideKeyword::Unset |
+ % endif
+ CSSWideKeyword::Initial => {
+ initial.get_${prop.style_struct.name_lower}()
+ },
+ % if prop.style_struct.inherited:
+ CSSWideKeyword::Revert |
+ CSSWideKeyword::RevertLayer |
+ CSSWideKeyword::Unset |
+ % endif
+ CSSWideKeyword::Inherit => {
+ context.builder
+ .get_parent_${prop.style_struct.name_lower}()
+ },
+ };
+ let computed = style_struct
+ % if prop.logical:
+ .clone_${prop.ident}(context.builder.writing_mode);
+ % else:
+ .clone_${prop.ident}();
+ % endif
+
+ % if not prop.is_animatable_with_computed_value:
+ let computed = computed.to_animated_value();
+ % endif
+ AnimationValue::${prop.camel_case}(computed)
+ },
+ % endif
+ % endfor
+ % for prop in data.longhands:
+ % if not prop.animatable or prop.logical:
+ LonghandId::${prop.camel_case} => return None,
+ % endif
+ % endfor
+ }
+ },
+ PropertyDeclaration::WithVariables(ref declaration) => {
+ let mut cache = Default::default();
+ let substituted = {
+ let custom_properties = &context.style().custom_properties();
+
+ debug_assert!(
+ context.builder.stylist.is_some(),
+ "Need a Stylist to substitute variables!"
+ );
+ declaration.value.substitute_variables(
+ declaration.id,
+ custom_properties,
+ context.builder.stylist.unwrap(),
+ context,
+ &mut cache,
+ )
+ };
+ return AnimationValue::from_declaration(
+ &substituted,
+ context,
+ initial,
+ )
+ },
+ PropertyDeclaration::Custom(ref declaration) => {
+ AnimationValue::Custom(CustomAnimatedValue::from_declaration(
+ declaration,
+ context,
+ initial,
+ )?)
+ },
+ _ => return None // non animatable properties will get included because of shorthands. ignore.
+ };
+ Some(animatable)
+ }
+
+ /// Get an AnimationValue for an declaration id from a given computed values.
+ pub fn from_computed_values(
+ property: PropertyDeclarationId,
+ style: &ComputedValues,
+ ) -> Option<Self> {
+ let property = match property {
+ PropertyDeclarationId::Longhand(id) => id,
+ PropertyDeclarationId::Custom(ref name) => {
+ // FIXME(bug 1869476): This should use a stylist to determine whether the name
+ // corresponds to an inherited custom property and then choose the
+ // inherited/non_inherited map accordingly.
+ let p = &style.custom_properties();
+ let value = p.inherited.get(*name).or_else(|| p.non_inherited.get(*name))?;
+ return Some(AnimationValue::Custom(CustomAnimatedValue::from_computed(name, value)))
+ }
+ };
+
+ Some(match property {
+ % for prop in data.longhands:
+ % if prop.animatable and not prop.logical:
+ LonghandId::${prop.camel_case} => {
+ let computed = style.clone_${prop.ident}();
+ AnimationValue::${prop.camel_case}(
+ % if prop.is_animatable_with_computed_value:
+ computed
+ % else:
+ computed.to_animated_value()
+ % endif
+ )
+ }
+ % endif
+ % endfor
+ _ => return None,
+ })
+ }
+
+ /// Update `style` with the value of this `AnimationValue`.
+ ///
+ /// SERVO ONLY: This doesn't properly handle things like updating 'em' units
+ /// when animated font-size.
+ #[cfg(feature = "servo")]
+ pub fn set_in_style_for_servo(&self, style: &mut ComputedValues) {
+ match self {
+ % for prop in data.longhands:
+ % if prop.animatable and not prop.logical:
+ AnimationValue::${prop.camel_case}(ref value) => {
+ % if not prop.is_animatable_with_computed_value:
+ let value: longhands::${prop.ident}::computed_value::T =
+ ToAnimatedValue::from_animated_value(value.clone());
+ style.mutate_${prop.style_struct.name_lower}().set_${prop.ident}(value);
+ % else:
+ style.mutate_${prop.style_struct.name_lower}().set_${prop.ident}(value.clone());
+ % endif
+ }
+ % else:
+ AnimationValue::${prop.camel_case}(..) => unreachable!(),
+ % endif
+ % endfor
+ AnimationValue::Custom(..) => unreachable!(),
+ }
+ }
+
+ /// As above, but a stub for Gecko.
+ #[cfg(feature = "gecko")]
+ pub fn set_in_style_for_servo(&self, _: &mut ComputedValues) {
+ }
+}
+
+fn animate_discrete<T: Clone>(this: &T, other: &T, procedure: Procedure) -> Result<T, ()> {
+ if let Procedure::Interpolate { progress } = procedure {
+ Ok(if progress < 0.5 { this.clone() } else { other.clone() })
+ } else {
+ Err(())
+ }
+}
+
+impl Animate for AnimationValue {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(unsafe {
+ use self::AnimationValue::*;
+
+ let this_tag = *(self as *const _ as *const u16);
+ let other_tag = *(other as *const _ as *const u16);
+ if this_tag != other_tag {
+ panic!("Unexpected AnimationValue::animate call");
+ }
+
+ match *self {
+ <% keyfunc = lambda x: (x.animated_type(), x.animation_value_type == "discrete") %>
+ % for (ty, discrete), props in groupby(animated, key=keyfunc):
+ ${" |\n".join("{}(ref this)".format(prop.camel_case) for prop in props)} => {
+ let other_repr =
+ &*(other as *const _ as *const AnimationValueVariantRepr<${ty}>);
+ % if discrete:
+ let value = animate_discrete(this, &other_repr.value, procedure)?;
+ % else:
+ let value = this.animate(&other_repr.value, procedure)?;
+ % endif
+
+ let mut out = mem::MaybeUninit::uninit();
+ ptr::write(
+ out.as_mut_ptr() as *mut AnimationValueVariantRepr<${ty}>,
+ AnimationValueVariantRepr {
+ tag: this_tag,
+ value,
+ },
+ );
+ out.assume_init()
+ },
+ % endfor
+ ${" |\n".join("{}(void)".format(prop.camel_case) for prop in unanimated)} => {
+ void::unreachable(void)
+ },
+ Custom(ref self_value) => {
+ let Custom(ref other_value) = *other else { unreachable!() };
+ Custom(self_value.animate(other_value, procedure)?)
+ },
+ }
+ })
+ }
+}
+
+<%
+ nondiscrete = []
+ for prop in animated:
+ if prop.animation_value_type != "discrete":
+ nondiscrete.append(prop)
+%>
+
+impl ComputeSquaredDistance for AnimationValue {
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ unsafe {
+ use self::AnimationValue::*;
+
+ let this_tag = *(self as *const _ as *const u16);
+ let other_tag = *(other as *const _ as *const u16);
+ if this_tag != other_tag {
+ panic!("Unexpected AnimationValue::compute_squared_distance call");
+ }
+
+ match *self {
+ % for ty, props in groupby(nondiscrete, key=lambda x: x.animated_type()):
+ ${" |\n".join("{}(ref this)".format(prop.camel_case) for prop in props)} => {
+ let other_repr =
+ &*(other as *const _ as *const AnimationValueVariantRepr<${ty}>);
+
+ this.compute_squared_distance(&other_repr.value)
+ }
+ % endfor
+ _ => Err(()),
+ }
+ }
+ }
+}
+
+impl ToAnimatedZero for AnimationValue {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ match *self {
+ % for prop in data.longhands:
+ % if prop.animatable and not prop.logical and prop.animation_value_type != "discrete":
+ AnimationValue::${prop.camel_case}(ref base) => {
+ Ok(AnimationValue::${prop.camel_case}(base.to_animated_zero()?))
+ },
+ % endif
+ % endfor
+ AnimationValue::Custom(..) => {
+ // TODO(bug 1869185): For some non-universal registered custom properties, it may make sense to implement this.
+ Err(())
+ },
+ _ => Err(()),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/web-animations-1/#animating-visibility>
+impl Animate for Visibility {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match procedure {
+ Procedure::Interpolate { .. } => {
+ let (this_weight, other_weight) = procedure.weights();
+ match (*self, *other) {
+ (Visibility::Visible, _) => {
+ Ok(if this_weight > 0.0 { *self } else { *other })
+ },
+ (_, Visibility::Visible) => {
+ Ok(if other_weight > 0.0 { *other } else { *self })
+ },
+ _ => Err(()),
+ }
+ },
+ _ => Err(()),
+ }
+ }
+}
+
+impl ComputeSquaredDistance for Visibility {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt(if *self == *other { 0. } else { 1. }))
+ }
+}
+
+impl ToAnimatedZero for Visibility {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+/// <https://drafts.csswg.org/css-contain-3/#content-visibility-animation>
+impl Animate for ContentVisibility {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match procedure {
+ Procedure::Interpolate { .. } => {
+ let (this_weight, other_weight) = procedure.weights();
+ match (*self, *other) {
+ (ContentVisibility::Hidden, _) => {
+ Ok(if other_weight > 0.0 { *other } else { *self })
+ },
+ (_, ContentVisibility::Hidden) => {
+ Ok(if this_weight > 0.0 { *self } else { *other })
+ },
+ _ => Err(()),
+ }
+ },
+ _ => Err(()),
+ }
+ }
+}
+
+impl ComputeSquaredDistance for ContentVisibility {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt(if *self == *other { 0. } else { 1. }))
+ }
+}
+
+impl ToAnimatedZero for ContentVisibility {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+/// <https://drafts.csswg.org/css-transitions/#animtype-rect>
+impl Animate for ClipRect {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ use crate::values::computed::LengthOrAuto;
+ let animate_component = |this: &LengthOrAuto, other: &LengthOrAuto| {
+ let result = this.animate(other, procedure)?;
+ if let Procedure::Interpolate { .. } = procedure {
+ return Ok(result);
+ }
+ if result.is_auto() {
+ // FIXME(emilio): Why? A couple SMIL tests fail without this,
+ // but it seems extremely fishy.
+ return Err(());
+ }
+ Ok(result)
+ };
+
+ Ok(ClipRect {
+ top: animate_component(&self.top, &other.top)?,
+ right: animate_component(&self.right, &other.right)?,
+ bottom: animate_component(&self.bottom, &other.bottom)?,
+ left: animate_component(&self.left, &other.left)?,
+ })
+ }
+}
+
+<%
+ FILTER_FUNCTIONS = [ 'Blur', 'Brightness', 'Contrast', 'Grayscale',
+ 'HueRotate', 'Invert', 'Opacity', 'Saturate',
+ 'Sepia' ]
+%>
+
+/// <https://drafts.fxtf.org/filters/#animation-of-filters>
+impl Animate for AnimatedFilter {
+ fn animate(
+ &self,
+ other: &Self,
+ procedure: Procedure,
+ ) -> Result<Self, ()> {
+ use crate::values::animated::animate_multiplicative_factor;
+ match (self, other) {
+ % for func in ['Blur', 'Grayscale', 'HueRotate', 'Invert', 'Sepia']:
+ (&Filter::${func}(ref this), &Filter::${func}(ref other)) => {
+ Ok(Filter::${func}(this.animate(other, procedure)?))
+ },
+ % endfor
+ % for func in ['Brightness', 'Contrast', 'Opacity', 'Saturate']:
+ (&Filter::${func}(this), &Filter::${func}(other)) => {
+ Ok(Filter::${func}(animate_multiplicative_factor(this, other, procedure)?))
+ },
+ % endfor
+ % if engine == "gecko":
+ (&Filter::DropShadow(ref this), &Filter::DropShadow(ref other)) => {
+ Ok(Filter::DropShadow(this.animate(other, procedure)?))
+ },
+ % endif
+ _ => Err(()),
+ }
+ }
+}
+
+/// <http://dev.w3.org/csswg/css-transforms/#none-transform-animation>
+impl ToAnimatedZero for AnimatedFilter {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ match *self {
+ % for func in ['Blur', 'Grayscale', 'HueRotate', 'Invert', 'Sepia']:
+ Filter::${func}(ref this) => Ok(Filter::${func}(this.to_animated_zero()?)),
+ % endfor
+ % for func in ['Brightness', 'Contrast', 'Opacity', 'Saturate']:
+ Filter::${func}(_) => Ok(Filter::${func}(1.)),
+ % endfor
+ % if engine == "gecko":
+ Filter::DropShadow(ref this) => Ok(Filter::DropShadow(this.to_animated_zero()?)),
+ % endif
+ _ => Err(()),
+ }
+ }
+}
+
+/// An iterator over all the properties that transition on a given style.
+pub struct TransitionPropertyIterator<'a> {
+ style: &'a ComputedValues,
+ index_range: core::ops::Range<usize>,
+ longhand_iterator: Option<NonCustomPropertyIterator<LonghandId>>,
+}
+
+impl<'a> TransitionPropertyIterator<'a> {
+ /// Create a `TransitionPropertyIterator` for the given style.
+ pub fn from_style(style: &'a ComputedValues) -> Self {
+ Self {
+ style,
+ index_range: 0..style.get_ui().transition_property_count(),
+ longhand_iterator: None,
+ }
+ }
+}
+
+/// A single iteration of the TransitionPropertyIterator.
+pub struct TransitionPropertyIteration {
+ /// The id of the longhand for this property.
+ pub longhand_id: LonghandId,
+
+ /// The index of this property in the list of transition properties for this
+ /// iterator's style.
+ pub index: usize,
+}
+
+impl<'a> Iterator for TransitionPropertyIterator<'a> {
+ type Item = TransitionPropertyIteration;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ use crate::values::computed::TransitionProperty;
+ loop {
+ if let Some(ref mut longhand_iterator) = self.longhand_iterator {
+ if let Some(longhand_id) = longhand_iterator.next() {
+ return Some(TransitionPropertyIteration {
+ longhand_id,
+ index: self.index_range.start - 1,
+ });
+ }
+ self.longhand_iterator = None;
+ }
+
+ let index = self.index_range.next()?;
+ match self.style.get_ui().transition_property_at(index) {
+ TransitionProperty::NonCustom(id) => {
+ match id.longhand_or_shorthand() {
+ Ok(longhand_id) => {
+ return Some(TransitionPropertyIteration {
+ longhand_id,
+ index,
+ });
+ },
+ Err(shorthand_id) => {
+ // In the other cases, we set up our state so that we are ready to
+ // compute the next value of the iterator and then loop (equivalent
+ // to calling self.next()).
+ self.longhand_iterator = Some(shorthand_id.longhands());
+ },
+ }
+ }
+ TransitionProperty::Custom(..) | TransitionProperty::Unsupported(..) => {}
+ }
+ }
+ }
+}
diff --git a/servo/components/style/properties/longhands/background.mako.rs b/servo/components/style/properties/longhands/background.mako.rs
new file mode 100644
index 0000000000..48270f748e
--- /dev/null
+++ b/servo/components/style/properties/longhands/background.mako.rs
@@ -0,0 +1,126 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("Background", inherited=False) %>
+
+${helpers.predefined_type(
+ "background-color",
+ "Color",
+ "computed::Color::TRANSPARENT_BLACK",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="SpecifiedValue::transparent()",
+ spec="https://drafts.csswg.org/css-backgrounds/#background-color",
+ animation_value_type="AnimatedColor",
+ ignored_when_colors_disabled=True,
+ allow_quirks="Yes",
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "background-image",
+ "Image",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::Image::None",
+ initial_specified_value="specified::Image::None",
+ spec="https://drafts.csswg.org/css-backgrounds/#the-background-image",
+ vector="True",
+ animation_value_type="discrete",
+ ignored_when_colors_disabled="True",
+ affects="paint",
+)}
+
+% for (axis, direction, initial) in [("x", "Horizontal", "left"), ("y", "Vertical", "top")]:
+ ${helpers.predefined_type(
+ "background-position-" + axis,
+ "position::" + direction + "Position",
+ "computed::LengthPercentage::zero_percent()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="SpecifiedValue::initial_specified_value()",
+ spec="https://drafts.csswg.org/css-backgrounds-4/#propdef-background-position-" + axis,
+ animation_value_type="ComputedValue",
+ vector=True,
+ vector_animation_type="repeatable_list",
+ affects="paint",
+ )}
+% endfor
+
+${helpers.predefined_type(
+ "background-repeat",
+ "BackgroundRepeat",
+ "computed::BackgroundRepeat::repeat()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::BackgroundRepeat::repeat()",
+ animation_value_type="discrete",
+ vector=True,
+ spec="https://drafts.csswg.org/css-backgrounds/#the-background-repeat",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "background-attachment",
+ "scroll" + (" fixed" if engine in ["gecko", "servo-2013"] else "") + (" local" if engine == "gecko" else ""),
+ engines="gecko servo-2013 servo-2020",
+ vector=True,
+ gecko_enum_prefix="StyleImageLayerAttachment",
+ spec="https://drafts.csswg.org/css-backgrounds/#the-background-attachment",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "background-clip",
+ "border-box padding-box content-box",
+ engines="gecko servo-2013 servo-2020",
+ extra_gecko_values="text",
+ vector=True, extra_prefixes="webkit",
+ gecko_enum_prefix="StyleGeometryBox",
+ gecko_inexhaustive=True,
+ spec="https://drafts.csswg.org/css-backgrounds/#the-background-clip",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "background-origin",
+ "padding-box border-box content-box",
+ engines="gecko servo-2013 servo-2020",
+ vector=True, extra_prefixes="webkit",
+ gecko_enum_prefix="StyleGeometryBox",
+ gecko_inexhaustive=True,
+ spec="https://drafts.csswg.org/css-backgrounds/#the-background-origin",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "background-size",
+ "BackgroundSize",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::BackgroundSize::auto()",
+ initial_specified_value="specified::BackgroundSize::auto()",
+ spec="https://drafts.csswg.org/css-backgrounds/#the-background-size",
+ vector=True,
+ vector_animation_type="repeatable_list",
+ animation_value_type="BackgroundSizeList",
+ extra_prefixes="webkit",
+ affects="paint",
+)}
+
+// https://drafts.fxtf.org/compositing/#background-blend-mode
+${helpers.single_keyword(
+ "background-blend-mode",
+ """normal multiply screen overlay darken lighten color-dodge
+ color-burn hard-light soft-light difference exclusion hue
+ saturation color luminosity""",
+ gecko_enum_prefix="StyleBlend",
+ vector=True,
+ engines="gecko",
+ animation_value_type="discrete",
+ gecko_inexhaustive=True,
+ spec="https://drafts.fxtf.org/compositing/#background-blend-mode",
+ affects="paint",
+)}
diff --git a/servo/components/style/properties/longhands/border.mako.rs b/servo/components/style/properties/longhands/border.mako.rs
new file mode 100644
index 0000000000..4d0676f678
--- /dev/null
+++ b/servo/components/style/properties/longhands/border.mako.rs
@@ -0,0 +1,170 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import Keyword, Method, ALL_CORNERS, PHYSICAL_SIDES, ALL_SIDES, maybe_moz_logical_alias %>
+
+<% data.new_style_struct("Border", inherited=False,
+ additional_methods=[Method("border_" + side + "_has_nonzero_width",
+ "bool") for side in ["top", "right", "bottom", "left"]]) %>
+<%
+ def maybe_logical_spec(side, kind):
+ if side[1]: # if it is logical
+ return "https://drafts.csswg.org/css-logical-props/#propdef-border-%s-%s" % (side[0], kind)
+ else:
+ return "https://drafts.csswg.org/css-backgrounds/#border-%s-%s" % (side[0], kind)
+%>
+% for side in ALL_SIDES:
+ <%
+ side_name = side[0]
+ is_logical = side[1]
+ %>
+ ${helpers.predefined_type(
+ "border-%s-color" % side_name, "Color",
+ "computed_value::T::currentcolor()",
+ engines="gecko servo-2013 servo-2020",
+ aliases=maybe_moz_logical_alias(engine, side, "-moz-border-%s-color"),
+ spec=maybe_logical_spec(side, "color"),
+ animation_value_type="AnimatedColor",
+ logical=is_logical,
+ logical_group="border-color",
+ allow_quirks="No" if is_logical else "Yes",
+ ignored_when_colors_disabled=True,
+ affects="paint",
+ )}
+
+ ${helpers.predefined_type(
+ "border-%s-style" % side_name, "BorderStyle",
+ "specified::BorderStyle::None",
+ engines="gecko servo-2013 servo-2020",
+ aliases=maybe_moz_logical_alias(engine, side, "-moz-border-%s-style"),
+ spec=maybe_logical_spec(side, "style"),
+ animation_value_type="discrete" if not is_logical else "none",
+ logical=is_logical,
+ logical_group="border-style",
+ affects="layout",
+ )}
+
+ ${helpers.predefined_type(
+ "border-%s-width" % side_name,
+ "BorderSideWidth",
+ "app_units::Au::from_px(3)",
+ engines="gecko servo-2013 servo-2020",
+ aliases=maybe_moz_logical_alias(engine, side, "-moz-border-%s-width"),
+ spec=maybe_logical_spec(side, "width"),
+ animation_value_type="NonNegativeLength",
+ logical=is_logical,
+ logical_group="border-width",
+ allow_quirks="No" if is_logical else "Yes",
+ servo_restyle_damage="reflow rebuild_and_reflow_inline",
+ affects="layout",
+ )}
+% endfor
+
+% for corner in ALL_CORNERS:
+ <%
+ corner_name = corner[0]
+ is_logical = corner[1]
+ if is_logical:
+ prefixes = None
+ else:
+ prefixes = "webkit"
+ %>
+ ${helpers.predefined_type(
+ "border-%s-radius" % corner_name,
+ "BorderCornerRadius",
+ "computed::BorderCornerRadius::zero()",
+ "parse",
+ engines="gecko servo-2013 servo-2020",
+ extra_prefixes=prefixes,
+ spec=maybe_logical_spec(corner, "radius"),
+ boxed=True,
+ animation_value_type="BorderCornerRadius",
+ logical_group="border-radius",
+ logical=is_logical,
+ affects="paint",
+ )}
+% endfor
+
+${helpers.single_keyword(
+ "box-decoration-break",
+ "slice clone",
+ engines="gecko",
+ gecko_enum_prefix="StyleBoxDecorationBreak",
+ spec="https://drafts.csswg.org/css-break/#propdef-box-decoration-break",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "-moz-float-edge",
+ "content-box margin-box",
+ engines="gecko",
+ gecko_ffi_name="mFloatEdge",
+ gecko_enum_prefix="StyleFloatEdge",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-float-edge)",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "border-image-source",
+ "Image",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::Image::None",
+ initial_specified_value="specified::Image::None",
+ spec="https://drafts.csswg.org/css-backgrounds/#the-background-image",
+ vector=False,
+ animation_value_type="discrete",
+ boxed=engine == "servo-2013",
+ ignored_when_colors_disabled=True,
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "border-image-outset",
+ "NonNegativeLengthOrNumberRect",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="generics::rect::Rect::all(computed::NonNegativeLengthOrNumber::zero())",
+ initial_specified_value="generics::rect::Rect::all(specified::NonNegativeLengthOrNumber::zero())",
+ spec="https://drafts.csswg.org/css-backgrounds/#border-image-outset",
+ animation_value_type="NonNegativeLengthOrNumberRect",
+ boxed=True,
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "border-image-repeat",
+ "BorderImageRepeat",
+ "computed::BorderImageRepeat::stretch()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::BorderImageRepeat::stretch()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-backgrounds/#the-border-image-repeat",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "border-image-width",
+ "BorderImageWidth",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::BorderImageWidth::all(computed::BorderImageSideWidth::one())",
+ initial_specified_value="specified::BorderImageWidth::all(specified::BorderImageSideWidth::one())",
+ spec="https://drafts.csswg.org/css-backgrounds/#border-image-width",
+ animation_value_type="BorderImageWidth",
+ boxed=True,
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "border-image-slice",
+ "BorderImageSlice",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::BorderImageSlice::hundred_percent()",
+ initial_specified_value="specified::BorderImageSlice::hundred_percent()",
+ spec="https://drafts.csswg.org/css-backgrounds/#border-image-slice",
+ animation_value_type="BorderImageSlice",
+ boxed=True,
+ affects="paint",
+)}
diff --git a/servo/components/style/properties/longhands/box.mako.rs b/servo/components/style/properties/longhands/box.mako.rs
new file mode 100644
index 0000000000..017bef38ea
--- /dev/null
+++ b/servo/components/style/properties/longhands/box.mako.rs
@@ -0,0 +1,644 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import ALL_AXES, Keyword, Method, to_rust_ident, to_camel_case%>
+
+<% data.new_style_struct("Box",
+ inherited=False,
+ gecko_name="Display") %>
+
+${helpers.predefined_type(
+ "display",
+ "Display",
+ "computed::Display::inline()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::Display::inline()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-display/#propdef-display",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "-moz-top-layer",
+ "none top",
+ engines="gecko",
+ gecko_enum_prefix="StyleTopLayer",
+ gecko_ffi_name="mTopLayer",
+ animation_value_type="none",
+ enabled_in="ua",
+ spec="Internal (not web-exposed)",
+ affects="layout",
+)}
+
+// An internal-only property for elements in a top layer
+// https://fullscreen.spec.whatwg.org/#top-layer
+${helpers.single_keyword(
+ "-servo-top-layer",
+ "none top",
+ engines="servo-2013 servo-2020",
+ animation_value_type="none",
+ enabled_in="ua",
+ spec="Internal (not web-exposed)",
+ affects="layout",
+)}
+
+<%helpers:single_keyword
+ name="position"
+ values="static absolute relative fixed ${'sticky' if engine in ['gecko', 'servo-2013'] else ''}"
+ engines="gecko servo-2013 servo-2020"
+ animation_value_type="discrete"
+ gecko_enum_prefix="StylePositionProperty"
+ spec="https://drafts.csswg.org/css-position/#position-property"
+ servo_restyle_damage="rebuild_and_reflow"
+ affects="layout"
+>
+impl computed_value::T {
+ pub fn is_absolutely_positioned(self) -> bool {
+ matches!(self, Self::Absolute | Self::Fixed)
+ }
+ pub fn is_relative(self) -> bool {
+ self == Self::Relative
+ }
+}
+</%helpers:single_keyword>
+
+${helpers.predefined_type(
+ "float",
+ "Float",
+ "computed::Float::None",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ initial_specified_value="specified::Float::None",
+ spec="https://drafts.csswg.org/css-box/#propdef-float",
+ animation_value_type="discrete",
+ servo_restyle_damage="rebuild_and_reflow",
+ gecko_ffi_name="mFloat",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "clear",
+ "Clear",
+ "computed::Clear::None",
+ engines="gecko servo-2013",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css2/#propdef-clear",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "vertical-align",
+ "VerticalAlign",
+ "computed::VerticalAlign::baseline()",
+ engines="gecko servo-2013",
+ animation_value_type="ComputedValue",
+ spec="https://www.w3.org/TR/CSS2/visudet.html#propdef-vertical-align",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "baseline-source",
+ "BaselineSource",
+ "computed::BaselineSource::Auto",
+ engines="gecko servo-2013",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-inline-3/#baseline-source",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+)}
+
+// CSS 2.1, Section 11 - Visual effects
+
+${helpers.single_keyword(
+ "-servo-overflow-clip-box",
+ "padding-box content-box",
+ engines="servo-2013",
+ animation_value_type="none",
+ enabled_in="ua",
+ spec="Internal, not web-exposed, \
+ may be standardized in the future (https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-box)",
+ affects="layout",
+)}
+
+% for direction in ["inline", "block"]:
+ ${helpers.predefined_type(
+ "overflow-clip-box-" + direction,
+ "OverflowClipBox",
+ "computed::OverflowClipBox::PaddingBox",
+ engines="gecko",
+ enabled_in="ua",
+ gecko_pref="layout.css.overflow-clip-box.enabled",
+ animation_value_type="discrete",
+ spec="Internal, may be standardized in the future: \
+ https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-box",
+ affects="layout",
+ )}
+% endfor
+
+% for (axis, logical) in ALL_AXES:
+ <% full_name = "overflow-{}".format(axis) %>
+ ${helpers.predefined_type(
+ full_name,
+ "Overflow",
+ "computed::Overflow::Visible",
+ engines="gecko servo-2013 servo-2020",
+ logical_group="overflow",
+ logical=logical,
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-overflow-3/#propdef-{}".format(full_name),
+ servo_restyle_damage = "reflow",
+ affects="layout",
+ )}
+% endfor
+
+${helpers.predefined_type(
+ "overflow-anchor",
+ "OverflowAnchor",
+ "computed::OverflowAnchor::Auto",
+ engines="gecko",
+ initial_specified_value="specified::OverflowAnchor::Auto",
+ gecko_pref="layout.css.scroll-anchoring.enabled",
+ spec="https://drafts.csswg.org/css-scroll-anchoring/#exclusion-api",
+ animation_value_type="discrete",
+ affects="",
+)}
+
+<% transform_extra_prefixes = "moz:layout.css.prefixes.transforms webkit" %>
+
+${helpers.predefined_type(
+ "transform",
+ "Transform",
+ "generics::transform::Transform::none()",
+ engines="gecko servo-2013 servo-2020",
+ extra_prefixes=transform_extra_prefixes,
+ animation_value_type="ComputedValue",
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ spec="https://drafts.csswg.org/css-transforms/#propdef-transform",
+ servo_restyle_damage="reflow_out_of_flow",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "rotate",
+ "Rotate",
+ "generics::transform::Rotate::None",
+ engines="gecko servo-2013",
+ animation_value_type="ComputedValue",
+ boxed=True,
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ gecko_pref="layout.css.individual-transform.enabled",
+ spec="https://drafts.csswg.org/css-transforms-2/#individual-transforms",
+ servo_restyle_damage = "reflow_out_of_flow",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "scale",
+ "Scale",
+ "generics::transform::Scale::None",
+ engines="gecko servo-2013",
+ animation_value_type="ComputedValue",
+ boxed=True,
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ gecko_pref="layout.css.individual-transform.enabled",
+ spec="https://drafts.csswg.org/css-transforms-2/#individual-transforms",
+ servo_restyle_damage = "reflow_out_of_flow",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "translate",
+ "Translate",
+ "generics::transform::Translate::None",
+ engines="gecko servo-2013",
+ animation_value_type="ComputedValue",
+ boxed=True,
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ gecko_pref="layout.css.individual-transform.enabled",
+ spec="https://drafts.csswg.org/css-transforms-2/#individual-transforms",
+ servo_restyle_damage="reflow_out_of_flow",
+ affects="overflow",
+)}
+
+// Motion Path Module Level 1
+${helpers.predefined_type(
+ "offset-path",
+ "OffsetPath",
+ "computed::OffsetPath::none()",
+ engines="gecko",
+ animation_value_type="motion::OffsetPath",
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ spec="https://drafts.fxtf.org/motion-1/#offset-path-property",
+ servo_restyle_damage="reflow_out_of_flow",
+ affects="overflow",
+)}
+
+// Motion Path Module Level 1
+${helpers.predefined_type(
+ "offset-distance",
+ "LengthPercentage",
+ "computed::LengthPercentage::zero()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ spec="https://drafts.fxtf.org/motion-1/#offset-distance-property",
+ servo_restyle_damage="reflow_out_of_flow",
+ affects="overflow",
+)}
+
+// Motion Path Module Level 1
+${helpers.predefined_type(
+ "offset-rotate",
+ "OffsetRotate",
+ "computed::OffsetRotate::auto()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ spec="https://drafts.fxtf.org/motion-1/#offset-rotate-property",
+ servo_restyle_damage="reflow_out_of_flow",
+ affects="overflow",
+)}
+
+// Motion Path Module Level 1
+${helpers.predefined_type(
+ "offset-anchor",
+ "PositionOrAuto",
+ "computed::PositionOrAuto::auto()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ spec="https://drafts.fxtf.org/motion-1/#offset-anchor-property",
+ servo_restyle_damage="reflow_out_of_flow",
+ boxed=True,
+ affects="overflow",
+)}
+
+// Motion Path Module Level 1
+${helpers.predefined_type(
+ "offset-position",
+ "OffsetPosition",
+ "computed::OffsetPosition::normal()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ gecko_pref="layout.css.motion-path-offset-position.enabled",
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ spec="https://drafts.fxtf.org/motion-1/#offset-position-property",
+ servo_restyle_damage="reflow_out_of_flow",
+ boxed=True,
+ affects="overflow",
+)}
+
+// CSSOM View Module
+// https://www.w3.org/TR/cssom-view-1/
+${helpers.single_keyword(
+ "scroll-behavior",
+ "auto smooth",
+ engines="gecko",
+ spec="https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior",
+ animation_value_type="discrete",
+ gecko_enum_prefix="StyleScrollBehavior",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "scroll-snap-align",
+ "ScrollSnapAlign",
+ "computed::ScrollSnapAlign::none()",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "scroll-snap-type",
+ "ScrollSnapType",
+ "computed::ScrollSnapType::none()",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "scroll-snap-stop",
+ "ScrollSnapStop",
+ "computed::ScrollSnapStop::Normal",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-stop",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+% for (axis, logical) in ALL_AXES:
+ ${helpers.predefined_type(
+ "overscroll-behavior-" + axis,
+ "OverscrollBehavior",
+ "computed::OverscrollBehavior::Auto",
+ engines="gecko",
+ logical_group="overscroll-behavior",
+ logical=logical,
+ gecko_pref="layout.css.overscroll-behavior.enabled",
+ spec="https://wicg.github.io/overscroll-behavior/#overscroll-behavior-properties",
+ animation_value_type="discrete",
+ affects="paint",
+ )}
+% endfor
+
+// Compositing and Blending Level 1
+// http://www.w3.org/TR/compositing-1/
+${helpers.single_keyword(
+ "isolation",
+ "auto isolate",
+ engines="gecko",
+ spec="https://drafts.fxtf.org/compositing/#isolation",
+ gecko_enum_prefix="StyleIsolation",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "break-after",
+ "BreakBetween",
+ "computed::BreakBetween::Auto",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-break/#propdef-break-after",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "break-before",
+ "BreakBetween",
+ "computed::BreakBetween::Auto",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-break/#propdef-break-before",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "break-inside",
+ "BreakWithin",
+ "computed::BreakWithin::Auto",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-break/#propdef-break-inside",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+// CSS Basic User Interface Module Level 3
+// http://dev.w3.org/csswg/css-ui
+${helpers.predefined_type(
+ "resize",
+ "Resize",
+ "computed::Resize::None",
+ engines="gecko",
+ animation_value_type="discrete",
+ gecko_ffi_name="mResize",
+ spec="https://drafts.csswg.org/css-ui/#propdef-resize",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "perspective",
+ "Perspective",
+ "computed::Perspective::none()",
+ engines="gecko servo-2013 servo-2020",
+ gecko_ffi_name="mChildPerspective",
+ spec="https://drafts.csswg.org/css-transforms/#perspective",
+ extra_prefixes=transform_extra_prefixes,
+ animation_value_type="AnimatedPerspective",
+ servo_restyle_damage = "reflow_out_of_flow",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "perspective-origin",
+ "Position",
+ "computed::position::Position::center()",
+ engines="gecko servo-2013 servo-2020",
+ boxed=True,
+ extra_prefixes=transform_extra_prefixes,
+ spec="https://drafts.csswg.org/css-transforms-2/#perspective-origin-property",
+ animation_value_type="ComputedValue",
+ servo_restyle_damage="reflow_out_of_flow",
+ affects="overflow",
+)}
+
+${helpers.single_keyword(
+ "backface-visibility",
+ "visible hidden",
+ engines="gecko servo-2013 servo-2020",
+ gecko_enum_prefix="StyleBackfaceVisibility",
+ spec="https://drafts.csswg.org/css-transforms/#backface-visibility-property",
+ extra_prefixes=transform_extra_prefixes,
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "transform-box",
+ "TransformBox",
+ "computed::TransformBox::ViewBox",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-transforms/#transform-box",
+ animation_value_type="discrete",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "transform-style",
+ "TransformStyle",
+ "computed::TransformStyle::Flat",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-transforms-2/#transform-style-property",
+ extra_prefixes=transform_extra_prefixes,
+ animation_value_type="discrete",
+ servo_restyle_damage = "reflow_out_of_flow",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "transform-origin",
+ "TransformOrigin",
+ "computed::TransformOrigin::initial_value()",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="ComputedValue",
+ extra_prefixes=transform_extra_prefixes,
+ gecko_ffi_name="mTransformOrigin",
+ boxed=True,
+ spec="https://drafts.csswg.org/css-transforms/#transform-origin-property",
+ servo_restyle_damage="reflow_out_of_flow",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "contain",
+ "Contain",
+ "specified::Contain::empty()",
+ engines="gecko",
+ animation_value_type="none",
+ spec="https://drafts.csswg.org/css-contain/#contain-property",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "content-visibility",
+ "ContentVisibility",
+ "computed::ContentVisibility::Visible",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-contain/#content-visibility",
+ gecko_pref="layout.css.content-visibility.enabled",
+ animation_value_type="ComputedValue",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "container-type",
+ "ContainerType",
+ "computed::ContainerType::Normal",
+ engines="gecko",
+ animation_value_type="none",
+ enabled_in="ua",
+ gecko_pref="layout.css.container-queries.enabled",
+ spec="https://drafts.csswg.org/css-contain-3/#container-type",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "container-name",
+ "ContainerName",
+ "computed::ContainerName::none()",
+ engines="gecko",
+ animation_value_type="none",
+ enabled_in="ua",
+ gecko_pref="layout.css.container-queries.enabled",
+ spec="https://drafts.csswg.org/css-contain-3/#container-name",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "appearance",
+ "Appearance",
+ "computed::Appearance::None",
+ engines="gecko",
+ aliases="-moz-appearance -webkit-appearance",
+ spec="https://drafts.csswg.org/css-ui-4/#propdef-appearance",
+ animation_value_type="discrete",
+ gecko_ffi_name="mAppearance",
+ affects="paint",
+)}
+
+// The inherent widget type of an element, selected by specifying
+// `appearance: auto`.
+${helpers.predefined_type(
+ "-moz-default-appearance",
+ "Appearance",
+ "computed::Appearance::None",
+ engines="gecko",
+ animation_value_type="none",
+ spec="Internal (not web-exposed)",
+ enabled_in="chrome",
+ gecko_ffi_name="mDefaultAppearance",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "-moz-orient",
+ "inline block horizontal vertical",
+ engines="gecko",
+ gecko_ffi_name="mOrient",
+ gecko_enum_prefix="StyleOrient",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-orient)",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "will-change",
+ "WillChange",
+ "computed::WillChange::auto()",
+ engines="gecko",
+ animation_value_type="none",
+ spec="https://drafts.csswg.org/css-will-change/#will-change",
+ affects="layout",
+)}
+
+// The spec issue for the parse_method: https://github.com/w3c/csswg-drafts/issues/4102.
+${helpers.predefined_type(
+ "shape-image-threshold",
+ "Opacity",
+ "0.0",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-shapes/#shape-image-threshold-property",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "shape-margin",
+ "NonNegativeLengthPercentage",
+ "computed::NonNegativeLengthPercentage::zero()",
+ engines="gecko",
+ animation_value_type="NonNegativeLengthPercentage",
+ spec="https://drafts.csswg.org/css-shapes/#shape-margin-property",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "shape-outside",
+ "basic_shape::ShapeOutside",
+ "generics::basic_shape::ShapeOutside::None",
+ engines="gecko",
+ animation_value_type="basic_shape::ShapeOutside",
+ spec="https://drafts.csswg.org/css-shapes/#shape-outside-property",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "touch-action",
+ "TouchAction",
+ "computed::TouchAction::auto()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://compat.spec.whatwg.org/#touch-action",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "-webkit-line-clamp",
+ "LineClamp",
+ "computed::LineClamp::none()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-overflow-3/#line-clamp",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "scrollbar-gutter",
+ "ScrollbarGutter",
+ "computed::ScrollbarGutter::AUTO",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-overflow-3/#scrollbar-gutter-property",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "zoom",
+ "Zoom",
+ "computed::box_::Zoom::ONE",
+ engines="gecko",
+ animation_value_type="Number",
+ spec="Non-standard (https://github.com/atanassov/css-zoom/ is the closest)",
+ gecko_pref="layout.css.zoom.enabled",
+ affects="layout",
+ enabled_in="chrome",
+)}
diff --git a/servo/components/style/properties/longhands/column.mako.rs b/servo/components/style/properties/longhands/column.mako.rs
new file mode 100644
index 0000000000..38c32938c6
--- /dev/null
+++ b/servo/components/style/properties/longhands/column.mako.rs
@@ -0,0 +1,90 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("Column", inherited=False) %>
+
+${helpers.predefined_type(
+ "column-width",
+ "length::NonNegativeLengthOrAuto",
+ "computed::length::NonNegativeLengthOrAuto::auto()",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ initial_specified_value="specified::length::NonNegativeLengthOrAuto::auto()",
+ animation_value_type="NonNegativeLengthOrAuto",
+ servo_2013_pref="layout.columns.enabled",
+ spec="https://drafts.csswg.org/css-multicol/#propdef-column-width",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "column-count",
+ "ColumnCount",
+ "computed::ColumnCount::auto()",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ initial_specified_value="specified::ColumnCount::auto()",
+ servo_2013_pref="layout.columns.enabled",
+ animation_value_type="AnimatedColumnCount",
+ spec="https://drafts.csswg.org/css-multicol/#propdef-column-count",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "column-fill",
+ "balance auto",
+ engines="gecko",
+ animation_value_type="discrete",
+ gecko_enum_prefix="StyleColumnFill",
+ spec="https://drafts.csswg.org/css-multicol/#propdef-column-fill",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "column-rule-width",
+ "BorderSideWidth",
+ "app_units::Au::from_px(3)",
+ engines="gecko",
+ initial_specified_value="specified::BorderSideWidth::medium()",
+ spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule-width",
+ animation_value_type="NonNegativeLength",
+ affects="layout",
+)}
+
+// https://drafts.csswg.org/css-multicol-1/#crc
+${helpers.predefined_type(
+ "column-rule-color",
+ "Color",
+ "computed_value::T::currentcolor()",
+ engines="gecko",
+ initial_specified_value="specified::Color::currentcolor()",
+ animation_value_type="AnimatedColor",
+ ignored_when_colors_disabled=True,
+ spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule-color",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "column-span",
+ "none all",
+ engines="gecko",
+ animation_value_type="discrete",
+ gecko_enum_prefix="StyleColumnSpan",
+ spec="https://drafts.csswg.org/css-multicol/#propdef-column-span",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "column-rule-style",
+ "BorderStyle",
+ "computed::BorderStyle::None",
+ engines="gecko",
+ initial_specified_value="specified::BorderStyle::None",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule-style",
+ affects="paint",
+)}
diff --git a/servo/components/style/properties/longhands/counters.mako.rs b/servo/components/style/properties/longhands/counters.mako.rs
new file mode 100644
index 0000000000..6c844c3567
--- /dev/null
+++ b/servo/components/style/properties/longhands/counters.mako.rs
@@ -0,0 +1,52 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("Counters", inherited=False, gecko_name="Content") %>
+
+${helpers.predefined_type(
+ "content",
+ "Content",
+ "computed::Content::normal()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::Content::normal()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-content/#propdef-content",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "counter-increment",
+ "CounterIncrement",
+ engines="gecko servo-2013",
+ initial_value="Default::default()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-lists/#propdef-counter-increment",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "counter-reset",
+ "CounterReset",
+ engines="gecko servo-2013",
+ initial_value="Default::default()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-lists-3/#propdef-counter-reset",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "counter-set",
+ "CounterSet",
+ engines="gecko",
+ initial_value="Default::default()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-lists-3/#propdef-counter-set",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/effects.mako.rs b/servo/components/style/properties/longhands/effects.mako.rs
new file mode 100644
index 0000000000..b301aab5dd
--- /dev/null
+++ b/servo/components/style/properties/longhands/effects.mako.rs
@@ -0,0 +1,92 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+// Box-shadow, etc.
+<% data.new_style_struct("Effects", inherited=False) %>
+
+${helpers.predefined_type(
+ "opacity",
+ "Opacity",
+ "1.0",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="ComputedValue",
+ flags="CAN_ANIMATE_ON_COMPOSITOR",
+ spec="https://drafts.csswg.org/css-color/#transparency",
+ servo_restyle_damage = "reflow_out_of_flow",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "box-shadow",
+ "BoxShadow",
+ None,
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ vector=True,
+ simple_vector_bindings=True,
+ animation_value_type="AnimatedBoxShadowList",
+ vector_animation_type="with_zero",
+ extra_prefixes="webkit",
+ ignored_when_colors_disabled=True,
+ spec="https://drafts.csswg.org/css-backgrounds/#box-shadow",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "clip",
+ "ClipRectOrAuto",
+ "computed::ClipRectOrAuto::auto()",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="ComputedValue",
+ boxed=True,
+ allow_quirks="Yes",
+ spec="https://drafts.fxtf.org/css-masking/#clip-property",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "filter",
+ "Filter",
+ None,
+ engines="gecko servo-2013 servo-2020",
+ vector=True,
+ simple_vector_bindings=True,
+ gecko_ffi_name="mFilters",
+ separator="Space",
+ animation_value_type="AnimatedFilterList",
+ vector_animation_type="with_zero",
+ extra_prefixes="webkit",
+ spec="https://drafts.fxtf.org/filters/#propdef-filter",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "backdrop-filter",
+ "Filter",
+ None,
+ engines="gecko",
+ vector=True,
+ simple_vector_bindings=True,
+ gecko_ffi_name="mBackdropFilters",
+ separator="Space",
+ animation_value_type="AnimatedFilterList",
+ vector_animation_type="with_zero",
+ gecko_pref="layout.css.backdrop-filter.enabled",
+ spec="https://drafts.fxtf.org/filter-effects-2/#propdef-backdrop-filter",
+ affects="overflow",
+)}
+
+${helpers.single_keyword(
+ "mix-blend-mode",
+ """normal multiply screen overlay darken lighten color-dodge
+ color-burn hard-light soft-light difference exclusion hue
+ saturation color luminosity plus-lighter""",
+ engines="gecko servo-2013 servo-2020",
+ gecko_enum_prefix="StyleBlend",
+ animation_value_type="discrete",
+ spec="https://drafts.fxtf.org/compositing/#propdef-mix-blend-mode",
+ affects="paint",
+)}
diff --git a/servo/components/style/properties/longhands/font.mako.rs b/servo/components/style/properties/longhands/font.mako.rs
new file mode 100644
index 0000000000..f188af5b1f
--- /dev/null
+++ b/servo/components/style/properties/longhands/font.mako.rs
@@ -0,0 +1,505 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import Method, to_camel_case, to_rust_ident, to_camel_case_lower, SYSTEM_FONT_LONGHANDS %>
+
+<% data.new_style_struct("Font", inherited=True) %>
+
+${helpers.predefined_type(
+ "font-family",
+ "FontFamily",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::FontFamily::serif()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-family",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-style",
+ "FontStyle",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::FontStyle::normal()",
+ initial_specified_value="specified::FontStyle::normal()",
+ animation_value_type="FontStyle",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-style",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+<% font_variant_caps_custom_consts= { "small-caps": "SMALLCAPS",
+ "all-small-caps": "ALLSMALL",
+ "petite-caps": "PETITECAPS",
+ "all-petite-caps": "ALLPETITE",
+ "titling-caps": "TITLING" } %>
+
+${helpers.single_keyword(
+ "font-variant-caps",
+ "normal small-caps",
+ engines="gecko servo-2013 servo-2020",
+ extra_gecko_values="all-small-caps petite-caps all-petite-caps unicase titling-caps",
+ gecko_constant_prefix="NS_FONT_VARIANT_CAPS",
+ gecko_ffi_name="mFont.variantCaps",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-caps",
+ custom_consts=font_variant_caps_custom_consts,
+ animation_value_type="discrete",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-weight",
+ "FontWeight",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::FontWeight::normal()",
+ initial_specified_value="specified::FontWeight::normal()",
+ animation_value_type="Number",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-weight",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-size",
+ "FontSize",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::FontSize::medium()",
+ initial_specified_value="specified::FontSize::medium()",
+ animation_value_type="NonNegativeLength",
+ allow_quirks="Yes",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-size",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-size-adjust",
+ "FontSizeAdjust",
+ engines="gecko",
+ initial_value="computed::FontSizeAdjust::None",
+ initial_specified_value="specified::FontSizeAdjust::None",
+ animation_value_type="FontSizeAdjust",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-size-adjust",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-synthesis-weight",
+ "FontSynthesis",
+ engines="gecko",
+ initial_value="computed::FontSynthesis::Auto",
+ initial_specified_value="specified::FontSynthesis::Auto",
+ gecko_ffi_name="mFont.synthesisWeight",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts-4/#font-synthesis-weight",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-synthesis-style",
+ "FontSynthesis",
+ engines="gecko",
+ initial_value="computed::FontSynthesis::Auto",
+ initial_specified_value="specified::FontSynthesis::Auto",
+ gecko_ffi_name="mFont.synthesisStyle",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts-4/#font-synthesis-style",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-synthesis-small-caps",
+ "FontSynthesis",
+ engines="gecko",
+ initial_value="computed::FontSynthesis::Auto",
+ initial_specified_value="specified::FontSynthesis::Auto",
+ gecko_ffi_name="mFont.synthesisSmallCaps",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts-4/#font-synthesis-small-caps",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-synthesis-position",
+ "FontSynthesis",
+ engines="gecko",
+ initial_value="computed::FontSynthesis::Auto",
+ initial_specified_value="specified::FontSynthesis::Auto",
+ gecko_ffi_name="mFont.synthesisPosition",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts-4/#font-synthesis-position",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-stretch",
+ "FontStretch",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::FontStretch::hundred()",
+ initial_specified_value="specified::FontStretch::normal()",
+ animation_value_type="Percentage",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-stretch",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "font-kerning",
+ "auto none normal",
+ engines="gecko",
+ gecko_ffi_name="mFont.kerning",
+ gecko_constant_prefix="NS_FONT_KERNING",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-kerning",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-variant-alternates",
+ "FontVariantAlternates",
+ engines="gecko",
+ initial_value="computed::FontVariantAlternates::default()",
+ initial_specified_value="specified::FontVariantAlternates::default()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-alternates",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-variant-east-asian",
+ "FontVariantEastAsian",
+ engines="gecko",
+ initial_value="computed::FontVariantEastAsian::empty()",
+ initial_specified_value="specified::FontVariantEastAsian::empty()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-east-asian",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "font-variant-emoji",
+ "normal text emoji unicode",
+ engines="gecko",
+ gecko_pref="layout.css.font-variant-emoji.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ gecko_enum_prefix="StyleFontVariantEmoji",
+ gecko_ffi_name="mFont.variantEmoji",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-emoji",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-variant-ligatures",
+ "FontVariantLigatures",
+ engines="gecko",
+ initial_value="computed::FontVariantLigatures::empty()",
+ initial_specified_value="specified::FontVariantLigatures::empty()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-ligatures",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-variant-numeric",
+ "FontVariantNumeric",
+ engines="gecko",
+ initial_value="computed::FontVariantNumeric::empty()",
+ initial_specified_value="specified::FontVariantNumeric::empty()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-numeric",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "font-variant-position",
+ "normal sub super",
+ engines="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_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-feature-settings",
+ "FontFeatureSettings",
+ engines="gecko",
+ initial_value="computed::FontFeatureSettings::normal()",
+ initial_specified_value="specified::FontFeatureSettings::normal()",
+ extra_prefixes="moz:layout.css.prefixes.font-features",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-fonts/#propdef-font-feature-settings",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-variation-settings",
+ "FontVariationSettings",
+ engines="gecko",
+ gecko_pref="layout.css.font-variations.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ initial_value="computed::FontVariationSettings::normal()",
+ initial_specified_value="specified::FontVariationSettings::normal()",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-fonts-4/#propdef-font-variation-settings",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-language-override",
+ "FontLanguageOverride",
+ engines="gecko",
+ initial_value="computed::FontLanguageOverride::normal()",
+ initial_specified_value="specified::FontLanguageOverride::normal()",
+ animation_value_type="discrete",
+ extra_prefixes="moz:layout.css.prefixes.font-features",
+ spec="https://drafts.csswg.org/css-fonts-3/#propdef-font-language-override",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "font-optical-sizing",
+ "auto none",
+ engines="gecko",
+ gecko_pref="layout.css.font-variations.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ gecko_ffi_name="mFont.opticalSizing",
+ gecko_constant_prefix="NS_FONT_OPTICAL_SIZING",
+ animation_value_type="discrete",
+ spec="https://www.w3.org/TR/css-fonts-4/#font-optical-sizing-def",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "font-palette",
+ "FontPalette",
+ engines="gecko",
+ initial_value="computed::FontPalette::normal()",
+ initial_specified_value="specified::FontPalette::normal()",
+ animation_value_type="discrete",
+ gecko_pref="layout.css.font-palette.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ spec="https://drafts.csswg.org/css-fonts/#font-palette-prop",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "-x-lang",
+ "XLang",
+ engines="gecko",
+ initial_value="computed::XLang::get_initial_value()",
+ animation_value_type="none",
+ enabled_in="",
+ has_effect_on_gecko_scrollbars=False,
+ spec="Internal (not web-exposed)",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "math-depth",
+ "MathDepth",
+ "0",
+ engines="gecko",
+ gecko_pref="layout.css.math-depth.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ animation_value_type="none",
+ enabled_in="ua",
+ spec="https://mathml-refresh.github.io/mathml-core/#the-math-script-level-property",
+ affects="",
+)}
+
+${helpers.single_keyword(
+ "math-style",
+ "normal compact",
+ engines="gecko",
+ gecko_enum_prefix="StyleMathStyle",
+ gecko_pref="layout.css.math-style.enabled",
+ spec="https://mathml-refresh.github.io/mathml-core/#the-math-style-property",
+ has_effect_on_gecko_scrollbars=False,
+ animation_value_type="none",
+ enabled_in="ua",
+ needs_conversion=True,
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "-moz-math-variant",
+ """none normal bold italic bold-italic script bold-script
+ fraktur double-struck bold-fraktur sans-serif
+ bold-sans-serif sans-serif-italic sans-serif-bold-italic
+ monospace initial tailed looped stretched""",
+ engines="gecko",
+ gecko_enum_prefix="StyleMathVariant",
+ gecko_ffi_name="mMathVariant",
+ spec="Internal (not web-exposed)",
+ animation_value_type="none",
+ enabled_in="",
+ has_effect_on_gecko_scrollbars=False,
+ needs_conversion=True,
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "-x-text-scale",
+ "XTextScale",
+ "computed::XTextScale::All",
+ engines="gecko",
+ animation_value_type="none",
+ enabled_in="",
+ has_effect_on_gecko_scrollbars=False,
+ spec="Internal (not web-exposed)",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "line-height",
+ "LineHeight",
+ "computed::LineHeight::normal()",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="LineHeight",
+ spec="https://drafts.csswg.org/css2/visudet.html#propdef-line-height",
+ servo_restyle_damage="reflow",
+ affects="layout",
+)}
+
+% if engine == "gecko":
+pub mod system_font {
+ //! We deal with system fonts here
+ //!
+ //! System fonts can only be set as a group via the font shorthand.
+ //! They resolve at compute time (not parse time -- this lets the
+ //! browser respond to changes to the OS font settings).
+ //!
+ //! While Gecko handles these as a separate property and keyword
+ //! values on each property indicating that the font should be picked
+ //! from the -x-system-font property, we avoid this. Instead,
+ //! each font longhand has a special SystemFont variant which contains
+ //! the specified system font. When the cascade function (in helpers)
+ //! detects that a value has a system font, it will resolve it, and
+ //! cache it on the ComputedValues. After this, it can be just fetched
+ //! whenever a font longhand on the same element needs the system font.
+ //!
+ //! When a longhand property is holding a SystemFont, it's serialized
+ //! to an empty string as if its value comes from a shorthand with
+ //! variable reference. We may want to improve this behavior at some
+ //! point. See also https://github.com/w3c/csswg-drafts/issues/1586.
+
+ use crate::properties::longhands;
+ use std::hash::{Hash, Hasher};
+ use crate::values::computed::{ToComputedValue, Context};
+ use crate::values::specified::font::SystemFont;
+ // ComputedValues are compared at times
+ // so we need these impls. We don't want to
+ // add Eq to Number (which contains a float)
+ // so instead we have an eq impl which skips the
+ // cached values
+ impl PartialEq for ComputedSystemFont {
+ fn eq(&self, other: &Self) -> bool {
+ self.system_font == other.system_font
+ }
+ }
+ impl Eq for ComputedSystemFont {}
+
+ impl Hash for ComputedSystemFont {
+ fn hash<H: Hasher>(&self, hasher: &mut H) {
+ self.system_font.hash(hasher)
+ }
+ }
+
+ impl ToComputedValue for SystemFont {
+ type ComputedValue = ComputedSystemFont;
+
+ fn to_computed_value(&self, cx: &Context) -> Self::ComputedValue {
+ use crate::gecko_bindings::bindings;
+ use crate::gecko_bindings::structs::nsFont;
+ use crate::values::computed::font::FontSize;
+ use crate::values::specified::font::KeywordInfo;
+ use crate::values::generics::NonNegative;
+ use std::mem;
+
+ let mut system = mem::MaybeUninit::<nsFont>::uninit();
+ let system = unsafe {
+ bindings::Gecko_nsFont_InitSystem(
+ system.as_mut_ptr(),
+ *self,
+ &**cx.style().get_font(),
+ cx.device().document()
+ );
+ &mut *system.as_mut_ptr()
+ };
+ let size = NonNegative(cx.maybe_zoom_text(system.size.0));
+ let ret = ComputedSystemFont {
+ font_family: system.family.clone(),
+ font_size: FontSize {
+ computed_size: size,
+ used_size: size,
+ keyword_info: KeywordInfo::none()
+ },
+ font_weight: system.weight,
+ font_stretch: system.stretch,
+ font_style: system.style,
+ system_font: *self,
+ };
+ unsafe { bindings::Gecko_nsFont_Destroy(system); }
+ ret
+ }
+
+ fn from_computed_value(_: &ComputedSystemFont) -> Self {
+ unreachable!()
+ }
+ }
+
+ #[inline]
+ /// Compute and cache a system font
+ ///
+ /// Must be called before attempting to compute a system font
+ /// specified value
+ pub fn resolve_system_font(system: SystemFont, context: &mut Context) {
+ // Checking if context.cached_system_font.is_none() isn't enough,
+ // if animating from one system font to another the cached system font
+ // may change
+ if Some(system) != context.cached_system_font.as_ref().map(|x| x.system_font) {
+ let computed = system.to_computed_value(context);
+ context.cached_system_font = Some(computed);
+ }
+ }
+
+ #[derive(Clone, Debug)]
+ pub struct ComputedSystemFont {
+ % for name in SYSTEM_FONT_LONGHANDS:
+ pub ${name}: longhands::${name}::computed_value::T,
+ % endfor
+ pub system_font: SystemFont,
+ }
+
+}
+% endif
+
+${helpers.single_keyword(
+ "-moz-osx-font-smoothing",
+ "auto grayscale",
+ engines="gecko",
+ gecko_constant_prefix="NS_FONT_SMOOTHING",
+ gecko_ffi_name="mFont.smoothing",
+ gecko_pref="layout.css.osx-font-smoothing.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/font-smooth)",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "-moz-min-font-size-ratio",
+ "Percentage",
+ "computed::Percentage::hundred()",
+ engines="gecko",
+ animation_value_type="none",
+ enabled_in="ua",
+ spec="Nonstandard (Internal-only)",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/inherited_box.mako.rs b/servo/components/style/properties/longhands/inherited_box.mako.rs
new file mode 100644
index 0000000000..7fd94d1a1f
--- /dev/null
+++ b/servo/components/style/properties/longhands/inherited_box.mako.rs
@@ -0,0 +1,105 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("InheritedBox", inherited=True, gecko_name="Visibility") %>
+
+// TODO: collapse. Well, do tables first.
+${helpers.single_keyword(
+ "visibility",
+ "visible hidden collapse",
+ engines="gecko servo-2013 servo-2020",
+ gecko_ffi_name="mVisible",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-box/#propdef-visibility",
+ gecko_enum_prefix="StyleVisibility",
+ affects="paint",
+)}
+
+// CSS Writing Modes Level 3
+// https://drafts.csswg.org/css-writing-modes-3
+${helpers.single_keyword(
+ "writing-mode",
+ "horizontal-tb vertical-rl vertical-lr",
+ engines="gecko servo-2013 servo-2020",
+ extra_gecko_values="sideways-rl sideways-lr",
+ gecko_aliases="lr=horizontal-tb lr-tb=horizontal-tb \
+ rl=horizontal-tb rl-tb=horizontal-tb \
+ tb=vertical-rl tb-rl=vertical-rl",
+ servo_2013_pref="layout.writing-mode.enabled",
+ servo_2020_pref="layout.writing-mode.enabled",
+ animation_value_type="none",
+ spec="https://drafts.csswg.org/css-writing-modes/#propdef-writing-mode",
+ gecko_enum_prefix="StyleWritingModeProperty",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "direction",
+ "ltr rtl",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ animation_value_type="none",
+ spec="https://drafts.csswg.org/css-writing-modes/#propdef-direction",
+ gecko_enum_prefix="StyleDirection",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "-moz-box-collapse",
+ "flex legacy",
+ engines="gecko",
+ gecko_enum_prefix="StyleMozBoxCollapse",
+ animation_value_type="none",
+ enabled_in="chrome",
+ spec="None (internal)",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "text-orientation",
+ "mixed upright sideways",
+ engines="gecko",
+ gecko_aliases="sideways-right=sideways",
+ gecko_enum_prefix="StyleTextOrientation",
+ animation_value_type="none",
+ spec="https://drafts.csswg.org/css-writing-modes/#propdef-text-orientation",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "print-color-adjust",
+ "PrintColorAdjust",
+ "computed::PrintColorAdjust::Economy",
+ engines="gecko",
+ aliases="color-adjust",
+ spec="https://drafts.csswg.org/css-color-adjust/#print-color-adjust",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+// According to to CSS-IMAGES-3, `optimizespeed` and `optimizequality` are synonyms for `auto`
+// And, firefox doesn't support `pixelated` yet (https://bugzilla.mozilla.org/show_bug.cgi?id=856337)
+${helpers.predefined_type(
+ "image-rendering",
+ "ImageRendering",
+ "computed::ImageRendering::Auto",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-images/#propdef-image-rendering",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "image-orientation",
+ "from-image none",
+ engines="gecko",
+ gecko_enum_prefix="StyleImageOrientation",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-images/#propdef-image-orientation",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/inherited_svg.mako.rs b/servo/components/style/properties/longhands/inherited_svg.mako.rs
new file mode 100644
index 0000000000..90443f962a
--- /dev/null
+++ b/servo/components/style/properties/longhands/inherited_svg.mako.rs
@@ -0,0 +1,239 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+// SVG 2
+// https://svgwg.org/svg2-draft/
+<% data.new_style_struct("InheritedSVG", inherited=True, gecko_name="SVG") %>
+
+// Section 10 - Text
+
+${helpers.single_keyword(
+ "dominant-baseline",
+ """auto ideographic alphabetic hanging mathematical central middle
+ text-after-edge text-before-edge""",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://www.w3.org/TR/css-inline-3/#propdef-dominant-baseline",
+ gecko_enum_prefix="StyleDominantBaseline",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "text-anchor",
+ "start middle end",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/text.html#TextAnchorProperty",
+ gecko_enum_prefix="StyleTextAnchor",
+ affects="layout",
+)}
+
+// Section 11 - Painting: Filling, Stroking and Marker Symbols
+${helpers.single_keyword(
+ "color-interpolation",
+ "srgb auto linearrgb",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty",
+ gecko_enum_prefix="StyleColorInterpolation",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "color-interpolation-filters",
+ "linearrgb auto srgb",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#ColorInterpolationFiltersProperty",
+ gecko_enum_prefix="StyleColorInterpolation",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "fill",
+ "SVGPaint",
+ "crate::values::computed::SVGPaint::BLACK",
+ engines="gecko",
+ animation_value_type="IntermediateSVGPaint",
+ boxed=True,
+ spec="https://svgwg.org/svg2-draft/painting.html#SpecifyingFillPaint",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "fill-opacity",
+ "SVGOpacity",
+ "Default::default()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/painting.html#FillOpacity",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "fill-rule",
+ "FillRule",
+ "Default::default()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#FillRuleProperty",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "shape-rendering",
+ "auto optimizespeed crispedges geometricprecision",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#ShapeRenderingProperty",
+ gecko_enum_prefix = "StyleShapeRendering",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "stroke",
+ "SVGPaint",
+ "Default::default()",
+ engines="gecko",
+ animation_value_type="IntermediateSVGPaint",
+ boxed=True,
+ spec="https://svgwg.org/svg2-draft/painting.html#SpecifyingStrokePaint",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "stroke-width",
+ "SVGWidth",
+ "computed::SVGWidth::one()",
+ engines="gecko",
+ animation_value_type="crate::values::computed::SVGWidth",
+ spec="https://svgwg.org/svg2-draft/painting.html#StrokeWidth",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "stroke-linecap",
+ "butt round square",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#StrokeLinecapProperty",
+ gecko_enum_prefix = "StyleStrokeLinecap",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "stroke-linejoin",
+ "miter round bevel",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#StrokeLinejoinProperty",
+ gecko_enum_prefix = "StyleStrokeLinejoin",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "stroke-miterlimit",
+ "NonNegativeNumber",
+ "From::from(4.0)",
+ engines="gecko",
+ animation_value_type="crate::values::computed::NonNegativeNumber",
+ spec="https://svgwg.org/svg2-draft/painting.html#StrokeMiterlimitProperty",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "stroke-opacity",
+ "SVGOpacity",
+ "Default::default()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/painting.html#StrokeOpacity",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "stroke-dasharray",
+ "SVGStrokeDashArray",
+ "Default::default()",
+ engines="gecko",
+ animation_value_type="crate::values::computed::SVGStrokeDashArray",
+ spec="https://svgwg.org/svg2-draft/painting.html#StrokeDashing",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "stroke-dashoffset",
+ "SVGLength",
+ "computed::SVGLength::zero()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/painting.html#StrokeDashing",
+ affects="paint",
+)}
+
+// Section 14 - Clipping, Masking and Compositing
+${helpers.predefined_type(
+ "clip-rule",
+ "FillRule",
+ "Default::default()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/masking.html#ClipRuleProperty",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "marker-start",
+ "url::UrlOrNone",
+ "computed::url::UrlOrNone::none()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "marker-mid",
+ "url::UrlOrNone",
+ "computed::url::UrlOrNone::none()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "marker-end",
+ "url::UrlOrNone",
+ "computed::url::UrlOrNone::none()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "paint-order",
+ "SVGPaintOrder",
+ "computed::SVGPaintOrder::normal()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#PaintOrder",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "-moz-context-properties",
+ "MozContextProperties",
+ "computed::MozContextProperties::default()",
+ engines="gecko",
+ enabled_in="chrome",
+ gecko_pref="svg.context-properties.content.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ animation_value_type="none",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-context-properties)",
+ affects="paint",
+)}
diff --git a/servo/components/style/properties/longhands/inherited_table.mako.rs b/servo/components/style/properties/longhands/inherited_table.mako.rs
new file mode 100644
index 0000000000..7eb42a6eb2
--- /dev/null
+++ b/servo/components/style/properties/longhands/inherited_table.mako.rs
@@ -0,0 +1,53 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("InheritedTable", inherited=True, gecko_name="TableBorder") %>
+
+${helpers.single_keyword(
+ "border-collapse",
+ "separate collapse",
+ engines="gecko servo-2013",
+ gecko_enum_prefix="StyleBorderCollapse",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-tables/#propdef-border-collapse",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "empty-cells",
+ "show hide",
+ engines="gecko servo-2013",
+ gecko_enum_prefix="StyleEmptyCells",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-tables/#propdef-empty-cells",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "caption-side",
+ "table::CaptionSide",
+ "computed::table::CaptionSide::Top",
+ engines="gecko servo-2013",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-tables/#propdef-caption-side",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "border-spacing",
+ "BorderSpacing",
+ "computed::BorderSpacing::zero()",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ animation_value_type="BorderSpacing",
+ boxed=True,
+ spec="https://drafts.csswg.org/css-tables/#propdef-border-spacing",
+ servo_restyle_damage="reflow",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/inherited_text.mako.rs b/servo/components/style/properties/longhands/inherited_text.mako.rs
new file mode 100644
index 0000000000..544ba99bf7
--- /dev/null
+++ b/servo/components/style/properties/longhands/inherited_text.mako.rs
@@ -0,0 +1,414 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import Keyword %>
+<% data.new_style_struct("InheritedText", inherited=True, gecko_name="Text") %>
+
+${helpers.predefined_type(
+ "color",
+ "ColorPropertyValue",
+ "crate::color::AbsoluteColor::BLACK",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="AbsoluteColor",
+ ignored_when_colors_disabled="True",
+ spec="https://drafts.csswg.org/css-color/#color",
+ affects="paint",
+)}
+
+// CSS Text Module Level 3
+
+${helpers.predefined_type(
+ "text-transform",
+ "TextTransform",
+ "computed::TextTransform::none()",
+ engines="gecko servo-2013",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text/#propdef-text-transform",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "hyphens",
+ "manual none auto",
+ engines="gecko",
+ gecko_enum_prefix="StyleHyphens",
+ animation_value_type="discrete",
+ extra_prefixes="moz",
+ spec="https://drafts.csswg.org/css-text/#propdef-hyphens",
+ affects="layout",
+)}
+
+// TODO: Support <percentage>
+${helpers.single_keyword(
+ "-moz-text-size-adjust",
+ "auto none",
+ engines="gecko",
+ gecko_enum_prefix="StyleTextSizeAdjust",
+ gecko_ffi_name="mTextSizeAdjust",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-size-adjust/#adjustment-control",
+ aliases="-webkit-text-size-adjust",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "text-indent",
+ "TextIndent",
+ "computed::TextIndent::zero()",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-text/#propdef-text-indent",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+)}
+
+// Also known as "word-wrap" (which is more popular because of IE), but this is
+// the preferred name per CSS-TEXT 6.2.
+${helpers.predefined_type(
+ "overflow-wrap",
+ "OverflowWrap",
+ "computed::OverflowWrap::Normal",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text/#propdef-overflow-wrap",
+ aliases="word-wrap",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "word-break",
+ "WordBreak",
+ "computed::WordBreak::Normal",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text/#propdef-word-break",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "text-justify",
+ "TextJustify",
+ "computed::TextJustify::Auto",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text/#propdef-text-justify",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "text-align-last",
+ "TextAlignLast",
+ "computed::text::TextAlignLast::Auto",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text/#propdef-text-align-last",
+ affects="layout",
+)}
+
+// TODO make this a shorthand and implement text-align-last/text-align-all
+${helpers.predefined_type(
+ "text-align",
+ "TextAlign",
+ "computed::TextAlign::Start",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text/#propdef-text-align",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "letter-spacing",
+ "LetterSpacing",
+ "computed::LetterSpacing::normal()",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-text/#propdef-letter-spacing",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "word-spacing",
+ "WordSpacing",
+ "computed::WordSpacing::zero()",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-text/#propdef-word-spacing",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+// TODO: `white-space-collapse: discard` not yet supported
+${helpers.single_keyword(
+ name="white-space-collapse",
+ values="collapse preserve preserve-breaks preserve-spaces break-spaces",
+ engines="gecko",
+ gecko_enum_prefix="StyleWhiteSpaceCollapse",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-4/#propdef-white-space-collapse",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "text-shadow",
+ "SimpleShadow",
+ None,
+ engines="gecko servo-2013",
+ vector=True,
+ vector_animation_type="with_zero",
+ animation_value_type="AnimatedTextShadowList",
+ ignored_when_colors_disabled=True,
+ simple_vector_bindings=True,
+ spec="https://drafts.csswg.org/css-text-decor-3/#text-shadow-property",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "text-emphasis-style",
+ "TextEmphasisStyle",
+ "computed::TextEmphasisStyle::None",
+ engines="gecko",
+ initial_specified_value="SpecifiedValue::None",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "text-emphasis-position",
+ "TextEmphasisPosition",
+ "computed::TextEmphasisPosition::OVER",
+ engines="gecko",
+ initial_specified_value="specified::TextEmphasisPosition::OVER",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-position",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "text-emphasis-color",
+ "Color",
+ "computed_value::T::currentcolor()",
+ engines="gecko",
+ initial_specified_value="specified::Color::currentcolor()",
+ animation_value_type="AnimatedColor",
+ ignored_when_colors_disabled=True,
+ spec="https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-color",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "tab-size",
+ "NonNegativeLengthOrNumber",
+ "generics::length::LengthOrNumber::Number(From::from(8.0))",
+ engines="gecko",
+ animation_value_type="LengthOrNumber",
+ spec="https://drafts.csswg.org/css-text-3/#tab-size-property",
+ aliases="-moz-tab-size",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "line-break",
+ "LineBreak",
+ "computed::LineBreak::Auto",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-3/#line-break-property",
+ affects="layout",
+)}
+
+// CSS Compatibility
+// https://compat.spec.whatwg.org
+${helpers.predefined_type(
+ "-webkit-text-fill-color",
+ "Color",
+ "computed_value::T::currentcolor()",
+ engines="gecko",
+ animation_value_type="AnimatedColor",
+ ignored_when_colors_disabled=True,
+ spec="https://compat.spec.whatwg.org/#the-webkit-text-fill-color",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "-webkit-text-stroke-color",
+ "Color",
+ "computed_value::T::currentcolor()",
+ initial_specified_value="specified::Color::currentcolor()",
+ engines="gecko",
+ animation_value_type="AnimatedColor",
+ ignored_when_colors_disabled=True,
+ spec="https://compat.spec.whatwg.org/#the-webkit-text-stroke-color",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "-webkit-text-stroke-width",
+ "LineWidth",
+ "app_units::Au(0)",
+ engines="gecko",
+ initial_specified_value="specified::LineWidth::zero()",
+ spec="https://compat.spec.whatwg.org/#the-webkit-text-stroke-width",
+ animation_value_type="discrete",
+ affects="overflow",
+)}
+
+// CSS Ruby Layout Module Level 1
+// https://drafts.csswg.org/css-ruby/
+${helpers.single_keyword(
+ "ruby-align",
+ "space-around start center space-between",
+ engines="gecko",
+ animation_value_type="discrete",
+ gecko_enum_prefix="StyleRubyAlign",
+ spec="https://drafts.csswg.org/css-ruby/#ruby-align-property",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "ruby-position",
+ "RubyPosition",
+ "computed::RubyPosition::AlternateOver",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-ruby/#ruby-position-property",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+// CSS Writing Modes Module Level 3
+// https://drafts.csswg.org/css-writing-modes-3/
+
+${helpers.single_keyword(
+ "text-combine-upright",
+ "none all",
+ engines="gecko",
+ gecko_enum_prefix="StyleTextCombineUpright",
+ animation_value_type="none",
+ spec="https://drafts.csswg.org/css-writing-modes-3/#text-combine-upright",
+ affects="layout",
+)}
+
+// SVG 2: Section 13 - Painting: Filling, Stroking and Marker Symbols
+${helpers.single_keyword(
+ "text-rendering",
+ "auto optimizespeed optimizelegibility geometricprecision",
+ engines="gecko servo-2013 servo-2020",
+ gecko_enum_prefix="StyleTextRendering",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/painting.html#TextRenderingProperty",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "-moz-control-character-visibility",
+ "text::MozControlCharacterVisibility",
+ "Default::default()",
+ engines="gecko",
+ enabled_in="chrome",
+ gecko_pref="layout.css.moz-control-character-visibility.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ animation_value_type="none",
+ spec="Nonstandard",
+ affects="layout",
+)}
+
+// text underline offset
+${helpers.predefined_type(
+ "text-underline-offset",
+ "LengthPercentageOrAuto",
+ "computed::LengthPercentageOrAuto::auto()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-text-decor-4/#underline-offset",
+ affects="overflow",
+)}
+
+// text underline position
+${helpers.predefined_type(
+ "text-underline-position",
+ "TextUnderlinePosition",
+ "computed::TextUnderlinePosition::AUTO",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-decor-3/#text-underline-position-property",
+ affects="overflow",
+)}
+
+// text decoration skip ink
+${helpers.predefined_type(
+ "text-decoration-skip-ink",
+ "TextDecorationSkipInk",
+ "computed::TextDecorationSkipInk::Auto",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property",
+ affects="overflow",
+)}
+
+// hyphenation character
+${helpers.predefined_type(
+ "hyphenate-character",
+ "HyphenateCharacter",
+ "computed::HyphenateCharacter::Auto",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://www.w3.org/TR/css-text-4/#hyphenate-character",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "forced-color-adjust",
+ "ForcedColorAdjust",
+ "computed::ForcedColorAdjust::Auto",
+ engines="gecko",
+ gecko_pref="layout.css.forced-color-adjust.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-color-adjust-1/#forced-color-adjust-prop",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "-webkit-text-security",
+ "none circle disc square",
+ engines="gecko",
+ gecko_enum_prefix="StyleTextSecurity",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text/#MISSING",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "text-wrap-mode",
+ "wrap nowrap",
+ engines="gecko",
+ gecko_enum_prefix="StyleTextWrapMode",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-4/#propdef-text-wrap-mode",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "text-wrap-style",
+ "auto stable balance",
+ engines="gecko",
+ gecko_pref="layout.css.text-wrap-balance.enabled",
+ has_effect_on_gecko_scrollbars=False,
+ gecko_enum_prefix="StyleTextWrapStyle",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-4/#text-wrap-style",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/inherited_ui.mako.rs b/servo/components/style/properties/longhands/inherited_ui.mako.rs
new file mode 100644
index 0000000000..6cdf721336
--- /dev/null
+++ b/servo/components/style/properties/longhands/inherited_ui.mako.rs
@@ -0,0 +1,135 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("InheritedUI", inherited=True, gecko_name="UI") %>
+
+${helpers.predefined_type(
+ "cursor",
+ "Cursor",
+ "computed::Cursor::auto()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::Cursor::auto()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-ui/#cursor",
+ affects="paint",
+)}
+
+// NB: `pointer-events: auto` (and use of `pointer-events` in anything that isn't SVG, in fact)
+// is nonstandard, slated for CSS4-UI.
+// TODO(pcwalton): SVG-only values.
+${helpers.single_keyword(
+ "pointer-events",
+ "auto none",
+ engines="gecko servo-2013 servo-2020",
+ animation_value_type="discrete",
+ extra_gecko_values="visiblepainted visiblefill visiblestroke visible painted fill stroke all",
+ spec="https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty",
+ gecko_enum_prefix="StylePointerEvents",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "-moz-inert",
+ "none inert",
+ engines="gecko",
+ gecko_ffi_name="mInert",
+ gecko_enum_prefix="StyleInert",
+ animation_value_type="discrete",
+ enabled_in="ua",
+ spec="Nonstandard (https://html.spec.whatwg.org/multipage/#inert-subtrees)",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "-moz-user-input",
+ "auto none",
+ engines="gecko",
+ gecko_ffi_name="mUserInput",
+ gecko_enum_prefix="StyleUserInput",
+ animation_value_type="discrete",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-user-input)",
+ affects="",
+)}
+
+${helpers.single_keyword(
+ "-moz-user-modify",
+ "read-only read-write write-only",
+ engines="gecko",
+ gecko_ffi_name="mUserModify",
+ gecko_enum_prefix="StyleUserModify",
+ needs_conversion=True,
+ animation_value_type="discrete",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-user-modify)",
+ affects="",
+)}
+
+${helpers.single_keyword(
+ "-moz-user-focus",
+ "normal none ignore",
+ engines="gecko",
+ gecko_ffi_name="mUserFocus",
+ gecko_enum_prefix="StyleUserFocus",
+ animation_value_type="discrete",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-user-focus)",
+ enabled_in="chrome",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "caret-color",
+ "color::CaretColor",
+ "generics::color::CaretColor::auto()",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-ui/#caret-color",
+ animation_value_type="CaretColor",
+ ignored_when_colors_disabled=True,
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "accent-color",
+ "ColorOrAuto",
+ "generics::color::ColorOrAuto::Auto",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-ui-4/#widget-accent",
+ animation_value_type="ColorOrAuto",
+ ignored_when_colors_disabled=True,
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "color-scheme",
+ "ColorScheme",
+ "specified::color::ColorScheme::normal()",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-color-adjust/#color-scheme-prop",
+ animation_value_type="discrete",
+ ignored_when_colors_disabled=True,
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "scrollbar-color",
+ "ui::ScrollbarColor",
+ "Default::default()",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color",
+ animation_value_type="ScrollbarColor",
+ boxed=True,
+ ignored_when_colors_disabled=True,
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "-moz-theme",
+ "ui::MozTheme",
+ "specified::ui::MozTheme::Auto",
+ engines="gecko",
+ enabled_in="chrome",
+ animation_value_type="discrete",
+ spec="Internal",
+ affects="paint",
+)}
diff --git a/servo/components/style/properties/longhands/list.mako.rs b/servo/components/style/properties/longhands/list.mako.rs
new file mode 100644
index 0000000000..619724bd32
--- /dev/null
+++ b/servo/components/style/properties/longhands/list.mako.rs
@@ -0,0 +1,80 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("List", inherited=True) %>
+
+${helpers.single_keyword(
+ "list-style-position",
+ "outside inside",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ gecko_enum_prefix="StyleListStylePosition",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-lists/#propdef-list-style-position",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+// TODO(pcwalton): Implement the full set of counter styles per CSS-COUNTER-STYLES [1] 6.1:
+//
+// decimal-leading-zero, armenian, upper-armenian, lower-armenian, georgian, lower-roman,
+// upper-roman
+//
+// [1]: http://dev.w3.org/csswg/css-counter-styles/
+% if engine in ["servo-2013", "servo-2020"]:
+ ${helpers.single_keyword(
+ "list-style-type",
+ "disc none circle square disclosure-open disclosure-closed",
+ extra_servo_2013_values="""
+ decimal lower-alpha upper-alpha arabic-indic bengali cambodian cjk-decimal devanagari
+ gujarati gurmukhi kannada khmer lao malayalam mongolian myanmar oriya persian telugu
+ thai tibetan cjk-earthly-branch cjk-heavenly-stem lower-greek hiragana hiragana-iroha
+ katakana katakana-iroha
+ """,
+ engines="servo-2013 servo-2020",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-lists/#propdef-list-style-type",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+ )}
+% endif
+% if engine == "gecko":
+ ${helpers.predefined_type(
+ "list-style-type",
+ "ListStyleType",
+ "computed::ListStyleType::disc()",
+ engines="gecko",
+ initial_specified_value="specified::ListStyleType::disc()",
+ animation_value_type="discrete",
+ boxed=True,
+ spec="https://drafts.csswg.org/css-lists/#propdef-list-style-type",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+ )}
+% endif
+
+${helpers.predefined_type(
+ "list-style-image",
+ "Image",
+ engines="gecko servo-2013 servo-2020",
+ initial_value="computed::Image::None",
+ initial_specified_value="specified::Image::None",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-lists/#propdef-list-style-image",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "quotes",
+ "Quotes",
+ "computed::Quotes::get_initial_value()",
+ engines="gecko servo-2013",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-content/#propdef-quotes",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/margin.mako.rs b/servo/components/style/properties/longhands/margin.mako.rs
new file mode 100644
index 0000000000..b5a87f9683
--- /dev/null
+++ b/servo/components/style/properties/longhands/margin.mako.rs
@@ -0,0 +1,55 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import ALL_SIDES, DEFAULT_RULES_AND_PAGE, maybe_moz_logical_alias %>
+<% data.new_style_struct("Margin", inherited=False) %>
+
+% for side in ALL_SIDES:
+ <%
+ spec = "https://drafts.csswg.org/css-box/#propdef-margin-%s" % side[0]
+ if side[1]:
+ spec = "https://drafts.csswg.org/css-logical-props/#propdef-margin-%s" % side[1]
+ %>
+ ${helpers.predefined_type(
+ "margin-%s" % side[0],
+ "LengthPercentageOrAuto",
+ "computed::LengthPercentageOrAuto::zero()",
+ engines="gecko servo-2013 servo-2020",
+ aliases=maybe_moz_logical_alias(engine, side, "-moz-margin-%s"),
+ allow_quirks="No" if side[1] else "Yes",
+ animation_value_type="ComputedValue",
+ logical=side[1],
+ logical_group="margin",
+ spec=spec,
+ rule_types_allowed=DEFAULT_RULES_AND_PAGE,
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+% endfor
+
+${helpers.predefined_type(
+ "overflow-clip-margin",
+ "Length",
+ "computed::Length::zero()",
+ parse_method="parse_non_negative",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-overflow/#propdef-overflow-clip-margin",
+ animation_value_type="ComputedValue",
+ affects="overflow",
+)}
+
+% for side in ALL_SIDES:
+ ${helpers.predefined_type(
+ "scroll-margin-%s" % side[0],
+ "Length",
+ "computed::Length::zero()",
+ engines="gecko",
+ logical=side[1],
+ logical_group="scroll-margin",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin-%s" % side[0],
+ animation_value_type="ComputedValue",
+ affects="",
+ )}
+% endfor
diff --git a/servo/components/style/properties/longhands/outline.mako.rs b/servo/components/style/properties/longhands/outline.mako.rs
new file mode 100644
index 0000000000..8e7f956bf5
--- /dev/null
+++ b/servo/components/style/properties/longhands/outline.mako.rs
@@ -0,0 +1,57 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import Method %>
+
+<% data.new_style_struct("Outline",
+ inherited=False,
+ additional_methods=[Method("outline_has_nonzero_width", "bool")]) %>
+
+// TODO(pcwalton): `invert`
+${helpers.predefined_type(
+ "outline-color",
+ "Color",
+ "computed_value::T::currentcolor()",
+ engines="gecko servo-2013",
+ initial_specified_value="specified::Color::currentcolor()",
+ animation_value_type="AnimatedColor",
+ ignored_when_colors_disabled=True,
+ spec="https://drafts.csswg.org/css-ui/#propdef-outline-color",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "outline-style",
+ "OutlineStyle",
+ "computed::OutlineStyle::none()",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ initial_specified_value="specified::OutlineStyle::none()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-ui/#propdef-outline-style",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "outline-width",
+ "BorderSideWidth",
+ "app_units::Au::from_px(3)",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.2020.unimplemented",
+ initial_specified_value="specified::BorderSideWidth::medium()",
+ animation_value_type="NonNegativeLength",
+ spec="https://drafts.csswg.org/css-ui/#propdef-outline-width",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "outline-offset",
+ "Length",
+ "crate::values::computed::Length::new(0.)",
+ engines="gecko servo-2013",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-ui/#propdef-outline-offset",
+ affects="overflow",
+)}
diff --git a/servo/components/style/properties/longhands/padding.mako.rs b/servo/components/style/properties/longhands/padding.mako.rs
new file mode 100644
index 0000000000..a165e2cd34
--- /dev/null
+++ b/servo/components/style/properties/longhands/padding.mako.rs
@@ -0,0 +1,43 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import ALL_SIDES, maybe_moz_logical_alias %>
+<% data.new_style_struct("Padding", inherited=False) %>
+
+% for side in ALL_SIDES:
+ <%
+ spec = "https://drafts.csswg.org/css-box/#propdef-padding-%s" % side[0]
+ if side[1]:
+ spec = "https://drafts.csswg.org/css-logical-props/#propdef-padding-%s" % side[1]
+ %>
+ ${helpers.predefined_type(
+ "padding-%s" % side[0],
+ "NonNegativeLengthPercentage",
+ "computed::NonNegativeLengthPercentage::zero()",
+ engines="gecko servo-2013 servo-2020",
+ aliases=maybe_moz_logical_alias(engine, side, "-moz-padding-%s"),
+ animation_value_type="NonNegativeLengthPercentage",
+ logical=side[1],
+ logical_group="padding",
+ spec=spec,
+ allow_quirks="No" if side[1] else "Yes",
+ servo_restyle_damage="reflow rebuild_and_reflow_inline",
+ affects="layout",
+ )}
+% endfor
+
+% for side in ALL_SIDES:
+ ${helpers.predefined_type(
+ "scroll-padding-%s" % side[0],
+ "NonNegativeLengthPercentageOrAuto",
+ "computed::NonNegativeLengthPercentageOrAuto::auto()",
+ engines="gecko",
+ logical=side[1],
+ logical_group="scroll-padding",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding-%s" % side[0],
+ animation_value_type="NonNegativeLengthPercentageOrAuto",
+ affects="",
+ )}
+% endfor
diff --git a/servo/components/style/properties/longhands/page.mako.rs b/servo/components/style/properties/longhands/page.mako.rs
new file mode 100644
index 0000000000..86cd284e18
--- /dev/null
+++ b/servo/components/style/properties/longhands/page.mako.rs
@@ -0,0 +1,44 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import PAGE_RULE %>
+
+<% data.new_style_struct("Page", inherited=False) %>
+
+${helpers.predefined_type(
+ "size",
+ "PageSize",
+ "computed::PageSize::auto()",
+ engines="gecko",
+ initial_specified_value="specified::PageSize::auto()",
+ spec="https://drafts.csswg.org/css-page-3/#page-size-prop",
+ boxed=True,
+ animation_value_type="none",
+ rule_types_allowed=PAGE_RULE,
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "page",
+ "PageName",
+ "computed::PageName::auto()",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-page-3/#using-named-pages",
+ animation_value_type="discrete",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "page-orientation",
+ "PageOrientation",
+ "computed::PageOrientation::Upright",
+ engines="gecko",
+ gecko_pref="layout.css.page-orientation.enabled",
+ initial_specified_value="specified::PageOrientation::Upright",
+ spec="https://drafts.csswg.org/css-page-3/#page-orientation-prop",
+ animation_value_type="none",
+ rule_types_allowed=PAGE_RULE,
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/position.mako.rs b/servo/components/style/properties/longhands/position.mako.rs
new file mode 100644
index 0000000000..fb68baa6b4
--- /dev/null
+++ b/servo/components/style/properties/longhands/position.mako.rs
@@ -0,0 +1,485 @@
+/* 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/. */
+
+<%! from data import to_rust_ident %>
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import ALL_SIZES, PHYSICAL_SIDES, LOGICAL_SIDES %>
+
+<% data.new_style_struct("Position", inherited=False) %>
+
+// "top" / "left" / "bottom" / "right"
+% for side in PHYSICAL_SIDES:
+ ${helpers.predefined_type(
+ side,
+ "LengthPercentageOrAuto",
+ "computed::LengthPercentageOrAuto::auto()",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://www.w3.org/TR/CSS2/visuren.html#propdef-%s" % side,
+ animation_value_type="ComputedValue",
+ allow_quirks="Yes",
+ servo_restyle_damage="reflow_out_of_flow",
+ logical_group="inset",
+ affects="layout",
+ )}
+% endfor
+// inset-* logical properties, map to "top" / "left" / "bottom" / "right"
+% for side in LOGICAL_SIDES:
+ ${helpers.predefined_type(
+ "inset-%s" % side,
+ "LengthPercentageOrAuto",
+ "computed::LengthPercentageOrAuto::auto()",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-logical-props/#propdef-inset-%s" % side,
+ animation_value_type="ComputedValue",
+ logical=True,
+ logical_group="inset",
+ affects="layout",
+ )}
+% endfor
+
+${helpers.predefined_type(
+ "z-index",
+ "ZIndex",
+ "computed::ZIndex::auto()",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://www.w3.org/TR/CSS2/visuren.html#z-index",
+ animation_value_type="ComputedValue",
+ affects="paint",
+)}
+
+// CSS Flexible Box Layout Module Level 1
+// http://www.w3.org/TR/css3-flexbox/
+
+// Flex container properties
+${helpers.single_keyword(
+ "flex-direction",
+ "row row-reverse column column-reverse",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ spec="https://drafts.csswg.org/css-flexbox/#flex-direction-property",
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ servo_restyle_damage = "reflow",
+ gecko_enum_prefix = "StyleFlexDirection",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "flex-wrap",
+ "nowrap wrap wrap-reverse",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ spec="https://drafts.csswg.org/css-flexbox/#flex-wrap-property",
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ servo_restyle_damage = "reflow",
+ gecko_enum_prefix = "StyleFlexWrap",
+ affects="layout",
+)}
+
+% if engine == "servo-2013":
+ // FIXME: Update Servo to support the same Syntax as Gecko.
+ ${helpers.single_keyword(
+ "justify-content",
+ "flex-start stretch flex-end center space-between space-around",
+ engines="servo-2013",
+ extra_prefixes="webkit",
+ spec="https://drafts.csswg.org/css-align/#propdef-justify-content",
+ animation_value_type="discrete",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+ )}
+% endif
+% if engine == "gecko":
+ ${helpers.predefined_type(
+ "justify-content",
+ "JustifyContent",
+ "specified::JustifyContent(specified::ContentDistribution::normal())",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-align/#propdef-justify-content",
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+
+ ${helpers.predefined_type(
+ "justify-tracks",
+ "JustifyTracks",
+ "specified::JustifyTracks::default()",
+ engines="gecko",
+ gecko_pref="layout.css.grid-template-masonry-value.enabled",
+ animation_value_type="discrete",
+ servo_restyle_damage="reflow",
+ spec="https://github.com/w3c/csswg-drafts/issues/4650",
+ affects="layout",
+ )}
+% endif
+
+% if engine in ["servo-2013", "servo-2020"]:
+ // FIXME: Update Servo to support the same Syntax as Gecko.
+ ${helpers.single_keyword(
+ "align-content",
+ "stretch flex-start flex-end center space-between space-around",
+ engines="servo-2013",
+ extra_prefixes="webkit",
+ spec="https://drafts.csswg.org/css-align/#propdef-align-content",
+ animation_value_type="discrete",
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+
+ ${helpers.single_keyword(
+ "align-items",
+ "stretch flex-start flex-end center baseline",
+ engines="servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ extra_prefixes="webkit",
+ spec="https://drafts.csswg.org/css-flexbox/#align-items-property",
+ animation_value_type="discrete",
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+% endif
+% if engine == "gecko":
+ ${helpers.predefined_type(
+ "align-content",
+ "AlignContent",
+ "specified::AlignContent(specified::ContentDistribution::normal())",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-align/#propdef-align-content",
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+
+ ${helpers.predefined_type(
+ "align-tracks",
+ "AlignTracks",
+ "specified::AlignTracks::default()",
+ engines="gecko",
+ gecko_pref="layout.css.grid-template-masonry-value.enabled",
+ animation_value_type="discrete",
+ servo_restyle_damage="reflow",
+ spec="https://github.com/w3c/csswg-drafts/issues/4650",
+ affects="layout",
+ )}
+
+ ${helpers.predefined_type(
+ "align-items",
+ "AlignItems",
+ "specified::AlignItems::normal()",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-align/#propdef-align-items",
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+
+ ${helpers.predefined_type(
+ "justify-items",
+ "JustifyItems",
+ "computed::JustifyItems::legacy()",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-align/#propdef-justify-items",
+ animation_value_type="discrete",
+ affects="layout",
+ )}
+% endif
+
+// Flex item properties
+${helpers.predefined_type(
+ "flex-grow",
+ "NonNegativeNumber",
+ "From::from(0.0)",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ spec="https://drafts.csswg.org/css-flexbox/#flex-grow-property",
+ extra_prefixes="webkit",
+ animation_value_type="NonNegativeNumber",
+ servo_restyle_damage="reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "flex-shrink",
+ "NonNegativeNumber",
+ "From::from(1.0)",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ spec="https://drafts.csswg.org/css-flexbox/#flex-shrink-property",
+ extra_prefixes="webkit",
+ animation_value_type="NonNegativeNumber",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+)}
+
+// https://drafts.csswg.org/css-align/#align-self-property
+% if engine in ["servo-2013", "servo-2020"]:
+ // FIXME: Update Servo to support the same syntax as Gecko.
+ ${helpers.single_keyword(
+ "align-self",
+ "auto stretch flex-start flex-end center baseline",
+ engines="servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ extra_prefixes="webkit",
+ spec="https://drafts.csswg.org/css-flexbox/#propdef-align-self",
+ animation_value_type="discrete",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+ )}
+% endif
+% if engine == "gecko":
+ ${helpers.predefined_type(
+ "align-self",
+ "AlignSelf",
+ "specified::AlignSelf(specified::SelfAlignment::auto())",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-align/#align-self-property",
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ affects="layout",
+ )}
+
+ ${helpers.predefined_type(
+ "justify-self",
+ "JustifySelf",
+ "specified::JustifySelf(specified::SelfAlignment::auto())",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-align/#justify-self-property",
+ animation_value_type="discrete",
+ affects="layout",
+ )}
+% endif
+
+// https://drafts.csswg.org/css-flexbox/#propdef-order
+${helpers.predefined_type(
+ "order",
+ "Integer",
+ "0",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ extra_prefixes="webkit",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-flexbox/#order-property",
+ servo_restyle_damage="reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "flex-basis",
+ "FlexBasis",
+ "computed::FlexBasis::auto()",
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ spec="https://drafts.csswg.org/css-flexbox/#flex-basis-property",
+ extra_prefixes="webkit",
+ animation_value_type="FlexBasis",
+ servo_restyle_damage="reflow",
+ boxed=True,
+ affects="layout",
+)}
+
+% for (size, logical) in ALL_SIZES:
+ <%
+ spec = "https://drafts.csswg.org/css-box/#propdef-%s"
+ if logical:
+ spec = "https://drafts.csswg.org/css-logical-props/#propdef-%s"
+ %>
+ // width, height, block-size, inline-size
+ ${helpers.predefined_type(
+ size,
+ "Size",
+ "computed::Size::auto()",
+ engines="gecko servo-2013 servo-2020",
+ logical=logical,
+ logical_group="size",
+ allow_quirks="No" if logical else "Yes",
+ spec=spec % size,
+ animation_value_type="Size",
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+ // min-width, min-height, min-block-size, min-inline-size
+ ${helpers.predefined_type(
+ "min-%s" % size,
+ "Size",
+ "computed::Size::auto()",
+ engines="gecko servo-2013 servo-2020",
+ logical=logical,
+ logical_group="min-size",
+ allow_quirks="No" if logical else "Yes",
+ spec=spec % size,
+ animation_value_type="Size",
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+ ${helpers.predefined_type(
+ "max-%s" % size,
+ "MaxSize",
+ "computed::MaxSize::none()",
+ engines="gecko servo-2013 servo-2020",
+ logical=logical,
+ logical_group="max-size",
+ allow_quirks="No" if logical else "Yes",
+ spec=spec % size,
+ animation_value_type="MaxSize",
+ servo_restyle_damage="reflow",
+ affects="layout",
+ )}
+% endfor
+
+${helpers.single_keyword(
+ "box-sizing",
+ "content-box border-box",
+ engines="gecko servo-2013 servo-2020",
+ extra_prefixes="moz:layout.css.prefixes.box-sizing webkit",
+ spec="https://drafts.csswg.org/css-ui/#propdef-box-sizing",
+ gecko_enum_prefix="StyleBoxSizing",
+ custom_consts={ "content-box": "Content", "border-box": "Border" },
+ animation_value_type="discrete",
+ servo_restyle_damage = "reflow",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "object-fit",
+ "fill contain cover none scale-down",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-images/#propdef-object-fit",
+ gecko_enum_prefix = "StyleObjectFit",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "object-position",
+ "Position",
+ "computed::Position::center()",
+ engines="gecko",
+ boxed=True,
+ spec="https://drafts.csswg.org/css-images-3/#the-object-position",
+ animation_value_type="ComputedValue",
+ affects="layout",
+)}
+
+% for kind in ["row", "column"]:
+ % for range in ["start", "end"]:
+ ${helpers.predefined_type(
+ "grid-%s-%s" % (kind, range),
+ "GridLine",
+ "Default::default()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid-%s-%s" % (kind, range),
+ affects="layout",
+ )}
+ % endfor
+
+ ${helpers.predefined_type(
+ "grid-auto-%ss" % kind,
+ "ImplicitGridTracks",
+ "Default::default()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid-auto-%ss" % kind,
+ affects="layout",
+ )}
+
+ ${helpers.predefined_type(
+ "grid-template-%ss" % kind,
+ "GridTemplateComponent",
+ "specified::GenericGridTemplateComponent::None",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid-template-%ss" % kind,
+ animation_value_type="ComputedValue",
+ affects="layout",
+ )}
+
+% endfor
+
+${helpers.predefined_type(
+ "masonry-auto-flow",
+ "MasonryAutoFlow",
+ "computed::MasonryAutoFlow::initial()",
+ engines="gecko",
+ gecko_pref="layout.css.grid-template-masonry-value.enabled",
+ animation_value_type="discrete",
+ spec="https://github.com/w3c/csswg-drafts/issues/4650",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "grid-auto-flow",
+ "GridAutoFlow",
+ "computed::GridAutoFlow::ROW",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid-auto-flow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "grid-template-areas",
+ "GridTemplateAreas",
+ "computed::GridTemplateAreas::none()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid-template-areas",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "column-gap",
+ "length::NonNegativeLengthPercentageOrNormal",
+ "computed::length::NonNegativeLengthPercentageOrNormal::normal()",
+ engines="gecko servo-2013",
+ aliases="grid-column-gap" if engine == "gecko" else "",
+ servo_2013_pref="layout.columns.enabled",
+ spec="https://drafts.csswg.org/css-align-3/#propdef-column-gap",
+ animation_value_type="NonNegativeLengthPercentageOrNormal",
+ servo_restyle_damage="reflow",
+ affects="layout",
+)}
+
+// no need for -moz- prefixed alias for this property
+${helpers.predefined_type(
+ "row-gap",
+ "length::NonNegativeLengthPercentageOrNormal",
+ "computed::length::NonNegativeLengthPercentageOrNormal::normal()",
+ engines="gecko",
+ aliases="grid-row-gap",
+ spec="https://drafts.csswg.org/css-align-3/#propdef-row-gap",
+ animation_value_type="NonNegativeLengthPercentageOrNormal",
+ servo_restyle_damage="reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "aspect-ratio",
+ "AspectRatio",
+ "computed::AspectRatio::auto()",
+ engines="gecko servo-2013",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-sizing-4/#aspect-ratio",
+ servo_restyle_damage="reflow",
+ affects="layout",
+)}
+
+% for (size, logical) in ALL_SIZES:
+ ${helpers.predefined_type(
+ "contain-intrinsic-" + size,
+ "ContainIntrinsicSize",
+ "computed::ContainIntrinsicSize::None",
+ engines="gecko",
+ logical_group="contain-intrinsic-size",
+ logical=logical,
+ gecko_pref="layout.css.contain-intrinsic-size.enabled",
+ spec="https://drafts.csswg.org/css-sizing-4/#intrinsic-size-override",
+ animation_value_type="NonNegativeLength",
+ affects="layout",
+ )}
+% endfor
diff --git a/servo/components/style/properties/longhands/svg.mako.rs b/servo/components/style/properties/longhands/svg.mako.rs
new file mode 100644
index 0000000000..10788d4802
--- /dev/null
+++ b/servo/components/style/properties/longhands/svg.mako.rs
@@ -0,0 +1,282 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("SVG", inherited=False, gecko_name="SVGReset") %>
+
+${helpers.single_keyword(
+ "vector-effect",
+ "none non-scaling-stroke",
+ engines="gecko",
+ gecko_enum_prefix="StyleVectorEffect",
+ animation_value_type="discrete",
+ spec="https://svgwg.org/svg2-draft/coords.html#VectorEffects",
+ affects="layout",
+)}
+
+// Section 14 - Gradients and Patterns
+
+${helpers.predefined_type(
+ "stop-color",
+ "Color",
+ "computed::Color::BLACK",
+ engines="gecko",
+ animation_value_type="AnimatedRGBA",
+ spec="https://svgwg.org/svg2-draft/pservers.html#StopColorProperties",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "stop-opacity",
+ "Opacity",
+ "1.0",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/pservers.html#StopOpacityProperty",
+ affects="paint",
+)}
+
+// Filter Effects Module
+
+${helpers.predefined_type(
+ "flood-color",
+ "Color",
+ "computed::Color::BLACK",
+ engines="gecko",
+ animation_value_type="AnimatedColor",
+ spec="https://drafts.fxtf.org/filter-effects-1/#FloodColorProperty",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "flood-opacity",
+ "Opacity",
+ "1.0",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.fxtf.org/filter-effects-1/#FloodOpacityProperty",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "lighting-color",
+ "Color",
+ "computed::Color::WHITE",
+ engines="gecko",
+ animation_value_type="AnimatedColor",
+ spec="https://drafts.fxtf.org/filter-effects-1#LightingColorProperty",
+ affects="paint",
+)}
+
+// CSS Masking Module Level 1
+// https://drafts.fxtf.org/css-masking-1
+${helpers.single_keyword(
+ "mask-type",
+ "luminance alpha",
+ engines="gecko",
+ gecko_enum_prefix="StyleMaskType",
+ animation_value_type="discrete",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-type",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "clip-path",
+ "basic_shape::ClipPath",
+ "generics::basic_shape::ClipPath::None",
+ engines="gecko",
+ extra_prefixes="webkit",
+ animation_value_type="basic_shape::ClipPath",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-clip-path",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "mask-mode",
+ "match-source alpha luminance",
+ engines="gecko",
+ gecko_enum_prefix="StyleMaskMode",
+ vector=True,
+ animation_value_type="discrete",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-mode",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "mask-repeat",
+ "BackgroundRepeat",
+ "computed::BackgroundRepeat::repeat()",
+ engines="gecko",
+ initial_specified_value="specified::BackgroundRepeat::repeat()",
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-repeat",
+ vector=True,
+ affects="paint",
+)}
+
+% for (axis, direction) in [("x", "Horizontal"), ("y", "Vertical")]:
+ ${helpers.predefined_type(
+ "mask-position-" + axis,
+ "position::" + direction + "Position",
+ "computed::LengthPercentage::zero_percent()",
+ engines="gecko",
+ extra_prefixes="webkit",
+ initial_specified_value="specified::PositionComponent::Center",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-position",
+ animation_value_type="ComputedValue",
+ vector_animation_type="repeatable_list",
+ vector=True,
+ affects="paint",
+ )}
+% endfor
+
+${helpers.single_keyword(
+ "mask-clip",
+ "border-box content-box padding-box",
+ engines="gecko",
+ extra_gecko_values="fill-box stroke-box view-box no-clip",
+ vector=True,
+ extra_prefixes="webkit",
+ gecko_enum_prefix="StyleGeometryBox",
+ gecko_inexhaustive=True,
+ animation_value_type="discrete",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-clip",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "mask-origin",
+ "border-box content-box padding-box",
+ engines="gecko",
+ extra_gecko_values="fill-box stroke-box view-box",
+ vector=True,
+ extra_prefixes="webkit",
+ gecko_enum_prefix="StyleGeometryBox",
+ gecko_inexhaustive=True,
+ animation_value_type="discrete",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-origin",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "mask-size",
+ "background::BackgroundSize",
+ "computed::BackgroundSize::auto()",
+ engines="gecko",
+ initial_specified_value="specified::BackgroundSize::auto()",
+ extra_prefixes="webkit",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-size",
+ animation_value_type="MaskSizeList",
+ vector=True,
+ vector_animation_type="repeatable_list",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "mask-composite",
+ "add subtract intersect exclude",
+ engines="gecko",
+ gecko_enum_prefix="StyleMaskComposite",
+ vector=True,
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-composite",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "mask-image",
+ "Image",
+ engines="gecko",
+ initial_value="computed::Image::None",
+ initial_specified_value="specified::Image::None",
+ parse_method="parse_with_cors_anonymous",
+ spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-image",
+ vector=True,
+ extra_prefixes="webkit",
+ animation_value_type="discrete",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "x",
+ "LengthPercentage",
+ "computed::LengthPercentage::zero()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/geometry.html#X",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "y",
+ "LengthPercentage",
+ "computed::LengthPercentage::zero()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/geometry.html#Y",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "cx",
+ "LengthPercentage",
+ "computed::LengthPercentage::zero()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/geometry.html#CX",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "cy",
+ "LengthPercentage",
+ "computed::LengthPercentage::zero()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/geometry.html#CY",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "rx",
+ "NonNegativeLengthPercentageOrAuto",
+ "computed::NonNegativeLengthPercentageOrAuto::auto()",
+ engines="gecko",
+ animation_value_type="LengthPercentageOrAuto",
+ spec="https://svgwg.org/svg2-draft/geometry.html#RX",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "ry",
+ "NonNegativeLengthPercentageOrAuto",
+ "computed::NonNegativeLengthPercentageOrAuto::auto()",
+ engines="gecko",
+ animation_value_type="LengthPercentageOrAuto",
+ spec="https://svgwg.org/svg2-draft/geometry.html#RY",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "r",
+ "NonNegativeLengthPercentage",
+ "computed::NonNegativeLengthPercentage::zero()",
+ engines="gecko",
+ animation_value_type="LengthPercentage",
+ spec="https://svgwg.org/svg2-draft/geometry.html#R",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "d",
+ "DProperty",
+ "specified::DProperty::none()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="https://svgwg.org/svg2-draft/paths.html#TheDProperty",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/table.mako.rs b/servo/components/style/properties/longhands/table.mako.rs
new file mode 100644
index 0000000000..3a756636ad
--- /dev/null
+++ b/servo/components/style/properties/longhands/table.mako.rs
@@ -0,0 +1,30 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<% data.new_style_struct("Table", inherited=False) %>
+
+${helpers.single_keyword(
+ "table-layout",
+ "auto fixed",
+ engines="gecko servo-2013",
+ gecko_ffi_name="mLayoutStrategy",
+ animation_value_type="discrete",
+ gecko_enum_prefix="StyleTableLayout",
+ spec="https://drafts.csswg.org/css-tables/#propdef-table-layout",
+ servo_restyle_damage="reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "-x-span",
+ "Integer",
+ "1",
+ engines="gecko",
+ spec="Internal-only (for `<col span>` pres attr)",
+ animation_value_type="none",
+ enabled_in="",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/longhands/text.mako.rs b/servo/components/style/properties/longhands/text.mako.rs
new file mode 100644
index 0000000000..0ee8ba3168
--- /dev/null
+++ b/servo/components/style/properties/longhands/text.mako.rs
@@ -0,0 +1,88 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import Method %>
+
+<% data.new_style_struct("Text", inherited=False, gecko_name="TextReset") %>
+
+${helpers.predefined_type(
+ "text-overflow",
+ "TextOverflow",
+ "computed::TextOverflow::get_initial_value()",
+ engines="gecko servo-2013",
+ animation_value_type="discrete",
+ boxed=True,
+ spec="https://drafts.csswg.org/css-ui/#propdef-text-overflow",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="paint",
+)}
+
+${helpers.single_keyword(
+ "unicode-bidi",
+ "normal embed isolate bidi-override isolate-override plaintext",
+ engines="gecko servo-2013",
+ gecko_enum_prefix="StyleUnicodeBidi",
+ animation_value_type="none",
+ spec="https://drafts.csswg.org/css-writing-modes/#propdef-unicode-bidi",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "text-decoration-line",
+ "TextDecorationLine",
+ "specified::TextDecorationLine::none()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::TextDecorationLine::none()",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration-line",
+ servo_restyle_damage="rebuild_and_reflow",
+ affects="overflow",
+)}
+
+${helpers.single_keyword(
+ "text-decoration-style",
+ "solid double dotted dashed wavy -moz-none",
+ engines="gecko servo-2020",
+ gecko_enum_prefix="StyleTextDecorationStyle",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration-style",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "text-decoration-color",
+ "Color",
+ "computed_value::T::currentcolor()",
+ engines="gecko servo-2020",
+ initial_specified_value="specified::Color::currentcolor()",
+ animation_value_type="AnimatedColor",
+ ignored_when_colors_disabled=True,
+ spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration-color",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "initial-letter",
+ "InitialLetter",
+ "computed::InitialLetter::normal()",
+ engines="gecko",
+ initial_specified_value="specified::InitialLetter::normal()",
+ animation_value_type="discrete",
+ gecko_pref="layout.css.initial-letter.enabled",
+ spec="https://drafts.csswg.org/css-inline/#sizing-drop-initials",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "text-decoration-thickness",
+ "TextDecorationLength",
+ "generics::text::GenericTextDecorationLength::Auto",
+ engines="gecko",
+ initial_specified_value="generics::text::GenericTextDecorationLength::Auto",
+ animation_value_type="ComputedValue",
+ spec="https://drafts.csswg.org/css-text-decor-4/#text-decoration-width-property",
+ affects="overflow",
+)}
diff --git a/servo/components/style/properties/longhands/ui.mako.rs b/servo/components/style/properties/longhands/ui.mako.rs
new file mode 100644
index 0000000000..1150816ac0
--- /dev/null
+++ b/servo/components/style/properties/longhands/ui.mako.rs
@@ -0,0 +1,422 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import DEFAULT_RULES_EXCEPT_KEYFRAME, Method %>
+
+// CSS Basic User Interface Module Level 1
+// https://drafts.csswg.org/css-ui-3/
+<% data.new_style_struct("UI", inherited=False, gecko_name="UIReset") %>
+
+// TODO spec says that UAs should not support this
+// we should probably remove from gecko (https://bugzilla.mozilla.org/show_bug.cgi?id=1328331)
+${helpers.single_keyword(
+ "ime-mode",
+ "auto normal active disabled inactive",
+ engines="gecko",
+ gecko_enum_prefix="StyleImeMode",
+ gecko_ffi_name="mIMEMode",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-ui/#input-method-editor",
+ affects="",
+)}
+
+${helpers.single_keyword(
+ "scrollbar-width",
+ "auto thin none",
+ engines="gecko",
+ gecko_enum_prefix="StyleScrollbarWidth",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-scrollbars-1/#scrollbar-width",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "user-select",
+ "UserSelect",
+ "computed::UserSelect::Auto",
+ engines="gecko",
+ extra_prefixes="moz webkit",
+ animation_value_type="discrete",
+ spec="https://drafts.csswg.org/css-ui-4/#propdef-user-select",
+ affects="",
+)}
+
+// TODO(emilio): This probably should be hidden from content.
+${helpers.single_keyword(
+ "-moz-window-dragging",
+ "default drag no-drag",
+ engines="gecko",
+ gecko_ffi_name="mWindowDragging",
+ gecko_enum_prefix="StyleWindowDragging",
+ animation_value_type="discrete",
+ spec="None (Nonstandard Firefox-only property)",
+ affects="paint",
+)}
+
+// TODO(emilio): Maybe make shadow behavior on macOS match Linux / Windows, and remove this? But
+// that requires making -moz-window-input-region-margin work there...
+${helpers.single_keyword(
+ "-moz-window-shadow",
+ "auto none",
+ engines="gecko",
+ gecko_ffi_name="mWindowShadow",
+ gecko_enum_prefix="StyleWindowShadow",
+ animation_value_type="discrete",
+ enabled_in="chrome",
+ spec="None (Nonstandard internal property)",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "-moz-window-opacity",
+ "Opacity",
+ "1.0",
+ engines="gecko",
+ gecko_ffi_name="mWindowOpacity",
+ animation_value_type="ComputedValue",
+ spec="None (Nonstandard internal property)",
+ enabled_in="chrome",
+ affects="paint",
+)}
+
+${helpers.predefined_type(
+ "-moz-window-transform",
+ "Transform",
+ "generics::transform::Transform::none()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="None (Nonstandard internal property)",
+ enabled_in="chrome",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "-moz-window-transform-origin",
+ "TransformOrigin",
+ "computed::TransformOrigin::initial_value()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ gecko_ffi_name="mWindowTransformOrigin",
+ boxed=True,
+ spec="None (Nonstandard internal property)",
+ enabled_in="chrome",
+ affects="overflow",
+)}
+
+${helpers.predefined_type(
+ "-moz-window-input-region-margin",
+ "Length",
+ "computed::Length::zero()",
+ engines="gecko",
+ animation_value_type="ComputedValue",
+ spec="None (Nonstandard internal property)",
+ enabled_in="chrome",
+ affects="",
+)}
+
+// Hack to allow chrome to hide stuff only visually (without hiding it from a11y).
+${helpers.predefined_type(
+ "-moz-subtree-hidden-only-visually",
+ "BoolInteger",
+ "computed::BoolInteger::zero()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="None (Nonstandard internal property)",
+ enabled_in="chrome",
+ affects="paint",
+)}
+
+// TODO(emilio): Probably also should be hidden from content.
+${helpers.predefined_type(
+ "-moz-force-broken-image-icon",
+ "BoolInteger",
+ "computed::BoolInteger::zero()",
+ engines="gecko",
+ animation_value_type="discrete",
+ spec="None (Nonstandard Firefox-only property)",
+ affects="layout",
+)}
+
+<% transition_extra_prefixes = "moz:layout.css.prefixes.transitions webkit" %>
+
+${helpers.predefined_type(
+ "transition-duration",
+ "Time",
+ "computed::Time::zero()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::Time::zero()",
+ parse_method="parse_non_negative",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=transition_extra_prefixes,
+ spec="https://drafts.csswg.org/css-transitions/#propdef-transition-duration",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "transition-timing-function",
+ "TimingFunction",
+ "computed::TimingFunction::ease()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::TimingFunction::ease()",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=transition_extra_prefixes,
+ spec="https://drafts.csswg.org/css-transitions/#propdef-transition-timing-function",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "transition-property",
+ "TransitionProperty",
+ "computed::TransitionProperty::all()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::TransitionProperty::all()",
+ vector=True,
+ none_value="computed::TransitionProperty::none()",
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=transition_extra_prefixes,
+ spec="https://drafts.csswg.org/css-transitions/#propdef-transition-property",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "transition-delay",
+ "Time",
+ "computed::Time::zero()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::Time::zero()",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=transition_extra_prefixes,
+ spec="https://drafts.csswg.org/css-transitions/#propdef-transition-delay",
+ affects="",
+)}
+
+<% animation_extra_prefixes = "moz:layout.css.prefixes.animations webkit" %>
+
+${helpers.predefined_type(
+ "animation-name",
+ "AnimationName",
+ "computed::AnimationName::none()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::AnimationName::none()",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=animation_extra_prefixes,
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ spec="https://drafts.csswg.org/css-animations/#propdef-animation-name",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "animation-duration",
+ "Time",
+ "computed::Time::zero()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::Time::zero()",
+ parse_method="parse_non_negative",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=animation_extra_prefixes,
+ spec="https://drafts.csswg.org/css-transitions/#propdef-transition-duration",
+ affects="",
+)}
+
+// animation-timing-function is the exception to the rule for allowed_in_keyframe_block:
+// https://drafts.csswg.org/css-animations/#keyframes
+${helpers.predefined_type(
+ "animation-timing-function",
+ "TimingFunction",
+ "computed::TimingFunction::ease()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::TimingFunction::ease()",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=animation_extra_prefixes,
+ spec="https://drafts.csswg.org/css-transitions/#propdef-animation-timing-function",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "animation-iteration-count",
+ "AnimationIterationCount",
+ "computed::AnimationIterationCount::one()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::AnimationIterationCount::one()",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=animation_extra_prefixes,
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ spec="https://drafts.csswg.org/css-animations/#propdef-animation-iteration-count",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "animation-direction",
+ "AnimationDirection",
+ "computed::AnimationDirection::Normal",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::AnimationDirection::Normal",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=animation_extra_prefixes,
+ spec="https://drafts.csswg.org/css-animations/#propdef-animation-direction",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "animation-play-state",
+ "AnimationPlayState",
+ "computed::AnimationPlayState::Running",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="computed::AnimationPlayState::Running",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=animation_extra_prefixes,
+ spec="https://drafts.csswg.org/css-animations/#propdef-animation-play-state",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "animation-fill-mode",
+ "AnimationFillMode",
+ "computed::AnimationFillMode::None",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="computed::AnimationFillMode::None",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=animation_extra_prefixes,
+ spec="https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "animation-composition",
+ "AnimationComposition",
+ "computed::AnimationComposition::Replace",
+ engines="gecko",
+ initial_specified_value="computed::AnimationComposition::Replace",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ gecko_pref="layout.css.animation-composition.enabled",
+ spec="https://drafts.csswg.org/css-animations-2/#animation-composition",
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "animation-delay",
+ "Time",
+ "computed::Time::zero()",
+ engines="gecko servo-2013 servo-2020",
+ initial_specified_value="specified::Time::zero()",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ extra_prefixes=animation_extra_prefixes,
+ spec="https://drafts.csswg.org/css-animations/#propdef-animation-delay",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "animation-timeline",
+ "AnimationTimeline",
+ "computed::AnimationTimeline::auto()",
+ engines="gecko",
+ initial_specified_value="specified::AnimationTimeline::auto()",
+ vector=True,
+ need_index=True,
+ animation_value_type="none",
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/css-animations-2/#propdef-animation-timeline",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "scroll-timeline-name",
+ "ScrollTimelineName",
+ "computed::ScrollTimelineName::none()",
+ vector=True,
+ need_index=True,
+ engines="gecko",
+ animation_value_type="none",
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-name",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "scroll-timeline-axis",
+ "ScrollAxis",
+ "computed::ScrollAxis::default()",
+ vector=True,
+ need_index=True,
+ engines="gecko",
+ animation_value_type="none",
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "view-timeline-name",
+ "ScrollTimelineName",
+ "computed::ScrollTimelineName::none()",
+ vector=True,
+ need_index=True,
+ engines="gecko",
+ animation_value_type="none",
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-name",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "view-timeline-axis",
+ "ScrollAxis",
+ "computed::ScrollAxis::default()",
+ vector=True,
+ need_index=True,
+ engines="gecko",
+ animation_value_type="none",
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
+
+${helpers.predefined_type(
+ "view-timeline-inset",
+ "ViewTimelineInset",
+ "computed::ViewTimelineInset::default()",
+ vector=True,
+ need_index=True,
+ engines="gecko",
+ animation_value_type="none",
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis",
+ rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME,
+ affects="",
+)}
diff --git a/servo/components/style/properties/longhands/xul.mako.rs b/servo/components/style/properties/longhands/xul.mako.rs
new file mode 100644
index 0000000000..8974ac30dc
--- /dev/null
+++ b/servo/components/style/properties/longhands/xul.mako.rs
@@ -0,0 +1,85 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import Method %>
+
+// Non-standard properties that Gecko uses for XUL elements.
+<% data.new_style_struct("XUL", inherited=False) %>
+
+${helpers.single_keyword(
+ "-moz-box-align",
+ "stretch start center baseline end",
+ engines="gecko",
+ gecko_ffi_name="mBoxAlign",
+ gecko_enum_prefix="StyleBoxAlign",
+ animation_value_type="discrete",
+ aliases="-webkit-box-align",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-align)",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "-moz-box-direction",
+ "normal reverse",
+ engines="gecko",
+ gecko_ffi_name="mBoxDirection",
+ gecko_enum_prefix="StyleBoxDirection",
+ animation_value_type="discrete",
+ aliases="-webkit-box-direction",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-direction)",
+ affects="layout",
+)}
+
+${helpers.predefined_type(
+ "-moz-box-flex",
+ "NonNegativeNumber",
+ "From::from(0.)",
+ engines="gecko",
+ gecko_ffi_name="mBoxFlex",
+ animation_value_type="NonNegativeNumber",
+ aliases="-webkit-box-flex",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-flex)",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "-moz-box-orient",
+ "horizontal vertical",
+ engines="gecko",
+ gecko_ffi_name="mBoxOrient",
+ gecko_aliases="inline-axis=horizontal block-axis=vertical",
+ gecko_enum_prefix="StyleBoxOrient",
+ animation_value_type="discrete",
+ aliases="-webkit-box-orient",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-orient)",
+ affects="layout",
+)}
+
+${helpers.single_keyword(
+ "-moz-box-pack",
+ "start center end justify",
+ engines="gecko",
+ gecko_ffi_name="mBoxPack",
+ gecko_enum_prefix="StyleBoxPack",
+ animation_value_type="discrete",
+ aliases="-webkit-box-pack",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-pack)",
+ affects="layout",
+)}
+
+// NOTE(heycam): Odd that the initial value is 1 yet 0 is a valid value. There
+// are uses of `-moz-box-ordinal-group: 0` in the tree, too.
+${helpers.predefined_type(
+ "-moz-box-ordinal-group",
+ "Integer",
+ "1",
+ engines="gecko",
+ parse_method="parse_non_negative",
+ aliases="-webkit-box-ordinal-group",
+ gecko_ffi_name="mBoxOrdinal",
+ animation_value_type="discrete",
+ spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-box-ordinal-group)",
+ affects="layout",
+)}
diff --git a/servo/components/style/properties/mod.rs b/servo/components/style/properties/mod.rs
new file mode 100644
index 0000000000..7adb6d4ae6
--- /dev/null
+++ b/servo/components/style/properties/mod.rs
@@ -0,0 +1,1531 @@
+/* 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/. */
+
+//! Supported CSS properties and the cascade.
+
+pub mod cascade;
+pub mod declaration_block;
+
+pub use self::cascade::*;
+pub use self::declaration_block::*;
+pub use self::generated::*;
+/// The CSS properties supported by the style system.
+/// Generated from the properties.mako.rs template by build.rs
+#[macro_use]
+#[allow(unsafe_code)]
+#[deny(missing_docs)]
+pub mod generated {
+ include!(concat!(env!("OUT_DIR"), "/properties.rs"));
+
+ #[cfg(feature = "gecko")]
+ #[allow(unsafe_code, missing_docs)]
+ pub mod gecko {
+ include!(concat!(env!("OUT_DIR"), "/gecko_properties.rs"));
+ }
+}
+
+use crate::custom_properties::{self, ComputedCustomProperties};
+#[cfg(feature = "gecko")]
+use crate::gecko_bindings::structs::{nsCSSPropertyID, AnimatedPropertyID, RefPtr};
+use crate::logical_geometry::WritingMode;
+use crate::parser::ParserContext;
+use crate::str::CssString;
+use crate::stylesheets::Origin;
+use crate::stylist::Stylist;
+use crate::values::{computed, serialize_atom_name};
+use arrayvec::{ArrayVec, Drain as ArrayVecDrain};
+use cssparser::{Parser, ParserInput};
+use fxhash::FxHashMap;
+use servo_arc::Arc;
+use std::{
+ borrow::Cow,
+ fmt::{self, Write},
+ mem,
+};
+use style_traits::{
+ CssWriter, KeywordsCollectFn, ParseError, ParsingMode, SpecifiedValueInfo, ToCss,
+};
+
+bitflags! {
+ /// A set of flags for properties.
+ #[derive(Clone, Copy)]
+ pub struct PropertyFlags: u16 {
+ /// This longhand property applies to ::first-letter.
+ const APPLIES_TO_FIRST_LETTER = 1 << 1;
+ /// This longhand property applies to ::first-line.
+ const APPLIES_TO_FIRST_LINE = 1 << 2;
+ /// This longhand property applies to ::placeholder.
+ const APPLIES_TO_PLACEHOLDER = 1 << 3;
+ /// This longhand property applies to ::cue.
+ const APPLIES_TO_CUE = 1 << 4;
+ /// This longhand property applies to ::marker.
+ const APPLIES_TO_MARKER = 1 << 5;
+ /// This property is a legacy shorthand.
+ ///
+ /// https://drafts.csswg.org/css-cascade/#legacy-shorthand
+ const IS_LEGACY_SHORTHAND = 1 << 6;
+
+ /* The following flags are currently not used in Rust code, they
+ * only need to be listed in corresponding properties so that
+ * they can be checked in the C++ side via ServoCSSPropList.h. */
+
+ /// This property can be animated on the compositor.
+ const CAN_ANIMATE_ON_COMPOSITOR = 0;
+ /// This shorthand property is accessible from getComputedStyle.
+ const SHORTHAND_IN_GETCS = 0;
+ /// See data.py's documentation about the affects_flags.
+ const AFFECTS_LAYOUT = 0;
+ #[allow(missing_docs)]
+ const AFFECTS_OVERFLOW = 0;
+ #[allow(missing_docs)]
+ const AFFECTS_PAINT = 0;
+ }
+}
+
+/// An enum to represent a CSS Wide keyword.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum CSSWideKeyword {
+ /// The `initial` keyword.
+ Initial,
+ /// The `inherit` keyword.
+ Inherit,
+ /// The `unset` keyword.
+ Unset,
+ /// The `revert` keyword.
+ Revert,
+ /// The `revert-layer` keyword.
+ RevertLayer,
+}
+
+impl CSSWideKeyword {
+ /// Returns the string representation of the keyword.
+ pub fn to_str(&self) -> &'static str {
+ match *self {
+ CSSWideKeyword::Initial => "initial",
+ CSSWideKeyword::Inherit => "inherit",
+ CSSWideKeyword::Unset => "unset",
+ CSSWideKeyword::Revert => "revert",
+ CSSWideKeyword::RevertLayer => "revert-layer",
+ }
+ }
+}
+
+impl CSSWideKeyword {
+ /// Parses a CSS wide keyword from a CSS identifier.
+ pub fn from_ident(ident: &str) -> Result<Self, ()> {
+ Ok(match_ignore_ascii_case! { ident,
+ "initial" => CSSWideKeyword::Initial,
+ "inherit" => CSSWideKeyword::Inherit,
+ "unset" => CSSWideKeyword::Unset,
+ "revert" => CSSWideKeyword::Revert,
+ "revert-layer" => CSSWideKeyword::RevertLayer,
+ _ => return Err(()),
+ })
+ }
+
+ /// Parses a CSS wide keyword completely.
+ pub fn parse(input: &mut Parser) -> Result<Self, ()> {
+ let keyword = {
+ let ident = input.expect_ident().map_err(|_| ())?;
+ Self::from_ident(ident)?
+ };
+ input.expect_exhausted().map_err(|_| ())?;
+ Ok(keyword)
+ }
+}
+
+/// A declaration using a CSS-wide keyword.
+#[derive(Clone, PartialEq, ToCss, ToShmem, MallocSizeOf)]
+pub struct WideKeywordDeclaration {
+ #[css(skip)]
+ id: LonghandId,
+ /// The CSS-wide keyword.
+ pub keyword: CSSWideKeyword,
+}
+
+/// An unparsed declaration that contains `var()` functions.
+#[derive(Clone, PartialEq, ToCss, ToShmem, MallocSizeOf)]
+pub struct VariableDeclaration {
+ /// The id of the property this declaration represents.
+ #[css(skip)]
+ id: LonghandId,
+ /// The unparsed value of the variable.
+ #[ignore_malloc_size_of = "Arc"]
+ pub value: Arc<UnparsedValue>,
+}
+
+/// A custom property declaration value is either an unparsed value or a CSS
+/// wide-keyword.
+#[derive(Clone, PartialEq, ToCss, ToShmem)]
+pub enum CustomDeclarationValue {
+ /// A value.
+ Value(Arc<custom_properties::SpecifiedValue>),
+ /// A wide keyword.
+ CSSWideKeyword(CSSWideKeyword),
+}
+
+/// A custom property declaration with the property name and the declared value.
+#[derive(Clone, PartialEq, ToCss, ToShmem, MallocSizeOf)]
+pub struct CustomDeclaration {
+ /// The name of the custom property.
+ #[css(skip)]
+ pub name: custom_properties::Name,
+ /// The value of the custom property.
+ #[ignore_malloc_size_of = "Arc"]
+ pub value: CustomDeclarationValue,
+}
+
+impl fmt::Debug for PropertyDeclaration {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.id().to_css(&mut CssWriter::new(f))?;
+ f.write_str(": ")?;
+
+ // Because PropertyDeclaration::to_css requires CssStringWriter, we can't write
+ // it directly to f, and need to allocate an intermediate string. This is
+ // fine for debug-only code.
+ let mut s = CssString::new();
+ self.to_css(&mut s)?;
+ write!(f, "{}", s)
+ }
+}
+
+/// A longhand or shorthand property.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, ToComputedValue, ToResolvedValue, ToShmem, MallocSizeOf)]
+#[repr(C)]
+pub struct NonCustomPropertyId(u16);
+
+impl ToCss for NonCustomPropertyId {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(self.name())
+ }
+}
+
+impl NonCustomPropertyId {
+ /// Returns the underlying index, used for use counter.
+ pub fn bit(self) -> usize {
+ self.0 as usize
+ }
+
+ /// Convert a `NonCustomPropertyId` into a `nsCSSPropertyID`.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn to_nscsspropertyid(self) -> nsCSSPropertyID {
+ // unsafe: guaranteed by static_assert_nscsspropertyid.
+ unsafe { mem::transmute(self.0 as i32) }
+ }
+
+ /// Convert an `nsCSSPropertyID` into a `NonCustomPropertyId`.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn from_nscsspropertyid(prop: nsCSSPropertyID) -> Option<Self> {
+ let prop = prop as i32;
+ if prop < 0 || prop >= property_counts::NON_CUSTOM as i32 {
+ return None;
+ }
+ // guaranteed by static_assert_nscsspropertyid above.
+ Some(NonCustomPropertyId(prop as u16))
+ }
+
+ /// Resolves the alias of a given property if needed.
+ pub fn unaliased(self) -> Self {
+ let Some(alias_id) = self.as_alias() else {
+ return self;
+ };
+ alias_id.aliased_property()
+ }
+
+ /// Turns this `NonCustomPropertyId` into a `PropertyId`.
+ #[inline]
+ pub fn to_property_id(self) -> PropertyId {
+ PropertyId::NonCustom(self)
+ }
+
+ /// Returns a longhand id, if this property is one.
+ #[inline]
+ pub fn as_longhand(self) -> Option<LonghandId> {
+ if self.0 < property_counts::LONGHANDS as u16 {
+ return Some(unsafe { mem::transmute(self.0 as u16) });
+ }
+ None
+ }
+
+ /// Returns a shorthand id, if this property is one.
+ #[inline]
+ pub fn as_shorthand(self) -> Option<ShorthandId> {
+ if self.0 >= property_counts::LONGHANDS as u16 &&
+ self.0 < property_counts::LONGHANDS_AND_SHORTHANDS as u16
+ {
+ return Some(unsafe { mem::transmute(self.0 - (property_counts::LONGHANDS as u16)) });
+ }
+ None
+ }
+
+ /// Returns an alias id, if this property is one.
+ #[inline]
+ pub fn as_alias(self) -> Option<AliasId> {
+ debug_assert!((self.0 as usize) < property_counts::NON_CUSTOM);
+ if self.0 >= property_counts::LONGHANDS_AND_SHORTHANDS as u16 {
+ return Some(unsafe {
+ mem::transmute(self.0 - (property_counts::LONGHANDS_AND_SHORTHANDS as u16))
+ });
+ }
+ None
+ }
+
+ /// Returns either a longhand or a shorthand, resolving aliases.
+ #[inline]
+ pub fn longhand_or_shorthand(self) -> Result<LonghandId, ShorthandId> {
+ let id = self.unaliased();
+ match id.as_longhand() {
+ Some(lh) => Ok(lh),
+ None => Err(id.as_shorthand().unwrap()),
+ }
+ }
+
+ /// Converts a longhand id into a non-custom property id.
+ #[inline]
+ pub const fn from_longhand(id: LonghandId) -> Self {
+ Self(id as u16)
+ }
+
+ /// Converts a shorthand id into a non-custom property id.
+ #[inline]
+ pub const fn from_shorthand(id: ShorthandId) -> Self {
+ Self((id as u16) + (property_counts::LONGHANDS as u16))
+ }
+
+ /// Converts an alias id into a non-custom property id.
+ #[inline]
+ pub const fn from_alias(id: AliasId) -> Self {
+ Self((id as u16) + (property_counts::LONGHANDS_AND_SHORTHANDS as u16))
+ }
+}
+
+impl From<LonghandId> for NonCustomPropertyId {
+ #[inline]
+ fn from(id: LonghandId) -> Self {
+ Self::from_longhand(id)
+ }
+}
+
+impl From<ShorthandId> for NonCustomPropertyId {
+ #[inline]
+ fn from(id: ShorthandId) -> Self {
+ Self::from_shorthand(id)
+ }
+}
+
+impl From<AliasId> for NonCustomPropertyId {
+ #[inline]
+ fn from(id: AliasId) -> Self {
+ Self::from_alias(id)
+ }
+}
+
+/// Representation of a CSS property, that is, either a longhand, a shorthand, or a custom
+/// property.
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub enum PropertyId {
+ /// An alias for a shorthand property.
+ NonCustom(NonCustomPropertyId),
+ /// A custom property.
+ Custom(custom_properties::Name),
+}
+
+impl ToCss for PropertyId {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ PropertyId::NonCustom(id) => dest.write_str(id.name()),
+ PropertyId::Custom(ref name) => {
+ dest.write_str("--")?;
+ serialize_atom_name(name, dest)
+ },
+ }
+ }
+}
+
+impl PropertyId {
+ /// Return the longhand id that this property id represents.
+ #[inline]
+ pub fn longhand_id(&self) -> Option<LonghandId> {
+ self.non_custom_non_alias_id()?.as_longhand()
+ }
+
+ /// Returns true if this property is one of the animatable properties.
+ pub fn is_animatable(&self) -> bool {
+ match self {
+ Self::NonCustom(id) => id.is_animatable(),
+ Self::Custom(..) => true,
+ }
+ }
+
+ /// Returns a given property from the given name, _regardless of whether it is enabled or
+ /// not_, or Err(()) for unknown properties.
+ ///
+ /// Do not use for non-testing purposes.
+ pub fn parse_unchecked_for_testing(name: &str) -> Result<Self, ()> {
+ Self::parse_unchecked(name, None)
+ }
+
+ /// Parses a property name, and returns an error if it's unknown or isn't enabled for all
+ /// content.
+ #[inline]
+ pub fn parse_enabled_for_all_content(name: &str) -> Result<Self, ()> {
+ let id = Self::parse_unchecked(name, None)?;
+
+ if !id.enabled_for_all_content() {
+ return Err(());
+ }
+
+ Ok(id)
+ }
+
+ /// Parses a property name, and returns an error if it's unknown or isn't allowed in this
+ /// context.
+ #[inline]
+ pub fn parse(name: &str, context: &ParserContext) -> Result<Self, ()> {
+ let id = Self::parse_unchecked(name, context.use_counters)?;
+ if !id.allowed_in(context) {
+ return Err(());
+ }
+ Ok(id)
+ }
+
+ /// Parses a property name, and returns an error if it's unknown or isn't allowed in this
+ /// context, ignoring the rule_type checks.
+ ///
+ /// This is useful for parsing stuff from CSS values, for example.
+ #[inline]
+ pub fn parse_ignoring_rule_type(name: &str, context: &ParserContext) -> Result<Self, ()> {
+ let id = Self::parse_unchecked(name, None)?;
+ if !id.allowed_in_ignoring_rule_type(context) {
+ return Err(());
+ }
+ Ok(id)
+ }
+
+ /// Returns a property id from Gecko's nsCSSPropertyID.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn from_nscsspropertyid(id: nsCSSPropertyID) -> Option<Self> {
+ Some(NonCustomPropertyId::from_nscsspropertyid(id)?.to_property_id())
+ }
+
+ /// Returns a property id from Gecko's AnimatedPropertyID.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn from_gecko_animated_property_id(property: &AnimatedPropertyID) -> Option<Self> {
+ Some(
+ if property.mID == nsCSSPropertyID::eCSSPropertyExtra_variable {
+ debug_assert!(!property.mCustomName.mRawPtr.is_null());
+ Self::Custom(unsafe { crate::Atom::from_raw(property.mCustomName.mRawPtr) })
+ } else {
+ Self::NonCustom(NonCustomPropertyId::from_nscsspropertyid(property.mID)?)
+ },
+ )
+ }
+
+ /// Returns true if the property is a shorthand or shorthand alias.
+ #[inline]
+ pub fn is_shorthand(&self) -> bool {
+ self.as_shorthand().is_ok()
+ }
+
+ /// Given this property id, get it either as a shorthand or as a
+ /// `PropertyDeclarationId`.
+ pub fn as_shorthand(&self) -> Result<ShorthandId, PropertyDeclarationId> {
+ match *self {
+ Self::NonCustom(id) => match id.longhand_or_shorthand() {
+ Ok(lh) => Err(PropertyDeclarationId::Longhand(lh)),
+ Err(sh) => Ok(sh),
+ },
+ Self::Custom(ref name) => Err(PropertyDeclarationId::Custom(name)),
+ }
+ }
+
+ /// Returns the `NonCustomPropertyId` corresponding to this property id.
+ pub fn non_custom_id(&self) -> Option<NonCustomPropertyId> {
+ match *self {
+ Self::Custom(_) => None,
+ Self::NonCustom(id) => Some(id),
+ }
+ }
+
+ /// Returns non-alias NonCustomPropertyId corresponding to this
+ /// property id.
+ fn non_custom_non_alias_id(&self) -> Option<NonCustomPropertyId> {
+ self.non_custom_id().map(NonCustomPropertyId::unaliased)
+ }
+
+ /// Whether the property is enabled for all content regardless of the
+ /// stylesheet it was declared on (that is, in practice only checks prefs).
+ #[inline]
+ pub fn enabled_for_all_content(&self) -> bool {
+ let id = match self.non_custom_id() {
+ // Custom properties are allowed everywhere
+ None => return true,
+ Some(id) => id,
+ };
+
+ id.enabled_for_all_content()
+ }
+
+ /// Converts this PropertyId in nsCSSPropertyID, resolving aliases to the
+ /// resolved property, and returning eCSSPropertyExtra_variable for custom
+ /// properties.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn to_nscsspropertyid_resolving_aliases(&self) -> nsCSSPropertyID {
+ match self.non_custom_non_alias_id() {
+ Some(id) => id.to_nscsspropertyid(),
+ None => nsCSSPropertyID::eCSSPropertyExtra_variable,
+ }
+ }
+
+ fn allowed_in(&self, context: &ParserContext) -> bool {
+ let id = match self.non_custom_id() {
+ // Custom properties are allowed everywhere
+ None => return true,
+ Some(id) => id,
+ };
+ id.allowed_in(context)
+ }
+
+ #[inline]
+ fn allowed_in_ignoring_rule_type(&self, context: &ParserContext) -> bool {
+ let id = match self.non_custom_id() {
+ // Custom properties are allowed everywhere
+ None => return true,
+ Some(id) => id,
+ };
+ id.allowed_in_ignoring_rule_type(context)
+ }
+
+ /// Whether the property supports the given CSS type.
+ /// `ty` should a bitflags of constants in style_traits::CssType.
+ pub fn supports_type(&self, ty: u8) -> bool {
+ let id = self.non_custom_non_alias_id();
+ id.map_or(0, |id| id.supported_types()) & ty != 0
+ }
+
+ /// Collect supported starting word of values of this property.
+ ///
+ /// See style_traits::SpecifiedValueInfo::collect_completion_keywords for more
+ /// details.
+ pub fn collect_property_completion_keywords(&self, f: KeywordsCollectFn) {
+ if let Some(id) = self.non_custom_non_alias_id() {
+ id.collect_property_completion_keywords(f);
+ }
+ CSSWideKeyword::collect_completion_keywords(f);
+ }
+}
+
+impl ToCss for LonghandId {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(self.name())
+ }
+}
+
+impl fmt::Debug for LonghandId {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str(self.name())
+ }
+}
+
+impl LonghandId {
+ /// Get the name of this longhand property.
+ #[inline]
+ pub fn name(&self) -> &'static str {
+ NonCustomPropertyId::from(*self).name()
+ }
+
+ /// Returns whether the longhand property is inherited by default.
+ #[inline]
+ pub fn inherited(self) -> bool {
+ !LonghandIdSet::reset().contains(self)
+ }
+
+ /// Returns true if the property is one that is ignored when document
+ /// colors are disabled.
+ #[inline]
+ pub fn ignored_when_document_colors_disabled(self) -> bool {
+ LonghandIdSet::ignored_when_colors_disabled().contains(self)
+ }
+
+ /// Returns whether this longhand is `non_custom` or is a longhand of it.
+ pub fn is_or_is_longhand_of(self, non_custom: NonCustomPropertyId) -> bool {
+ match non_custom.longhand_or_shorthand() {
+ Ok(lh) => self == lh,
+ Err(sh) => self.is_longhand_of(sh),
+ }
+ }
+
+ /// Returns whether this longhand is a longhand of `shorthand`.
+ pub fn is_longhand_of(self, shorthand: ShorthandId) -> bool {
+ self.shorthands().any(|s| s == shorthand)
+ }
+
+ /// Returns whether this property is animatable.
+ #[inline]
+ pub fn is_animatable(self) -> bool {
+ NonCustomPropertyId::from(self).is_animatable()
+ }
+
+ /// Returns whether this property is animatable in a discrete way.
+ #[inline]
+ pub fn is_discrete_animatable(self) -> bool {
+ LonghandIdSet::discrete_animatable().contains(self)
+ }
+
+ /// Converts from a LonghandId to an adequate nsCSSPropertyID.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn to_nscsspropertyid(self) -> nsCSSPropertyID {
+ NonCustomPropertyId::from(self).to_nscsspropertyid()
+ }
+
+ #[cfg(feature = "gecko")]
+ /// Returns a longhand id from Gecko's nsCSSPropertyID.
+ pub fn from_nscsspropertyid(id: nsCSSPropertyID) -> Option<Self> {
+ NonCustomPropertyId::from_nscsspropertyid(id)?
+ .unaliased()
+ .as_longhand()
+ }
+
+ /// Return whether this property is logical.
+ #[inline]
+ pub fn is_logical(self) -> bool {
+ LonghandIdSet::logical().contains(self)
+ }
+}
+
+impl ToCss for ShorthandId {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(self.name())
+ }
+}
+
+impl ShorthandId {
+ /// Get the name for this shorthand property.
+ #[inline]
+ pub fn name(&self) -> &'static str {
+ NonCustomPropertyId::from(*self).name()
+ }
+
+ /// Converts from a ShorthandId to an adequate nsCSSPropertyID.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn to_nscsspropertyid(self) -> nsCSSPropertyID {
+ NonCustomPropertyId::from(self).to_nscsspropertyid()
+ }
+
+ /// Converts from a nsCSSPropertyID to a ShorthandId.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn from_nscsspropertyid(id: nsCSSPropertyID) -> Option<Self> {
+ NonCustomPropertyId::from_nscsspropertyid(id)?
+ .unaliased()
+ .as_shorthand()
+ }
+
+ /// Finds and returns an appendable value for the given declarations.
+ ///
+ /// Returns the optional appendable value.
+ pub fn get_shorthand_appendable_value<'a, 'b: 'a>(
+ self,
+ declarations: &'a [&'b PropertyDeclaration],
+ ) -> Option<AppendableValue<'a, 'b>> {
+ let first_declaration = declarations.get(0)?;
+ let rest = || declarations.iter().skip(1);
+
+ // https://drafts.csswg.org/css-variables/#variables-in-shorthands
+ if let Some(css) = first_declaration.with_variables_from_shorthand(self) {
+ if rest().all(|d| d.with_variables_from_shorthand(self) == Some(css)) {
+ return Some(AppendableValue::Css(css));
+ }
+ return None;
+ }
+
+ // Check whether they are all the same CSS-wide keyword.
+ if let Some(keyword) = first_declaration.get_css_wide_keyword() {
+ if rest().all(|d| d.get_css_wide_keyword() == Some(keyword)) {
+ return Some(AppendableValue::Css(keyword.to_str()));
+ }
+ return None;
+ }
+
+ if self == ShorthandId::All {
+ // 'all' only supports variables and CSS wide keywords.
+ return None;
+ }
+
+ // Check whether all declarations can be serialized as part of shorthand.
+ if declarations
+ .iter()
+ .all(|d| d.may_serialize_as_part_of_shorthand())
+ {
+ return Some(AppendableValue::DeclarationsForShorthand(
+ self,
+ declarations,
+ ));
+ }
+
+ None
+ }
+
+ /// Returns whether this property is a legacy shorthand.
+ #[inline]
+ pub fn is_legacy_shorthand(self) -> bool {
+ self.flags().contains(PropertyFlags::IS_LEGACY_SHORTHAND)
+ }
+}
+
+impl PropertyDeclaration {
+ fn with_variables_from_shorthand(&self, shorthand: ShorthandId) -> Option<&str> {
+ match *self {
+ PropertyDeclaration::WithVariables(ref declaration) => {
+ let s = declaration.value.from_shorthand?;
+ if s != shorthand {
+ return None;
+ }
+ Some(&*declaration.value.variable_value.css)
+ },
+ _ => None,
+ }
+ }
+
+ /// Returns a CSS-wide keyword declaration for a given property.
+ #[inline]
+ pub fn css_wide_keyword(id: LonghandId, keyword: CSSWideKeyword) -> Self {
+ Self::CSSWideKeyword(WideKeywordDeclaration { id, keyword })
+ }
+
+ /// Returns a CSS-wide keyword if the declaration's value is one.
+ #[inline]
+ pub fn get_css_wide_keyword(&self) -> Option<CSSWideKeyword> {
+ match *self {
+ PropertyDeclaration::CSSWideKeyword(ref declaration) => Some(declaration.keyword),
+ _ => None,
+ }
+ }
+
+ /// Returns whether the declaration may be serialized as part of a shorthand.
+ ///
+ /// This method returns false if this declaration contains variable or has a
+ /// CSS-wide keyword value, since these values cannot be serialized as part
+ /// of a shorthand.
+ ///
+ /// Caller should check `with_variables_from_shorthand()` and whether all
+ /// needed declarations has the same CSS-wide keyword first.
+ ///
+ /// Note that, serialization of a shorthand may still fail because of other
+ /// property-specific requirement even when this method returns true for all
+ /// the longhand declarations.
+ pub fn may_serialize_as_part_of_shorthand(&self) -> bool {
+ match *self {
+ PropertyDeclaration::CSSWideKeyword(..) | PropertyDeclaration::WithVariables(..) => {
+ false
+ },
+ PropertyDeclaration::Custom(..) => {
+ unreachable!("Serializing a custom property as part of shorthand?")
+ },
+ _ => true,
+ }
+ }
+
+ /// Returns true if this property declaration is for one of the animatable properties.
+ pub fn is_animatable(&self) -> bool {
+ self.id().is_animatable()
+ }
+
+ /// Returns true if this property is a custom property, false
+ /// otherwise.
+ pub fn is_custom(&self) -> bool {
+ matches!(*self, PropertyDeclaration::Custom(..))
+ }
+
+ /// The `context` parameter controls this:
+ ///
+ /// <https://drafts.csswg.org/css-animations/#keyframes>
+ /// > The <declaration-list> inside of <keyframe-block> accepts any CSS property
+ /// > except those defined in this specification,
+ /// > but does accept the `animation-play-state` property and interprets it specially.
+ ///
+ /// This will not actually parse Importance values, and will always set things
+ /// to Importance::Normal. Parsing Importance values is the job of PropertyDeclarationParser,
+ /// we only set them here so that we don't have to reallocate
+ pub fn parse_into<'i, 't>(
+ declarations: &mut SourcePropertyDeclaration,
+ id: PropertyId,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ assert!(declarations.is_empty());
+ debug_assert!(id.allowed_in(context), "{:?}", id);
+ input.skip_whitespace();
+
+ let start = input.state();
+ let non_custom_id = match id {
+ PropertyId::Custom(property_name) => {
+ let value = match input.try_parse(CSSWideKeyword::parse) {
+ Ok(keyword) => CustomDeclarationValue::CSSWideKeyword(keyword),
+ Err(()) => CustomDeclarationValue::Value(Arc::new(
+ custom_properties::VariableValue::parse(input, &context.url_data)?,
+ )),
+ };
+ declarations.push(PropertyDeclaration::Custom(CustomDeclaration {
+ name: property_name,
+ value,
+ }));
+ return Ok(());
+ },
+ PropertyId::NonCustom(id) => id,
+ };
+ match non_custom_id.longhand_or_shorthand() {
+ Ok(longhand_id) => {
+ let declaration = input
+ .try_parse(CSSWideKeyword::parse)
+ .map(|keyword| PropertyDeclaration::css_wide_keyword(longhand_id, keyword))
+ .or_else(|()| {
+ input.look_for_var_or_env_functions();
+ input.parse_entirely(|input| longhand_id.parse_value(context, input))
+ })
+ .or_else(|err| {
+ while let Ok(_) = input.next() {} // Look for var() after the error.
+ if !input.seen_var_or_env_functions() {
+ return Err(err);
+ }
+ input.reset(&start);
+ let variable_value =
+ custom_properties::VariableValue::parse(input, &context.url_data)?;
+ Ok(PropertyDeclaration::WithVariables(VariableDeclaration {
+ id: longhand_id,
+ value: Arc::new(UnparsedValue {
+ variable_value,
+ from_shorthand: None,
+ }),
+ }))
+ })?;
+ declarations.push(declaration)
+ },
+ Err(shorthand_id) => {
+ if let Ok(keyword) = input.try_parse(CSSWideKeyword::parse) {
+ if shorthand_id == ShorthandId::All {
+ declarations.all_shorthand = AllShorthand::CSSWideKeyword(keyword)
+ } else {
+ for longhand in shorthand_id.longhands() {
+ declarations
+ .push(PropertyDeclaration::css_wide_keyword(longhand, keyword));
+ }
+ }
+ } else {
+ input.look_for_var_or_env_functions();
+ // Not using parse_entirely here: each
+ // ${shorthand.ident}::parse_into function needs to do so
+ // *before* pushing to `declarations`.
+ shorthand_id
+ .parse_into(declarations, context, input)
+ .or_else(|err| {
+ while let Ok(_) = input.next() {} // Look for var() after the error.
+ if !input.seen_var_or_env_functions() {
+ return Err(err);
+ }
+
+ input.reset(&start);
+ let variable_value =
+ custom_properties::VariableValue::parse(input, &context.url_data)?;
+ let unparsed = Arc::new(UnparsedValue {
+ variable_value,
+ from_shorthand: Some(shorthand_id),
+ });
+ if shorthand_id == ShorthandId::All {
+ declarations.all_shorthand = AllShorthand::WithVariables(unparsed)
+ } else {
+ for id in shorthand_id.longhands() {
+ declarations.push(PropertyDeclaration::WithVariables(
+ VariableDeclaration {
+ id,
+ value: unparsed.clone(),
+ },
+ ))
+ }
+ }
+ Ok(())
+ })?;
+ }
+ },
+ }
+ if let Some(use_counters) = context.use_counters {
+ use_counters.non_custom_properties.record(non_custom_id);
+ }
+ Ok(())
+ }
+}
+
+/// A PropertyDeclarationId without references, for use as a hash map key.
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub enum OwnedPropertyDeclarationId {
+ /// A longhand.
+ Longhand(LonghandId),
+ /// A custom property declaration.
+ Custom(custom_properties::Name),
+}
+
+impl OwnedPropertyDeclarationId {
+ /// Return whether this property is logical.
+ #[inline]
+ pub fn is_logical(&self) -> bool {
+ self.as_borrowed().is_logical()
+ }
+
+ /// Returns the corresponding PropertyDeclarationId.
+ #[inline]
+ pub fn as_borrowed(&self) -> PropertyDeclarationId {
+ match self {
+ Self::Longhand(id) => PropertyDeclarationId::Longhand(*id),
+ Self::Custom(name) => PropertyDeclarationId::Custom(name),
+ }
+ }
+
+ /// Convert an `AnimatedPropertyID` into an `OwnedPropertyDeclarationId`.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn from_gecko_animated_property_id(property: &AnimatedPropertyID) -> Option<Self> {
+ Some(
+ match PropertyId::from_gecko_animated_property_id(property)? {
+ PropertyId::Custom(name) => Self::Custom(name),
+ PropertyId::NonCustom(id) => Self::Longhand(id.as_longhand()?),
+ },
+ )
+ }
+}
+
+/// An identifier for a given property declaration, which can be either a
+/// longhand or a custom property.
+#[derive(Clone, Copy, Debug, PartialEq, MallocSizeOf)]
+pub enum PropertyDeclarationId<'a> {
+ /// A longhand.
+ Longhand(LonghandId),
+ /// A custom property declaration.
+ Custom(&'a custom_properties::Name),
+}
+
+impl<'a> ToCss for PropertyDeclarationId<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ PropertyDeclarationId::Longhand(id) => dest.write_str(id.name()),
+ PropertyDeclarationId::Custom(name) => {
+ dest.write_str("--")?;
+ serialize_atom_name(name, dest)
+ },
+ }
+ }
+}
+
+impl<'a> PropertyDeclarationId<'a> {
+ /// Returns PropertyFlags for given property.
+ #[inline(always)]
+ pub fn flags(&self) -> PropertyFlags {
+ match self {
+ Self::Longhand(id) => id.flags(),
+ Self::Custom(_) => PropertyFlags::empty(),
+ }
+ }
+
+ /// Convert to an OwnedPropertyDeclarationId.
+ pub fn to_owned(&self) -> OwnedPropertyDeclarationId {
+ match self {
+ PropertyDeclarationId::Longhand(id) => OwnedPropertyDeclarationId::Longhand(*id),
+ PropertyDeclarationId::Custom(name) => {
+ OwnedPropertyDeclarationId::Custom((*name).clone())
+ },
+ }
+ }
+
+ /// Whether a given declaration id is either the same as `other`, or a
+ /// longhand of it.
+ pub fn is_or_is_longhand_of(&self, other: &PropertyId) -> bool {
+ match *self {
+ PropertyDeclarationId::Longhand(id) => match *other {
+ PropertyId::NonCustom(non_custom_id) => id.is_or_is_longhand_of(non_custom_id),
+ PropertyId::Custom(_) => false,
+ },
+ PropertyDeclarationId::Custom(name) => {
+ matches!(*other, PropertyId::Custom(ref other_name) if name == other_name)
+ },
+ }
+ }
+
+ /// Whether a given declaration id is a longhand belonging to this
+ /// shorthand.
+ pub fn is_longhand_of(&self, shorthand: ShorthandId) -> bool {
+ match *self {
+ PropertyDeclarationId::Longhand(ref id) => id.is_longhand_of(shorthand),
+ _ => false,
+ }
+ }
+
+ /// Returns the name of the property without CSS escaping.
+ pub fn name(&self) -> Cow<'static, str> {
+ match *self {
+ PropertyDeclarationId::Longhand(id) => id.name().into(),
+ PropertyDeclarationId::Custom(name) => {
+ let mut s = String::new();
+ write!(&mut s, "--{}", name).unwrap();
+ s.into()
+ },
+ }
+ }
+
+ /// Returns longhand id if it is, None otherwise.
+ #[inline]
+ pub fn as_longhand(&self) -> Option<LonghandId> {
+ match *self {
+ PropertyDeclarationId::Longhand(id) => Some(id),
+ _ => None,
+ }
+ }
+
+ /// Return whether this property is logical.
+ #[inline]
+ pub fn is_logical(&self) -> bool {
+ match self {
+ PropertyDeclarationId::Longhand(id) => id.is_logical(),
+ PropertyDeclarationId::Custom(_) => false,
+ }
+ }
+
+ /// If this is a logical property, return the corresponding physical one in
+ /// the given writing mode.
+ ///
+ /// Otherwise, return unchanged.
+ #[inline]
+ pub fn to_physical(&self, wm: WritingMode) -> Self {
+ match self {
+ Self::Longhand(id) => Self::Longhand(id.to_physical(wm)),
+ Self::Custom(_) => self.clone(),
+ }
+ }
+
+ /// Returns whether this property is animatable.
+ #[inline]
+ pub fn is_animatable(&self) -> bool {
+ match self {
+ Self::Longhand(id) => id.is_animatable(),
+ Self::Custom(_) => true,
+ }
+ }
+
+ /// Returns whether this property is animatable in a discrete way.
+ #[inline]
+ pub fn is_discrete_animatable(&self) -> bool {
+ match self {
+ Self::Longhand(longhand) => longhand.is_discrete_animatable(),
+ // TODO(bug 1846516): Refine this?
+ Self::Custom(_) => true,
+ }
+ }
+
+ /// Converts from a to an adequate nsCSSPropertyID, returning
+ /// eCSSPropertyExtra_variable for custom properties.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn to_nscsspropertyid(self) -> nsCSSPropertyID {
+ match self {
+ PropertyDeclarationId::Longhand(id) => id.to_nscsspropertyid(),
+ PropertyDeclarationId::Custom(_) => nsCSSPropertyID::eCSSPropertyExtra_variable,
+ }
+ }
+
+ /// Convert a `PropertyDeclarationId` into an `AnimatedPropertyID`
+ /// Note that the rust AnimatedPropertyID doesn't implement Drop, so owned controls whether the
+ /// custom name should be addrefed or not.
+ ///
+ /// FIXME(emilio, bug 1870107): This is a bit error-prone. We should consider using cbindgen to
+ /// generate the property id representation or so.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn to_gecko_animated_property_id(&self, owned: bool) -> AnimatedPropertyID {
+ match self {
+ Self::Longhand(id) => AnimatedPropertyID {
+ mID: id.to_nscsspropertyid(),
+ mCustomName: RefPtr::null(),
+ },
+ Self::Custom(name) => {
+ let mut property_id = AnimatedPropertyID {
+ mID: nsCSSPropertyID::eCSSPropertyExtra_variable,
+ mCustomName: RefPtr::null(),
+ };
+ property_id.mCustomName.mRawPtr = if owned {
+ (*name).clone().into_addrefed()
+ } else {
+ name.as_ptr()
+ };
+ property_id
+ },
+ }
+ }
+}
+
+/// A set of all properties.
+#[derive(Clone, PartialEq, Default)]
+pub struct NonCustomPropertyIdSet {
+ storage: [u32; ((property_counts::NON_CUSTOM as usize) - 1 + 32) / 32],
+}
+
+impl NonCustomPropertyIdSet {
+ /// Creates an empty `NonCustomPropertyIdSet`.
+ pub fn new() -> Self {
+ Self {
+ storage: Default::default(),
+ }
+ }
+
+ /// Insert a non-custom-property in the set.
+ #[inline]
+ pub fn insert(&mut self, id: NonCustomPropertyId) {
+ let bit = id.0 as usize;
+ self.storage[bit / 32] |= 1 << (bit % 32);
+ }
+
+ /// Return whether the given property is in the set
+ #[inline]
+ pub fn contains(&self, id: NonCustomPropertyId) -> bool {
+ let bit = id.0 as usize;
+ (self.storage[bit / 32] & (1 << (bit % 32))) != 0
+ }
+}
+
+/// A set of longhand properties
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq)]
+pub struct LonghandIdSet {
+ storage: [u32; ((property_counts::LONGHANDS as usize) - 1 + 32) / 32],
+}
+
+to_shmem::impl_trivial_to_shmem!(LonghandIdSet);
+
+impl LonghandIdSet {
+ /// Return an empty LonghandIdSet.
+ #[inline]
+ pub fn new() -> Self {
+ Self {
+ storage: Default::default(),
+ }
+ }
+
+ /// Iterate over the current longhand id set.
+ pub fn iter(&self) -> LonghandIdSetIterator {
+ LonghandIdSetIterator {
+ longhands: self,
+ cur: 0,
+ }
+ }
+
+ /// Returns whether this set contains at least every longhand that `other`
+ /// also contains.
+ pub fn contains_all(&self, other: &Self) -> bool {
+ for (self_cell, other_cell) in self.storage.iter().zip(other.storage.iter()) {
+ if (*self_cell & *other_cell) != *other_cell {
+ return false;
+ }
+ }
+ true
+ }
+
+ /// Returns whether this set contains any longhand that `other` also contains.
+ pub fn contains_any(&self, other: &Self) -> bool {
+ for (self_cell, other_cell) in self.storage.iter().zip(other.storage.iter()) {
+ if (*self_cell & *other_cell) != 0 {
+ return true;
+ }
+ }
+ false
+ }
+
+ /// Remove all the given properties from the set.
+ #[inline]
+ pub fn remove_all(&mut self, other: &Self) {
+ for (self_cell, other_cell) in self.storage.iter_mut().zip(other.storage.iter()) {
+ *self_cell &= !*other_cell;
+ }
+ }
+
+ /// Return whether the given property is in the set
+ #[inline]
+ pub fn contains(&self, id: LonghandId) -> bool {
+ let bit = id as usize;
+ (self.storage[bit / 32] & (1 << (bit % 32))) != 0
+ }
+
+ /// Return whether this set contains any reset longhand.
+ #[inline]
+ pub fn contains_any_reset(&self) -> bool {
+ self.contains_any(Self::reset())
+ }
+
+ /// Add the given property to the set
+ #[inline]
+ pub fn insert(&mut self, id: LonghandId) {
+ let bit = id as usize;
+ self.storage[bit / 32] |= 1 << (bit % 32);
+ }
+
+ /// Remove the given property from the set
+ #[inline]
+ pub fn remove(&mut self, id: LonghandId) {
+ let bit = id as usize;
+ self.storage[bit / 32] &= !(1 << (bit % 32));
+ }
+
+ /// Clear all bits
+ #[inline]
+ pub fn clear(&mut self) {
+ for cell in &mut self.storage {
+ *cell = 0
+ }
+ }
+
+ /// Returns whether the set is empty.
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.storage.iter().all(|c| *c == 0)
+ }
+}
+
+/// An iterator over a set of longhand ids.
+pub struct LonghandIdSetIterator<'a> {
+ longhands: &'a LonghandIdSet,
+ cur: usize,
+}
+
+impl<'a> Iterator for LonghandIdSetIterator<'a> {
+ type Item = LonghandId;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ if self.cur >= property_counts::LONGHANDS {
+ return None;
+ }
+
+ let id: LonghandId = unsafe { mem::transmute(self.cur as u16) };
+ self.cur += 1;
+
+ if self.longhands.contains(id) {
+ return Some(id);
+ }
+ }
+ }
+}
+
+/// An ArrayVec of subproperties, contains space for the longest shorthand except all.
+pub type SubpropertiesVec<T> = ArrayVec<T, { property_counts::MAX_SHORTHAND_EXPANDED }>;
+
+/// A stack-allocated vector of `PropertyDeclaration`
+/// large enough to parse one CSS `key: value` declaration.
+/// (Shorthands expand to multiple `PropertyDeclaration`s.)
+#[derive(Default)]
+pub struct SourcePropertyDeclaration {
+ /// The storage for the actual declarations (except for all).
+ pub declarations: SubpropertiesVec<PropertyDeclaration>,
+ /// Stored separately to keep SubpropertiesVec smaller.
+ pub all_shorthand: AllShorthand,
+}
+
+// This is huge, but we allocate it on the stack and then never move it,
+// we only pass `&mut SourcePropertyDeclaration` references around.
+size_of_test!(SourcePropertyDeclaration, 632);
+
+impl SourcePropertyDeclaration {
+ /// Create one with a single PropertyDeclaration.
+ #[inline]
+ pub fn with_one(decl: PropertyDeclaration) -> Self {
+ let mut result = Self::default();
+ result.declarations.push(decl);
+ result
+ }
+
+ /// Similar to Vec::drain: leaves this empty when the return value is dropped.
+ pub fn drain(&mut self) -> SourcePropertyDeclarationDrain {
+ SourcePropertyDeclarationDrain {
+ declarations: self.declarations.drain(..),
+ all_shorthand: mem::replace(&mut self.all_shorthand, AllShorthand::NotSet),
+ }
+ }
+
+ /// Reset to initial state
+ pub fn clear(&mut self) {
+ self.declarations.clear();
+ self.all_shorthand = AllShorthand::NotSet;
+ }
+
+ /// Whether we're empty.
+ pub fn is_empty(&self) -> bool {
+ self.declarations.is_empty() && matches!(self.all_shorthand, AllShorthand::NotSet)
+ }
+
+ /// Push a single declaration.
+ pub fn push(&mut self, declaration: PropertyDeclaration) {
+ let _result = self.declarations.try_push(declaration);
+ debug_assert!(_result.is_ok());
+ }
+}
+
+/// Return type of SourcePropertyDeclaration::drain
+pub struct SourcePropertyDeclarationDrain<'a> {
+ /// A drain over the non-all declarations.
+ pub declarations:
+ ArrayVecDrain<'a, PropertyDeclaration, { property_counts::MAX_SHORTHAND_EXPANDED }>,
+ /// The all shorthand that was set.
+ pub all_shorthand: AllShorthand,
+}
+
+/// An unparsed property value that contains `var()` functions.
+#[derive(Debug, Eq, PartialEq, ToShmem)]
+pub struct UnparsedValue {
+ /// The variable value, references and so on.
+ pub(super) variable_value: custom_properties::VariableValue,
+ /// The shorthand this came from.
+ from_shorthand: Option<ShorthandId>,
+}
+
+impl ToCss for UnparsedValue {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ // https://drafts.csswg.org/css-variables/#variables-in-shorthands
+ if self.from_shorthand.is_none() {
+ self.variable_value.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// A simple cache for properties that come from a shorthand and have variable
+/// references.
+///
+/// This cache works because of the fact that you can't have competing values
+/// for a given longhand coming from the same shorthand (but note that this is
+/// why the shorthand needs to be part of the cache key).
+pub type ShorthandsWithPropertyReferencesCache =
+ FxHashMap<(ShorthandId, LonghandId), PropertyDeclaration>;
+
+impl UnparsedValue {
+ fn substitute_variables<'cache>(
+ &self,
+ longhand_id: LonghandId,
+ custom_properties: &ComputedCustomProperties,
+ stylist: &Stylist,
+ computed_context: &computed::Context,
+ shorthand_cache: &'cache mut ShorthandsWithPropertyReferencesCache,
+ ) -> Cow<'cache, PropertyDeclaration> {
+ let invalid_at_computed_value_time = || {
+ let keyword = if longhand_id.inherited() {
+ CSSWideKeyword::Inherit
+ } else {
+ CSSWideKeyword::Initial
+ };
+ Cow::Owned(PropertyDeclaration::css_wide_keyword(longhand_id, keyword))
+ };
+
+ if computed_context
+ .builder
+ .invalid_non_custom_properties
+ .contains(longhand_id)
+ {
+ return invalid_at_computed_value_time();
+ }
+
+ if let Some(shorthand_id) = self.from_shorthand {
+ let key = (shorthand_id, longhand_id);
+ if shorthand_cache.contains_key(&key) {
+ // FIXME: This double lookup should be avoidable, but rustc
+ // doesn't like that, see:
+ //
+ // https://github.com/rust-lang/rust/issues/82146
+ return Cow::Borrowed(&shorthand_cache[&key]);
+ }
+ }
+
+ let css = match custom_properties::substitute(
+ &self.variable_value,
+ custom_properties,
+ stylist,
+ computed_context,
+ ) {
+ Ok(css) => css,
+ Err(..) => return invalid_at_computed_value_time(),
+ };
+
+ // As of this writing, only the base URL is used for property
+ // values.
+ //
+ // NOTE(emilio): we intentionally pase `None` as the rule type here.
+ // If something starts depending on it, it's probably a bug, since
+ // it'd change how values are parsed depending on whether we're in a
+ // @keyframes rule or not, for example... So think twice about
+ // whether you want to do this!
+ //
+ // FIXME(emilio): ParsingMode is slightly fishy...
+ let context = ParserContext::new(
+ Origin::Author,
+ &self.variable_value.url_data,
+ None,
+ ParsingMode::DEFAULT,
+ computed_context.quirks_mode,
+ /* namespaces = */ Default::default(),
+ None,
+ None,
+ );
+
+ let mut input = ParserInput::new(&css);
+ let mut input = Parser::new(&mut input);
+ input.skip_whitespace();
+
+ if let Ok(keyword) = input.try_parse(CSSWideKeyword::parse) {
+ return Cow::Owned(PropertyDeclaration::css_wide_keyword(longhand_id, keyword));
+ }
+
+ let shorthand = match self.from_shorthand {
+ None => {
+ return match input.parse_entirely(|input| longhand_id.parse_value(&context, input))
+ {
+ Ok(decl) => Cow::Owned(decl),
+ Err(..) => invalid_at_computed_value_time(),
+ }
+ },
+ Some(shorthand) => shorthand,
+ };
+
+ let mut decls = SourcePropertyDeclaration::default();
+ // parse_into takes care of doing `parse_entirely` for us.
+ if shorthand
+ .parse_into(&mut decls, &context, &mut input)
+ .is_err()
+ {
+ return invalid_at_computed_value_time();
+ }
+
+ for declaration in decls.declarations.drain(..) {
+ let longhand = declaration.id().as_longhand().unwrap();
+ if longhand.is_logical() {
+ let writing_mode = computed_context.builder.writing_mode;
+ shorthand_cache.insert(
+ (shorthand, longhand.to_physical(writing_mode)),
+ declaration.clone(),
+ );
+ }
+ shorthand_cache.insert((shorthand, longhand), declaration);
+ }
+
+ let key = (shorthand, longhand_id);
+ match shorthand_cache.get(&key) {
+ Some(decl) => Cow::Borrowed(decl),
+ None => {
+ // FIXME: We should always have the key here but it seems
+ // sometimes we don't, see bug 1696409.
+ #[cfg(feature = "gecko")]
+ {
+ if crate::gecko_bindings::structs::GECKO_IS_NIGHTLY {
+ panic!("Expected {:?} to be in the cache but it was not!", key);
+ }
+ }
+ invalid_at_computed_value_time()
+ },
+ }
+ }
+}
+/// A parsed all-shorthand value.
+pub enum AllShorthand {
+ /// Not present.
+ NotSet,
+ /// A CSS-wide keyword.
+ CSSWideKeyword(CSSWideKeyword),
+ /// An all shorthand with var() references that we can't resolve right now.
+ WithVariables(Arc<UnparsedValue>),
+}
+
+impl Default for AllShorthand {
+ fn default() -> Self {
+ Self::NotSet
+ }
+}
+
+impl AllShorthand {
+ /// Iterates property declarations from the given all shorthand value.
+ #[inline]
+ pub fn declarations(&self) -> AllShorthandDeclarationIterator {
+ AllShorthandDeclarationIterator {
+ all_shorthand: self,
+ longhands: ShorthandId::All.longhands(),
+ }
+ }
+}
+
+/// An iterator over the all shorthand's shorthand declarations.
+pub struct AllShorthandDeclarationIterator<'a> {
+ all_shorthand: &'a AllShorthand,
+ longhands: NonCustomPropertyIterator<LonghandId>,
+}
+
+impl<'a> Iterator for AllShorthandDeclarationIterator<'a> {
+ type Item = PropertyDeclaration;
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ match *self.all_shorthand {
+ AllShorthand::NotSet => None,
+ AllShorthand::CSSWideKeyword(ref keyword) => Some(
+ PropertyDeclaration::css_wide_keyword(self.longhands.next()?, *keyword),
+ ),
+ AllShorthand::WithVariables(ref unparsed) => {
+ Some(PropertyDeclaration::WithVariables(VariableDeclaration {
+ id: self.longhands.next()?,
+ value: unparsed.clone(),
+ }))
+ },
+ }
+ }
+}
+
+/// An iterator over all the property ids that are enabled for a given
+/// shorthand, if that shorthand is enabled for all content too.
+pub struct NonCustomPropertyIterator<Item: 'static> {
+ filter: bool,
+ iter: std::slice::Iter<'static, Item>,
+}
+
+impl<Item> Iterator for NonCustomPropertyIterator<Item>
+where
+ Item: 'static + Copy + Into<NonCustomPropertyId>,
+{
+ type Item = Item;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ let id = *self.iter.next()?;
+ if !self.filter || id.into().enabled_for_all_content() {
+ return Some(id);
+ }
+ }
+ }
+}
diff --git a/servo/components/style/properties/properties.html.mako b/servo/components/style/properties/properties.html.mako
new file mode 100644
index 0000000000..5c51593517
--- /dev/null
+++ b/servo/components/style/properties/properties.html.mako
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Supported CSS properties in Servo</title>
+ <link rel="stylesheet" type="text/css" href="../normalize.css">
+ <link rel="stylesheet" type="text/css" href="../rustdoc.css">
+ <link rel="stylesheet" type="text/css" href="../light.css">
+</head>
+<body class="rustdoc">
+ <section id='main' class="content mod">
+ <h1 class='fqn'><span class='in-band'>CSS properties currently supported in Servo</span></h1>
+ % for kind, props in sorted(properties.items()):
+ <h2>${kind.capitalize()}</h2>
+ <table>
+ <tr>
+ <th>Name</th>
+ <th>Pref</th>
+ </tr>
+ % for name, data in sorted(props.items()):
+ <tr>
+ <td><code>${name}</code></td>
+ <td><code>${data['pref'] or ''}</code></td>
+ </tr>
+ % endfor
+ </table>
+ % endfor
+ </section>
+</body>
+</html>
diff --git a/servo/components/style/properties/properties.mako.rs b/servo/components/style/properties/properties.mako.rs
new file mode 100644
index 0000000000..b08314d7d5
--- /dev/null
+++ b/servo/components/style/properties/properties.mako.rs
@@ -0,0 +1,2958 @@
+/* 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/. */
+
+// This file is a Mako template: http://www.makotemplates.org/
+
+// Please note that valid Rust syntax may be mangled by the Mako parser.
+// For example, Vec<&Foo> will be mangled as Vec&Foo>. To work around these issues, the code
+// can be escaped. In the above example, Vec<<&Foo> or Vec< &Foo> achieves the desired result of Vec<&Foo>.
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+use app_units::Au;
+use servo_arc::{Arc, UniqueArc};
+use std::{ops, ptr};
+use std::{fmt, mem};
+
+#[cfg(feature = "servo")] use euclid::SideOffsets2D;
+#[cfg(feature = "gecko")] use crate::gecko_bindings::structs::{self, nsCSSPropertyID};
+#[cfg(feature = "servo")] use crate::logical_geometry::LogicalMargin;
+#[cfg(feature = "servo")] use crate::computed_values;
+use crate::logical_geometry::WritingMode;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use crate::computed_value_flags::*;
+use cssparser::Parser;
+use crate::media_queries::Device;
+use crate::parser::ParserContext;
+use crate::selector_parser::PseudoElement;
+use crate::stylist::Stylist;
+#[cfg(feature = "servo")] use servo_config::prefs;
+use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+use crate::stylesheets::{CssRuleType, CssRuleTypes, Origin};
+use crate::logical_geometry::{LogicalAxis, LogicalCorner, LogicalSide};
+use crate::use_counters::UseCounters;
+use crate::rule_tree::StrongRuleNode;
+use crate::str::CssStringWriter;
+use crate::values::{
+ computed,
+ resolved,
+ specified::{font::SystemFont, length::LineHeightBase},
+};
+use std::cell::Cell;
+use super::{
+ PropertyDeclarationId, PropertyId, NonCustomPropertyId,
+ NonCustomPropertyIdSet, PropertyFlags, SourcePropertyDeclaration,
+ LonghandIdSet, VariableDeclaration, CustomDeclaration,
+ WideKeywordDeclaration, NonCustomPropertyIterator,
+};
+
+<%!
+ from collections import defaultdict
+ from data import Method, PropertyRestrictions, Keyword, to_rust_ident, \
+ to_camel_case, RULE_VALUES, SYSTEM_FONT_LONGHANDS, PRIORITARY_PROPERTIES
+ import os.path
+%>
+
+/// Conversion with fewer impls than From/Into
+pub trait MaybeBoxed<Out> {
+ /// Convert
+ fn maybe_boxed(self) -> Out;
+}
+
+impl<T> MaybeBoxed<T> for T {
+ #[inline]
+ fn maybe_boxed(self) -> T { self }
+}
+
+impl<T> MaybeBoxed<Box<T>> for T {
+ #[inline]
+ fn maybe_boxed(self) -> Box<T> { Box::new(self) }
+}
+
+macro_rules! expanded {
+ ( $( $name: ident: $value: expr ),+ ) => {
+ expanded!( $( $name: $value, )+ )
+ };
+ ( $( $name: ident: $value: expr, )+ ) => {
+ Longhands {
+ $(
+ $name: MaybeBoxed::maybe_boxed($value),
+ )+
+ }
+ }
+}
+
+/// A module with all the code for longhand properties.
+#[allow(missing_docs)]
+pub mod longhands {
+ % for style_struct in data.style_structs:
+ include!("${repr(os.path.join(OUT_DIR, 'longhands/{}.rs'.format(style_struct.name_lower)))[1:-1]}");
+ % endfor
+}
+
+macro_rules! unwrap_or_initial {
+ ($prop: ident) => (unwrap_or_initial!($prop, $prop));
+ ($prop: ident, $expr: expr) =>
+ ($expr.unwrap_or_else(|| $prop::get_initial_specified_value()));
+}
+
+/// A module with code for all the shorthand css properties, and a few
+/// serialization helpers.
+#[allow(missing_docs)]
+pub mod shorthands {
+ use cssparser::Parser;
+ use crate::parser::{Parse, ParserContext};
+ use style_traits::{ParseError, StyleParseErrorKind};
+ use crate::values::specified;
+
+ % for style_struct in data.style_structs:
+ include!("${repr(os.path.join(OUT_DIR, 'shorthands/{}.rs'.format(style_struct.name_lower)))[1:-1]}");
+ % endfor
+
+ // We didn't define the 'all' shorthand using the regular helpers:shorthand
+ // mechanism, since it causes some very large types to be generated.
+ //
+ // Also, make sure logical properties appear before its physical
+ // counter-parts, in order to prevent bugs like:
+ //
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1410028
+ //
+ // FIXME(emilio): Adopt the resolution from:
+ //
+ // https://github.com/w3c/csswg-drafts/issues/1898
+ //
+ // when there is one, whatever that is.
+ <%
+ logical_longhands = []
+ other_longhands = []
+
+ for p in data.longhands:
+ if p.name in ['direction', 'unicode-bidi']:
+ continue;
+ if not p.enabled_in_content() and not p.experimental(engine):
+ continue;
+ if "Style" not in p.rule_types_allowed_names():
+ continue;
+ if p.logical:
+ logical_longhands.append(p.name)
+ else:
+ other_longhands.append(p.name)
+
+ data.declare_shorthand(
+ "all",
+ logical_longhands + other_longhands,
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-cascade-3/#all-shorthand"
+ )
+ ALL_SHORTHAND_LEN = len(logical_longhands) + len(other_longhands);
+ %>
+}
+
+<%
+ from itertools import groupby
+
+ # After this code, `data.longhands` is sorted in the following order:
+ # - first all keyword variants and all variants known to be Copy,
+ # - second all the other variants, such as all variants with the same field
+ # have consecutive discriminants.
+ # The variable `variants` contain the same entries as `data.longhands` in
+ # the same order, but must exist separately to the data source, because
+ # we then need to add three additional variants `WideKeywordDeclaration`,
+ # `VariableDeclaration` and `CustomDeclaration`.
+
+ variants = []
+ for property in data.longhands:
+ variants.append({
+ "name": property.camel_case,
+ "type": property.specified_type(),
+ "doc": "`" + property.name + "`",
+ "copy": property.specified_is_copy(),
+ })
+
+ groups = {}
+ keyfunc = lambda x: x["type"]
+ sortkeys = {}
+ for ty, group in groupby(sorted(variants, key=keyfunc), keyfunc):
+ group = list(group)
+ groups[ty] = group
+ for v in group:
+ if len(group) == 1:
+ sortkeys[v["name"]] = (not v["copy"], 1, v["name"], "")
+ else:
+ sortkeys[v["name"]] = (not v["copy"], len(group), ty, v["name"])
+ variants.sort(key=lambda x: sortkeys[x["name"]])
+
+ # It is extremely important to sort the `data.longhands` array here so
+ # that it is in the same order as `variants`, for `LonghandId` and
+ # `PropertyDeclarationId` to coincide.
+ data.longhands.sort(key=lambda x: sortkeys[x.camel_case])
+%>
+
+// WARNING: It is *really* important for the variants of `LonghandId`
+// and `PropertyDeclaration` to be defined in the exact same order,
+// with the exception of `CSSWideKeyword`, `WithVariables` and `Custom`,
+// which don't exist in `LonghandId`.
+
+<%
+ extra_variants = [
+ {
+ "name": "CSSWideKeyword",
+ "type": "WideKeywordDeclaration",
+ "doc": "A CSS-wide keyword.",
+ "copy": False,
+ },
+ {
+ "name": "WithVariables",
+ "type": "VariableDeclaration",
+ "doc": "An unparsed declaration.",
+ "copy": False,
+ },
+ {
+ "name": "Custom",
+ "type": "CustomDeclaration",
+ "doc": "A custom property declaration.",
+ "copy": False,
+ },
+ ]
+ for v in extra_variants:
+ variants.append(v)
+ groups[v["type"]] = [v]
+%>
+
+/// Servo's representation for a property declaration.
+#[derive(ToShmem)]
+#[repr(u16)]
+pub enum PropertyDeclaration {
+ % for variant in variants:
+ /// ${variant["doc"]}
+ ${variant["name"]}(${variant["type"]}),
+ % endfor
+}
+
+// There's one of these for each parsed declaration so it better be small.
+size_of_test!(PropertyDeclaration, 32);
+
+#[repr(C)]
+struct PropertyDeclarationVariantRepr<T> {
+ tag: u16,
+ value: T
+}
+
+impl Clone for PropertyDeclaration {
+ #[inline]
+ fn clone(&self) -> Self {
+ use self::PropertyDeclaration::*;
+
+ <%
+ [copy, others] = [list(g) for _, g in groupby(variants, key=lambda x: not x["copy"])]
+ %>
+
+ let self_tag = unsafe {
+ (*(self as *const _ as *const PropertyDeclarationVariantRepr<()>)).tag
+ };
+ if self_tag <= LonghandId::${copy[-1]["name"]} as u16 {
+ #[derive(Clone, Copy)]
+ #[repr(u16)]
+ enum CopyVariants {
+ % for v in copy:
+ _${v["name"]}(${v["type"]}),
+ % endfor
+ }
+
+ unsafe {
+ let mut out = mem::MaybeUninit::uninit();
+ ptr::write(
+ out.as_mut_ptr() as *mut CopyVariants,
+ *(self as *const _ as *const CopyVariants),
+ );
+ return out.assume_init();
+ }
+ }
+
+ // This function ensures that all properties not handled above
+ // do not have a specified value implements Copy. If you hit
+ // compile error here, you may want to add the type name into
+ // Longhand.specified_is_copy in data.py.
+ fn _static_assert_others_are_not_copy() {
+ struct Helper<T>(T);
+ trait AssertCopy { fn assert() {} }
+ trait AssertNotCopy { fn assert() {} }
+ impl<T: Copy> AssertCopy for Helper<T> {}
+ % for ty in sorted(set(x["type"] for x in others)):
+ impl AssertNotCopy for Helper<${ty}> {}
+ Helper::<${ty}>::assert();
+ % endfor
+ }
+
+ match *self {
+ ${" |\n".join("{}(..)".format(v["name"]) for v in copy)} => {
+ unsafe { debug_unreachable!() }
+ }
+ % for ty, vs in groupby(others, key=lambda x: x["type"]):
+ <%
+ vs = list(vs)
+ %>
+ % if len(vs) == 1:
+ ${vs[0]["name"]}(ref value) => {
+ ${vs[0]["name"]}(value.clone())
+ }
+ % else:
+ ${" |\n".join("{}(ref value)".format(v["name"]) for v in vs)} => {
+ unsafe {
+ let mut out = mem::MaybeUninit::uninit();
+ ptr::write(
+ out.as_mut_ptr() as *mut PropertyDeclarationVariantRepr<${ty}>,
+ PropertyDeclarationVariantRepr {
+ tag: *(self as *const _ as *const u16),
+ value: value.clone(),
+ },
+ );
+ out.assume_init()
+ }
+ }
+ % endif
+ % endfor
+ }
+ }
+}
+
+impl PartialEq for PropertyDeclaration {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ use self::PropertyDeclaration::*;
+
+ unsafe {
+ let this_repr =
+ &*(self as *const _ as *const PropertyDeclarationVariantRepr<()>);
+ let other_repr =
+ &*(other as *const _ as *const PropertyDeclarationVariantRepr<()>);
+ if this_repr.tag != other_repr.tag {
+ return false;
+ }
+ match *self {
+ % for ty, vs in groupby(variants, key=lambda x: x["type"]):
+ ${" |\n".join("{}(ref this)".format(v["name"]) for v in vs)} => {
+ let other_repr =
+ &*(other as *const _ as *const PropertyDeclarationVariantRepr<${ty}>);
+ *this == other_repr.value
+ }
+ % endfor
+ }
+ }
+ }
+}
+
+impl MallocSizeOf for PropertyDeclaration {
+ #[inline]
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ use self::PropertyDeclaration::*;
+
+ match *self {
+ % for ty, vs in groupby(variants, key=lambda x: x["type"]):
+ ${" | ".join("{}(ref value)".format(v["name"]) for v in vs)} => {
+ value.size_of(ops)
+ }
+ % endfor
+ }
+ }
+}
+
+
+impl PropertyDeclaration {
+ /// Returns the given value for this declaration as a particular type.
+ /// It's the caller's responsibility to guarantee that the longhand id has the right specified
+ /// value representation.
+ pub(crate) unsafe fn unchecked_value_as<T>(&self) -> &T {
+ &(*(self as *const _ as *const PropertyDeclarationVariantRepr<T>)).value
+ }
+
+ /// Dumps the property declaration before crashing.
+ #[cold]
+ #[cfg(debug_assertions)]
+ pub(crate) fn debug_crash(&self, reason: &str) {
+ panic!("{}: {:?}", reason, self);
+ }
+ #[cfg(not(debug_assertions))]
+ #[inline(always)]
+ pub(crate) fn debug_crash(&self, _reason: &str) {}
+
+ /// Returns whether this is a variant of the Longhand(Value) type, rather
+ /// than one of the special variants in extra_variants.
+ fn is_longhand_value(&self) -> bool {
+ match *self {
+ % for v in extra_variants:
+ PropertyDeclaration::${v["name"]}(..) => false,
+ % endfor
+ _ => true,
+ }
+ }
+
+ /// Like the method on ToCss, but without the type parameter to avoid
+ /// accidentally monomorphizing this large function multiple times for
+ /// different writers.
+ pub fn to_css(&self, dest: &mut CssStringWriter) -> fmt::Result {
+ use self::PropertyDeclaration::*;
+
+ let mut dest = CssWriter::new(dest);
+ match *self {
+ % for ty, vs in groupby(variants, key=lambda x: x["type"]):
+ ${" | ".join("{}(ref value)".format(v["name"]) for v in vs)} => {
+ value.to_css(&mut dest)
+ }
+ % endfor
+ }
+ }
+
+ /// Returns the color value of a given property, for high-contrast-mode tweaks.
+ pub(super) fn color_value(&self) -> Option<<&crate::values::specified::Color> {
+ ${static_longhand_id_set("COLOR_PROPERTIES", lambda p: p.predefined_type == "Color")}
+ <%
+ # sanity check
+ assert data.longhands_by_name["background-color"].predefined_type == "Color"
+
+ color_specified_type = data.longhands_by_name["background-color"].specified_type()
+ %>
+ let id = self.id().as_longhand()?;
+ if !COLOR_PROPERTIES.contains(id) || !self.is_longhand_value() {
+ return None;
+ }
+ let repr = self as *const _ as *const PropertyDeclarationVariantRepr<${color_specified_type}>;
+ Some(unsafe { &(*repr).value })
+ }
+}
+
+/// A module with all the code related to animated properties.
+///
+/// This needs to be "included" by mako at least after all longhand modules,
+/// given they populate the global data.
+pub mod animated_properties {
+ <%include file="/helpers/animated_properties.mako.rs" />
+}
+
+/// A module to group various interesting property counts.
+pub mod property_counts {
+ /// The number of (non-alias) longhand properties.
+ pub const LONGHANDS: usize = ${len(data.longhands)};
+ /// The number of (non-alias) shorthand properties.
+ pub const SHORTHANDS: usize = ${len(data.shorthands)};
+ /// The number of aliases.
+ pub const ALIASES: usize = ${len(data.all_aliases())};
+ /// The number of counted unknown properties.
+ pub const COUNTED_UNKNOWN: usize = ${len(data.counted_unknown_properties)};
+ /// The number of (non-alias) longhands and shorthands.
+ pub const LONGHANDS_AND_SHORTHANDS: usize = LONGHANDS + SHORTHANDS;
+ /// The number of non-custom properties.
+ pub const NON_CUSTOM: usize = LONGHANDS_AND_SHORTHANDS + ALIASES;
+ /// The number of prioritary properties that we have.
+ pub const PRIORITARY: usize = ${len(PRIORITARY_PROPERTIES)};
+ /// The max number of longhands that a shorthand other than "all" expands to.
+ pub const MAX_SHORTHAND_EXPANDED: usize =
+ ${max(len(s.sub_properties) for s in data.shorthands_except_all())};
+ /// The max amount of longhands that the `all` shorthand will ever contain.
+ pub const ALL_SHORTHAND_EXPANDED: usize = ${ALL_SHORTHAND_LEN};
+ /// The number of animatable properties.
+ pub const ANIMATABLE: usize = ${sum(1 for prop in data.longhands if prop.animatable)};
+}
+
+% if engine == "gecko":
+#[allow(dead_code)]
+unsafe fn static_assert_nscsspropertyid() {
+ % for i, property in enumerate(data.longhands + data.shorthands + data.all_aliases()):
+ std::mem::transmute::<[u8; ${i}], [u8; ${property.nscsspropertyid()} as usize]>([0; ${i}]); // ${property.name}
+ % endfor
+}
+% endif
+
+impl NonCustomPropertyId {
+ /// Get the property name.
+ #[inline]
+ pub fn name(self) -> &'static str {
+ static MAP: [&'static str; property_counts::NON_CUSTOM] = [
+ % for property in data.longhands + data.shorthands + data.all_aliases():
+ "${property.name}",
+ % endfor
+ ];
+ MAP[self.0 as usize]
+ }
+
+ /// Returns whether this property is animatable.
+ #[inline]
+ pub fn is_animatable(self) -> bool {
+ ${static_non_custom_property_id_set("ANIMATABLE", lambda p: p.animatable)}
+ ANIMATABLE.contains(self)
+ }
+
+ /// Whether this property is enabled for all content right now.
+ #[inline]
+ pub(super) fn enabled_for_all_content(self) -> bool {
+ ${static_non_custom_property_id_set(
+ "EXPERIMENTAL",
+ lambda p: p.experimental(engine)
+ )}
+
+ ${static_non_custom_property_id_set(
+ "ALWAYS_ENABLED",
+ lambda p: (not p.experimental(engine)) and p.enabled_in_content()
+ )}
+
+ let passes_pref_check = || {
+ % if engine == "gecko":
+ unsafe { structs::nsCSSProps_gPropertyEnabled[self.0 as usize] }
+ % else:
+ static PREF_NAME: [Option< &str>; ${
+ len(data.longhands) + len(data.shorthands) + len(data.all_aliases())
+ }] = [
+ % for property in data.longhands + data.shorthands + data.all_aliases():
+ <%
+ attrs = {"servo-2013": "servo_2013_pref", "servo-2020": "servo_2020_pref"}
+ pref = getattr(property, attrs[engine])
+ %>
+ % if pref:
+ Some("${pref}"),
+ % else:
+ None,
+ % endif
+ % endfor
+ ];
+ let pref = match PREF_NAME[self.0 as usize] {
+ None => return true,
+ Some(pref) => pref,
+ };
+
+ prefs::pref_map().get(pref).as_bool().unwrap_or(false)
+ % endif
+ };
+
+ if ALWAYS_ENABLED.contains(self) {
+ return true
+ }
+
+ if EXPERIMENTAL.contains(self) && passes_pref_check() {
+ return true
+ }
+
+ false
+ }
+
+ /// Returns whether a given rule allows a given property.
+ #[inline]
+ pub fn allowed_in_rule(self, rule_types: CssRuleTypes) -> bool {
+ debug_assert!(
+ rule_types.contains(CssRuleType::Keyframe) ||
+ rule_types.contains(CssRuleType::Page) ||
+ rule_types.contains(CssRuleType::Style),
+ "Declarations are only expected inside a keyframe, page, or style rule."
+ );
+
+ static MAP: [u32; property_counts::NON_CUSTOM] = [
+ % for property in data.longhands + data.shorthands + data.all_aliases():
+ % for name in RULE_VALUES:
+ % if property.rule_types_allowed & RULE_VALUES[name] != 0:
+ CssRuleType::${name}.bit() |
+ % endif
+ % endfor
+ 0,
+ % endfor
+ ];
+ MAP[self.0 as usize] & rule_types.bits() != 0
+ }
+
+ pub(super) fn allowed_in(self, context: &ParserContext) -> bool {
+ if !self.allowed_in_rule(context.rule_types()) {
+ return false;
+ }
+
+ self.allowed_in_ignoring_rule_type(context)
+ }
+
+
+ pub(super) fn allowed_in_ignoring_rule_type(self, context: &ParserContext) -> bool {
+ // The semantics of these are kinda hard to reason about, what follows
+ // is a description of the different combinations that can happen with
+ // these three sets.
+ //
+ // Experimental properties are generally controlled by prefs, but an
+ // experimental property explicitly enabled in certain context (UA or
+ // chrome sheets) is always usable in the context regardless of the
+ // pref value.
+ //
+ // Non-experimental properties are either normal properties which are
+ // usable everywhere, or internal-only properties which are only usable
+ // in certain context they are explicitly enabled in.
+ if self.enabled_for_all_content() {
+ return true;
+ }
+
+ ${static_non_custom_property_id_set(
+ "ENABLED_IN_UA_SHEETS",
+ lambda p: p.explicitly_enabled_in_ua_sheets()
+ )}
+ ${static_non_custom_property_id_set(
+ "ENABLED_IN_CHROME",
+ lambda p: p.explicitly_enabled_in_chrome()
+ )}
+
+ if context.stylesheet_origin == Origin::UserAgent &&
+ ENABLED_IN_UA_SHEETS.contains(self)
+ {
+ return true
+ }
+
+ if context.chrome_rules_enabled() && ENABLED_IN_CHROME.contains(self) {
+ return true
+ }
+
+ false
+ }
+
+ /// The supported types of this property. The return value should be
+ /// style_traits::CssType when it can become a bitflags type.
+ pub(super) fn supported_types(&self) -> u8 {
+ const SUPPORTED_TYPES: [u8; ${len(data.longhands) + len(data.shorthands)}] = [
+ % for prop in data.longhands:
+ <${prop.specified_type()} as SpecifiedValueInfo>::SUPPORTED_TYPES,
+ % endfor
+ % for prop in data.shorthands:
+ % if prop.name == "all":
+ 0, // 'all' accepts no value other than CSS-wide keywords
+ % else:
+ <shorthands::${prop.ident}::Longhands as SpecifiedValueInfo>::SUPPORTED_TYPES,
+ % endif
+ % endfor
+ ];
+ SUPPORTED_TYPES[self.0 as usize]
+ }
+
+ /// See PropertyId::collect_property_completion_keywords.
+ pub(super) fn collect_property_completion_keywords(&self, f: KeywordsCollectFn) {
+ fn do_nothing(_: KeywordsCollectFn) {}
+ const COLLECT_FUNCTIONS: [fn(KeywordsCollectFn);
+ ${len(data.longhands) + len(data.shorthands)}] = [
+ % for prop in data.longhands:
+ <${prop.specified_type()} as SpecifiedValueInfo>::collect_completion_keywords,
+ % endfor
+ % for prop in data.shorthands:
+ % if prop.name == "all":
+ do_nothing, // 'all' accepts no value other than CSS-wide keywords
+ % else:
+ <shorthands::${prop.ident}::Longhands as SpecifiedValueInfo>::
+ collect_completion_keywords,
+ % endif
+ % endfor
+ ];
+ COLLECT_FUNCTIONS[self.0 as usize](f);
+ }
+}
+
+<%def name="static_non_custom_property_id_set(name, is_member)">
+static ${name}: NonCustomPropertyIdSet = NonCustomPropertyIdSet {
+ <%
+ storage = [0] * int((len(data.longhands) + len(data.shorthands) + len(data.all_aliases()) - 1 + 32) / 32)
+ for i, property in enumerate(data.longhands + data.shorthands + data.all_aliases()):
+ if is_member(property):
+ storage[int(i / 32)] |= 1 << (i % 32)
+ %>
+ storage: [${", ".join("0x%x" % word for word in storage)}]
+};
+</%def>
+
+<%def name="static_longhand_id_set(name, is_member)">
+static ${name}: LonghandIdSet = LonghandIdSet {
+ <%
+ storage = [0] * int((len(data.longhands) - 1 + 32) / 32)
+ for i, property in enumerate(data.longhands):
+ if is_member(property):
+ storage[int(i / 32)] |= 1 << (i % 32)
+ %>
+ storage: [${", ".join("0x%x" % word for word in storage)}]
+};
+</%def>
+
+<%
+ logical_groups = defaultdict(list)
+ for prop in data.longhands:
+ if prop.logical_group:
+ logical_groups[prop.logical_group].append(prop)
+
+ for group, props in logical_groups.items():
+ logical_count = sum(1 for p in props if p.logical)
+ if logical_count * 2 != len(props):
+ raise RuntimeError("Logical group {} has ".format(group) +
+ "unbalanced logical / physical properties")
+
+ FIRST_LINE_RESTRICTIONS = PropertyRestrictions.first_line(data)
+ FIRST_LETTER_RESTRICTIONS = PropertyRestrictions.first_letter(data)
+ MARKER_RESTRICTIONS = PropertyRestrictions.marker(data)
+ PLACEHOLDER_RESTRICTIONS = PropertyRestrictions.placeholder(data)
+ CUE_RESTRICTIONS = PropertyRestrictions.cue(data)
+
+ def restriction_flags(property):
+ name = property.name
+ flags = []
+ if name in FIRST_LINE_RESTRICTIONS:
+ flags.append("APPLIES_TO_FIRST_LINE")
+ if name in FIRST_LETTER_RESTRICTIONS:
+ flags.append("APPLIES_TO_FIRST_LETTER")
+ if name in PLACEHOLDER_RESTRICTIONS:
+ flags.append("APPLIES_TO_PLACEHOLDER")
+ if name in MARKER_RESTRICTIONS:
+ flags.append("APPLIES_TO_MARKER")
+ if name in CUE_RESTRICTIONS:
+ flags.append("APPLIES_TO_CUE")
+ return flags
+
+%>
+
+/// A group for properties which may override each other via logical resolution.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+#[repr(u8)]
+pub enum LogicalGroupId {
+ % for i, group in enumerate(logical_groups.keys()):
+ /// ${group}
+ ${to_camel_case(group)} = ${i},
+ % endfor
+}
+
+impl LogicalGroupId {
+ /// Return the list of physical mapped properties for a given logical group.
+ fn physical_properties(self) -> &'static [LonghandId] {
+ static PROPS: [[LonghandId; 4]; ${len(logical_groups)}] = [
+ % for group, props in logical_groups.items():
+ [
+ <% physical_props = [p for p in props if p.logical][0].all_physical_mapped_properties(data) %>
+ % for phys in physical_props:
+ LonghandId::${phys.camel_case},
+ % endfor
+ % for i in range(len(physical_props), 4):
+ LonghandId::${physical_props[0].camel_case},
+ % endfor
+ ],
+ % endfor
+ ];
+ &PROPS[self as usize]
+ }
+}
+
+/// A set of logical groups.
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq)]
+pub struct LogicalGroupSet {
+ storage: [u32; (${len(logical_groups)} - 1 + 32) / 32]
+}
+
+impl LogicalGroupSet {
+ /// Creates an empty `NonCustomPropertyIdSet`.
+ pub fn new() -> Self {
+ Self {
+ storage: Default::default(),
+ }
+ }
+
+ /// Return whether the given group is in the set
+ #[inline]
+ pub fn contains(&self, g: LogicalGroupId) -> bool {
+ let bit = g as usize;
+ (self.storage[bit / 32] & (1 << (bit % 32))) != 0
+ }
+
+ /// Insert a group the set.
+ #[inline]
+ pub fn insert(&mut self, g: LogicalGroupId) {
+ let bit = g as usize;
+ self.storage[bit / 32] |= 1 << (bit % 32);
+ }
+}
+
+
+#[repr(u8)]
+#[derive(Copy, Clone, Debug)]
+pub(crate) enum PrioritaryPropertyId {
+ % for p in data.longhands:
+ % if p.is_prioritary():
+ ${p.camel_case},
+ % endif
+ % endfor
+}
+
+impl PrioritaryPropertyId {
+ #[inline]
+ pub fn to_longhand(self) -> LonghandId {
+ static PRIORITARY_TO_LONGHAND: [LonghandId; property_counts::PRIORITARY] = [
+ % for p in data.longhands:
+ % if p.is_prioritary():
+ LonghandId::${p.camel_case},
+ % endif
+ % endfor
+ ];
+ PRIORITARY_TO_LONGHAND[self as usize]
+ }
+ #[inline]
+ pub fn from_longhand(l: LonghandId) -> Option<Self> {
+ static LONGHAND_TO_PRIORITARY: [Option<PrioritaryPropertyId>; ${len(data.longhands)}] = [
+ % for p in data.longhands:
+ % if p.is_prioritary():
+ Some(PrioritaryPropertyId::${p.camel_case}),
+ % else:
+ None,
+ % endif
+ % endfor
+ ];
+ LONGHAND_TO_PRIORITARY[l as usize]
+ }
+}
+
+impl LonghandIdSet {
+ /// The set of non-inherited longhands.
+ #[inline]
+ pub(super) fn reset() -> &'static Self {
+ ${static_longhand_id_set("RESET", lambda p: not p.style_struct.inherited)}
+ &RESET
+ }
+
+ #[inline]
+ pub(super) fn discrete_animatable() -> &'static Self {
+ ${static_longhand_id_set("DISCRETE_ANIMATABLE", lambda p: p.animation_value_type == "discrete")}
+ &DISCRETE_ANIMATABLE
+ }
+
+ #[inline]
+ pub(super) fn logical() -> &'static Self {
+ ${static_longhand_id_set("LOGICAL", lambda p: p.logical)}
+ &LOGICAL
+ }
+
+ /// Returns the set of longhands that are ignored when document colors are
+ /// disabled.
+ #[inline]
+ pub(super) fn ignored_when_colors_disabled() -> &'static Self {
+ ${static_longhand_id_set(
+ "IGNORED_WHEN_COLORS_DISABLED",
+ lambda p: p.ignored_when_colors_disabled
+ )}
+ &IGNORED_WHEN_COLORS_DISABLED
+ }
+
+ /// Only a few properties are allowed to depend on the visited state of
+ /// links. When cascading visited styles, we can save time by only
+ /// processing these properties.
+ pub(super) fn visited_dependent() -> &'static Self {
+ ${static_longhand_id_set("VISITED_DEPENDENT", lambda p: p.is_visited_dependent())}
+ debug_assert!(Self::late_group().contains_all(&VISITED_DEPENDENT));
+ &VISITED_DEPENDENT
+ }
+
+ #[inline]
+ pub(super) fn prioritary_properties() -> &'static Self {
+ ${static_longhand_id_set("PRIORITARY_PROPERTIES", lambda p: p.is_prioritary())}
+ &PRIORITARY_PROPERTIES
+ }
+
+ #[inline]
+ pub(super) fn late_group_only_inherited() -> &'static Self {
+ ${static_longhand_id_set("LATE_GROUP_ONLY_INHERITED", lambda p: p.style_struct.inherited and not p.is_prioritary())}
+ &LATE_GROUP_ONLY_INHERITED
+ }
+
+ #[inline]
+ pub(super) fn late_group() -> &'static Self {
+ ${static_longhand_id_set("LATE_GROUP", lambda p: not p.is_prioritary())}
+ &LATE_GROUP
+ }
+
+ /// Returns the set of properties that are declared as having no effect on
+ /// Gecko <scrollbar> elements or their descendant scrollbar parts.
+ #[cfg(debug_assertions)]
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn has_no_effect_on_gecko_scrollbars() -> &'static Self {
+ // data.py asserts that has_no_effect_on_gecko_scrollbars is True or
+ // False for properties that are inherited and Gecko pref controlled,
+ // and is None for all other properties.
+ ${static_longhand_id_set(
+ "HAS_NO_EFFECT_ON_SCROLLBARS",
+ lambda p: p.has_effect_on_gecko_scrollbars is False
+ )}
+ &HAS_NO_EFFECT_ON_SCROLLBARS
+ }
+
+ /// Returns the set of border properties for the purpose of disabling native
+ /// appearance.
+ #[inline]
+ pub fn border_background_properties() -> &'static Self {
+ ${static_longhand_id_set(
+ "BORDER_BACKGROUND_PROPERTIES",
+ lambda p: (p.logical_group and p.logical_group.startswith("border")) or \
+ p.name in ["background-color", "background-image"]
+ )}
+ &BORDER_BACKGROUND_PROPERTIES
+ }
+}
+
+/// An identifier for a given longhand property.
+#[derive(Clone, Copy, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+#[repr(u16)]
+pub enum LonghandId {
+ % for i, property in enumerate(data.longhands):
+ /// ${property.name}
+ ${property.camel_case} = ${i},
+ % endfor
+}
+
+enum LogicalMappingKind {
+ Side(LogicalSide),
+ Corner(LogicalCorner),
+ Axis(LogicalAxis),
+}
+
+struct LogicalMappingData {
+ group: LogicalGroupId,
+ kind: LogicalMappingKind,
+}
+
+impl LogicalMappingData {
+ fn to_physical(&self, wm: WritingMode) -> LonghandId {
+ let index = match self.kind {
+ LogicalMappingKind::Side(s) => s.to_physical(wm) as usize,
+ LogicalMappingKind::Corner(c) => c.to_physical(wm) as usize,
+ LogicalMappingKind::Axis(a) => a.to_physical(wm) as usize,
+ };
+ self.group.physical_properties()[index]
+ }
+}
+
+impl LonghandId {
+ /// Returns an iterator over all the shorthands that include this longhand.
+ pub fn shorthands(self) -> NonCustomPropertyIterator<ShorthandId> {
+ // first generate longhand to shorthands lookup map
+ //
+ // NOTE(emilio): This currently doesn't exclude the "all" shorthand. It
+ // could potentially do so, which would speed up serialization
+ // algorithms and what not, I guess.
+ <%
+ from functools import cmp_to_key
+ longhand_to_shorthand_map = {}
+ num_sub_properties = {}
+ for shorthand in data.shorthands:
+ num_sub_properties[shorthand.camel_case] = len(shorthand.sub_properties)
+ for sub_property in shorthand.sub_properties:
+ if sub_property.ident not in longhand_to_shorthand_map:
+ longhand_to_shorthand_map[sub_property.ident] = []
+
+ longhand_to_shorthand_map[sub_property.ident].append(shorthand.camel_case)
+
+ def cmp(a, b):
+ return (a > b) - (a < b)
+
+ def preferred_order(x, y):
+ # Since we want properties in order from most subproperties to least,
+ # reverse the arguments to cmp from the expected order.
+ result = cmp(num_sub_properties.get(y, 0), num_sub_properties.get(x, 0))
+ if result:
+ return result
+ # Fall back to lexicographic comparison.
+ return cmp(x, y)
+
+ # Sort the lists of shorthand properties according to preferred order:
+ # https://drafts.csswg.org/cssom/#concept-shorthands-preferred-order
+ for shorthand_list in longhand_to_shorthand_map.values():
+ shorthand_list.sort(key=cmp_to_key(preferred_order))
+ %>
+
+ // based on lookup results for each longhand, create result arrays
+ static MAP: [&'static [ShorthandId]; property_counts::LONGHANDS] = [
+ % for property in data.longhands:
+ &[
+ % for shorthand in longhand_to_shorthand_map.get(property.ident, []):
+ ShorthandId::${shorthand},
+ % endfor
+ ],
+ % endfor
+ ];
+
+ NonCustomPropertyIterator {
+ filter: NonCustomPropertyId::from(self).enabled_for_all_content(),
+ iter: MAP[self as usize].iter(),
+ }
+ }
+
+ pub(super) fn parse_value<'i, 't>(
+ self,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<PropertyDeclaration, ParseError<'i>> {
+ type ParsePropertyFn = for<'i, 't> fn(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<PropertyDeclaration, ParseError<'i>>;
+ static PARSE_PROPERTY: [ParsePropertyFn; ${len(data.longhands)}] = [
+ % for property in data.longhands:
+ longhands::${property.ident}::parse_declared,
+ % endfor
+ ];
+ (PARSE_PROPERTY[self as usize])(context, input)
+ }
+
+ /// Return the relevant data to map a particular logical property into physical.
+ fn logical_mapping_data(self) -> Option<<&'static LogicalMappingData> {
+ const LOGICAL_MAPPING_DATA: [Option<LogicalMappingData>; ${len(data.longhands)}] = [
+ % for prop in data.longhands:
+ % if prop.logical:
+ Some(LogicalMappingData {
+ group: LogicalGroupId::${to_camel_case(prop.logical_group)},
+ kind: ${prop.logical_mapping_kind(data)}
+ }),
+ % else:
+ None,
+ % endif
+ % endfor
+ ];
+ LOGICAL_MAPPING_DATA[self as usize].as_ref()
+ }
+
+ /// If this is a logical property, return the corresponding physical one in the given
+ /// writing mode. Otherwise, return unchanged.
+ #[inline]
+ pub fn to_physical(self, wm: WritingMode) -> Self {
+ let Some(data) = self.logical_mapping_data() else { return self };
+ data.to_physical(wm)
+ }
+
+ /// Return the logical group of this longhand property.
+ pub fn logical_group(self) -> Option<LogicalGroupId> {
+ const LOGICAL_GROUP_IDS: [Option<LogicalGroupId>; ${len(data.longhands)}] = [
+ % for prop in data.longhands:
+ % if prop.logical_group:
+ Some(LogicalGroupId::${to_camel_case(prop.logical_group)}),
+ % else:
+ None,
+ % endif
+ % endfor
+ ];
+ LOGICAL_GROUP_IDS[self as usize]
+ }
+
+ /// Returns PropertyFlags for given longhand property.
+ #[inline(always)]
+ pub fn flags(self) -> PropertyFlags {
+ // TODO(emilio): This can be simplified further as Rust gains more
+ // constant expression support.
+ const FLAGS: [u16; ${len(data.longhands)}] = [
+ % for property in data.longhands:
+ % for flag in property.flags + restriction_flags(property):
+ PropertyFlags::${flag}.bits() |
+ % endfor
+ 0,
+ % endfor
+ ];
+ PropertyFlags::from_bits_retain(FLAGS[self as usize])
+ }
+}
+
+/// An identifier for a given shorthand property.
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+#[repr(u16)]
+pub enum ShorthandId {
+ % for i, property in enumerate(data.shorthands):
+ /// ${property.name}
+ ${property.camel_case} = ${i},
+ % endfor
+}
+
+impl ShorthandId {
+ /// Get the longhand ids that form this shorthand.
+ pub fn longhands(self) -> NonCustomPropertyIterator<LonghandId> {
+ static MAP: [&'static [LonghandId]; property_counts::SHORTHANDS] = [
+ % for property in data.shorthands:
+ &[
+ % for sub in property.sub_properties:
+ LonghandId::${sub.camel_case},
+ % endfor
+ ],
+ % endfor
+ ];
+ NonCustomPropertyIterator {
+ filter: NonCustomPropertyId::from(self).enabled_for_all_content(),
+ iter: MAP[self as usize].iter(),
+ }
+ }
+
+ /// Try to serialize the given declarations as this shorthand.
+ ///
+ /// Returns an error if writing to the stream fails, or if the declarations
+ /// do not map to a shorthand.
+ pub fn longhands_to_css(
+ self,
+ declarations: &[&PropertyDeclaration],
+ dest: &mut CssStringWriter,
+ ) -> fmt::Result {
+ type LonghandsToCssFn = for<'a, 'b> fn(&'a [&'b PropertyDeclaration], &mut CssStringWriter) -> fmt::Result;
+ fn all_to_css(_: &[&PropertyDeclaration], _: &mut CssStringWriter) -> fmt::Result {
+ // No need to try to serialize the declarations as the 'all'
+ // shorthand, since it only accepts CSS-wide keywords (and variable
+ // references), which will be handled in
+ // get_shorthand_appendable_value.
+ Ok(())
+ }
+
+ static LONGHANDS_TO_CSS: [LonghandsToCssFn; ${len(data.shorthands)}] = [
+ % for shorthand in data.shorthands:
+ % if shorthand.ident == "all":
+ all_to_css,
+ % else:
+ shorthands::${shorthand.ident}::to_css,
+ % endif
+ % endfor
+ ];
+
+ LONGHANDS_TO_CSS[self as usize](declarations, dest)
+ }
+
+ /// Returns PropertyFlags for the given shorthand property.
+ #[inline]
+ pub fn flags(self) -> PropertyFlags {
+ const FLAGS: [u16; ${len(data.shorthands)}] = [
+ % for property in data.shorthands:
+ % for flag in property.flags:
+ PropertyFlags::${flag}.bits() |
+ % endfor
+ 0,
+ % endfor
+ ];
+ PropertyFlags::from_bits_retain(FLAGS[self as usize])
+ }
+
+ /// Returns the order in which this property appears relative to other
+ /// shorthands in idl-name-sorting order.
+ #[inline]
+ pub fn idl_name_sort_order(self) -> u32 {
+ <%
+ from data import to_idl_name
+ ordered = {}
+ sorted_shorthands = sorted(data.shorthands, key=lambda p: to_idl_name(p.ident))
+ for order, shorthand in enumerate(sorted_shorthands):
+ ordered[shorthand.ident] = order
+ %>
+ static IDL_NAME_SORT_ORDER: [u32; ${len(data.shorthands)}] = [
+ % for property in data.shorthands:
+ ${ordered[property.ident]},
+ % endfor
+ ];
+ IDL_NAME_SORT_ORDER[self as usize]
+ }
+
+ pub(super) fn parse_into<'i, 't>(
+ self,
+ declarations: &mut SourcePropertyDeclaration,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ type ParseIntoFn = for<'i, 't> fn(
+ declarations: &mut SourcePropertyDeclaration,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>>;
+
+ fn parse_all<'i, 't>(
+ _: &mut SourcePropertyDeclaration,
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>
+ ) -> Result<(), ParseError<'i>> {
+ // 'all' accepts no value other than CSS-wide keywords
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ static PARSE_INTO: [ParseIntoFn; ${len(data.shorthands)}] = [
+ % for shorthand in data.shorthands:
+ % if shorthand.ident == "all":
+ parse_all,
+ % else:
+ shorthands::${shorthand.ident}::parse_into,
+ % endif
+ % endfor
+ ];
+
+ (PARSE_INTO[self as usize])(declarations, context, input)
+ }
+}
+
+/// The counted unknown property list which is used for css use counters.
+///
+/// FIXME: This should be just #[repr(u8)], but can't be because of ABI issues,
+/// see https://bugs.llvm.org/show_bug.cgi?id=44228.
+#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, PartialEq)]
+#[repr(u32)]
+pub enum CountedUnknownProperty {
+ % for prop in data.counted_unknown_properties:
+ /// ${prop.name}
+ ${prop.camel_case},
+ % endfor
+}
+
+impl CountedUnknownProperty {
+ /// Parse the counted unknown property, for testing purposes only.
+ pub fn parse_for_testing(property_name: &str) -> Option<Self> {
+ ascii_case_insensitive_phf_map! {
+ unknown_ids -> CountedUnknownProperty = {
+ % for property in data.counted_unknown_properties:
+ "${property.name}" => CountedUnknownProperty::${property.camel_case},
+ % endfor
+ }
+ }
+ unknown_ids::get(property_name).cloned()
+ }
+
+ /// Returns the underlying index, used for use counter.
+ #[inline]
+ pub fn bit(self) -> usize {
+ self as usize
+ }
+}
+
+impl PropertyId {
+ /// Returns a given property from the given name, _regardless of whether it
+ /// is enabled or not_, or Err(()) for unknown properties.
+ pub(super) fn parse_unchecked(
+ property_name: &str,
+ use_counters: Option< &UseCounters>,
+ ) -> Result<Self, ()> {
+ // A special id for css use counters. ShorthandAlias is not used in the Servo build.
+ // That's why we need to allow dead_code.
+ pub enum StaticId {
+ NonCustom(NonCustomPropertyId),
+ CountedUnknown(CountedUnknownProperty),
+ }
+ ascii_case_insensitive_phf_map! {
+ static_ids -> StaticId = {
+ % for i, property in enumerate(data.longhands + data.shorthands + data.all_aliases()):
+ "${property.name}" => StaticId::NonCustom(NonCustomPropertyId(${i})),
+ % endfor
+ % for property in data.counted_unknown_properties:
+ "${property.name}" => {
+ StaticId::CountedUnknown(CountedUnknownProperty::${property.camel_case})
+ },
+ % endfor
+ }
+ }
+
+ if let Some(id) = static_ids::get(property_name) {
+ return Ok(match *id {
+ StaticId::NonCustom(id) => PropertyId::NonCustom(id),
+ StaticId::CountedUnknown(unknown_prop) => {
+ if let Some(counters) = use_counters {
+ counters.counted_unknown_properties.record(unknown_prop);
+ }
+ // Always return Err(()) because these aren't valid custom property names.
+ return Err(());
+ }
+ });
+ }
+
+ let name = crate::custom_properties::parse_name(property_name)?;
+ Ok(PropertyId::Custom(crate::custom_properties::Name::from(name)))
+ }
+}
+
+impl PropertyDeclaration {
+ /// Given a property declaration, return the property declaration id.
+ #[inline]
+ pub fn id(&self) -> PropertyDeclarationId {
+ match *self {
+ PropertyDeclaration::Custom(ref declaration) => {
+ return PropertyDeclarationId::Custom(&declaration.name)
+ }
+ PropertyDeclaration::CSSWideKeyword(ref declaration) => {
+ return PropertyDeclarationId::Longhand(declaration.id);
+ }
+ PropertyDeclaration::WithVariables(ref declaration) => {
+ return PropertyDeclarationId::Longhand(declaration.id);
+ }
+ _ => {}
+ }
+ // This is just fine because PropertyDeclaration and LonghandId
+ // have corresponding discriminants.
+ let id = unsafe { *(self as *const _ as *const LonghandId) };
+ debug_assert_eq!(id, match *self {
+ % for property in data.longhands:
+ PropertyDeclaration::${property.camel_case}(..) => LonghandId::${property.camel_case},
+ % endfor
+ _ => id,
+ });
+ PropertyDeclarationId::Longhand(id)
+ }
+
+ /// Given a declaration, convert it into a declaration for a corresponding
+ /// physical property.
+ #[inline]
+ pub fn to_physical(&self, wm: WritingMode) -> Self {
+ match *self {
+ PropertyDeclaration::WithVariables(VariableDeclaration {
+ id,
+ ref value,
+ }) => {
+ return PropertyDeclaration::WithVariables(VariableDeclaration {
+ id: id.to_physical(wm),
+ value: value.clone(),
+ })
+ }
+ PropertyDeclaration::CSSWideKeyword(WideKeywordDeclaration {
+ id,
+ keyword,
+ }) => {
+ return PropertyDeclaration::CSSWideKeyword(WideKeywordDeclaration {
+ id: id.to_physical(wm),
+ keyword,
+ })
+ }
+ PropertyDeclaration::Custom(..) => return self.clone(),
+ % for prop in data.longhands:
+ PropertyDeclaration::${prop.camel_case}(..) => {},
+ % endfor
+ }
+
+ let mut ret = self.clone();
+
+ % for prop in data.longhands:
+ % for physical_property in prop.all_physical_mapped_properties(data):
+ % if physical_property.specified_type() != prop.specified_type():
+ <% raise "Logical property %s should share specified value with physical property %s" % \
+ (prop.name, physical_property.name) %>
+ % endif
+ % endfor
+ % endfor
+
+ unsafe {
+ let longhand_id = *(&mut ret as *mut _ as *mut LonghandId);
+
+ debug_assert_eq!(
+ PropertyDeclarationId::Longhand(longhand_id),
+ ret.id()
+ );
+
+ // This is just fine because PropertyDeclaration and LonghandId
+ // have corresponding discriminants.
+ *(&mut ret as *mut _ as *mut LonghandId) = longhand_id.to_physical(wm);
+
+ debug_assert_eq!(
+ PropertyDeclarationId::Longhand(longhand_id.to_physical(wm)),
+ ret.id()
+ );
+ }
+
+ ret
+ }
+
+ /// Returns whether or not the property is set by a system font
+ pub fn get_system(&self) -> Option<SystemFont> {
+ match *self {
+ % if engine == "gecko":
+ % for prop in SYSTEM_FONT_LONGHANDS:
+ PropertyDeclaration::${to_camel_case(prop)}(ref prop) => {
+ prop.get_system()
+ }
+ % endfor
+ % endif
+ _ => None,
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+pub use super::gecko::style_structs;
+
+/// The module where all the style structs are defined.
+#[cfg(feature = "servo")]
+pub mod style_structs {
+ use fxhash::FxHasher;
+ use super::longhands;
+ use std::hash::{Hash, Hasher};
+ use crate::logical_geometry::WritingMode;
+ use crate::media_queries::Device;
+ use crate::values::computed::NonNegativeLength;
+
+ % for style_struct in data.active_style_structs():
+ % if style_struct.name == "Font":
+ #[derive(Clone, Debug, MallocSizeOf)]
+ #[cfg_attr(feature = "servo", derive(Serialize, Deserialize))]
+ % else:
+ #[derive(Clone, Debug, MallocSizeOf, PartialEq)]
+ % endif
+ /// The ${style_struct.name} style struct.
+ pub struct ${style_struct.name} {
+ % for longhand in style_struct.longhands:
+ % if not longhand.logical:
+ /// The ${longhand.name} computed value.
+ pub ${longhand.ident}: longhands::${longhand.ident}::computed_value::T,
+ % endif
+ % endfor
+ % if style_struct.name == "InheritedText":
+ /// The "used" text-decorations that apply to this box.
+ ///
+ /// FIXME(emilio): This is technically a box-tree concept, and
+ /// would be nice to move away from style.
+ pub text_decorations_in_effect: crate::values::computed::text::TextDecorationsInEffect,
+ % endif
+ % if style_struct.name == "Font":
+ /// The font hash, used for font caching.
+ pub hash: u64,
+ % endif
+ % if style_struct.name == "Box":
+ /// The display value specified by the CSS stylesheets (without any style adjustments),
+ /// which is needed for hypothetical layout boxes.
+ pub original_display: longhands::display::computed_value::T,
+ % endif
+ }
+ % if style_struct.name == "Font":
+ impl PartialEq for Font {
+ fn eq(&self, other: &Font) -> bool {
+ self.hash == other.hash
+ % for longhand in style_struct.longhands:
+ && self.${longhand.ident} == other.${longhand.ident}
+ % endfor
+ }
+ }
+ % endif
+
+ impl ${style_struct.name} {
+ % for longhand in style_struct.longhands:
+ % if not longhand.logical:
+ % if longhand.ident == "display":
+ /// Set `display`.
+ ///
+ /// We need to keep track of the original display for hypothetical boxes,
+ /// so we need to special-case this.
+ #[allow(non_snake_case)]
+ #[inline]
+ pub fn set_display(&mut self, v: longhands::display::computed_value::T) {
+ self.display = v;
+ self.original_display = v;
+ }
+ % else:
+ /// Set ${longhand.name}.
+ #[allow(non_snake_case)]
+ #[inline]
+ pub fn set_${longhand.ident}(&mut self, v: longhands::${longhand.ident}::computed_value::T) {
+ self.${longhand.ident} = v;
+ }
+ % endif
+ % if longhand.ident == "display":
+ /// Set `display` from other struct.
+ ///
+ /// Same as `set_display` above.
+ /// Thus, we need to special-case this.
+ #[allow(non_snake_case)]
+ #[inline]
+ pub fn copy_display_from(&mut self, other: &Self) {
+ self.display = other.display.clone();
+ self.original_display = other.display.clone();
+ }
+ % else:
+ /// Set ${longhand.name} from other struct.
+ #[allow(non_snake_case)]
+ #[inline]
+ pub fn copy_${longhand.ident}_from(&mut self, other: &Self) {
+ self.${longhand.ident} = other.${longhand.ident}.clone();
+ }
+ % endif
+ /// Reset ${longhand.name} from the initial struct.
+ #[allow(non_snake_case)]
+ #[inline]
+ pub fn reset_${longhand.ident}(&mut self, other: &Self) {
+ self.copy_${longhand.ident}_from(other)
+ }
+
+ /// Get the computed value for ${longhand.name}.
+ #[allow(non_snake_case)]
+ #[inline]
+ pub fn clone_${longhand.ident}(&self) -> longhands::${longhand.ident}::computed_value::T {
+ self.${longhand.ident}.clone()
+ }
+ % endif
+ % if longhand.need_index:
+ /// If this longhand is indexed, get the number of elements.
+ #[allow(non_snake_case)]
+ pub fn ${longhand.ident}_count(&self) -> usize {
+ self.${longhand.ident}.0.len()
+ }
+
+ /// If this longhand is indexed, get the element at given
+ /// index.
+ #[allow(non_snake_case)]
+ pub fn ${longhand.ident}_at(&self, index: usize)
+ -> longhands::${longhand.ident}::computed_value::SingleComputedValue {
+ self.${longhand.ident}.0[index].clone()
+ }
+ % endif
+ % endfor
+ % if style_struct.name == "Border":
+ % for side in ["top", "right", "bottom", "left"]:
+ /// Whether the border-${side} property has nonzero width.
+ #[allow(non_snake_case)]
+ pub fn border_${side}_has_nonzero_width(&self) -> bool {
+ use crate::Zero;
+ !self.border_${side}_width.is_zero()
+ }
+ % endfor
+ % elif style_struct.name == "Font":
+ /// Computes a font hash in order to be able to cache fonts
+ /// effectively in GFX and layout.
+ pub fn compute_font_hash(&mut self) {
+ // Corresponds to the fields in
+ // `gfx::font_template::FontTemplateDescriptor`.
+ let mut hasher: FxHasher = Default::default();
+ self.font_weight.hash(&mut hasher);
+ self.font_stretch.hash(&mut hasher);
+ self.font_style.hash(&mut hasher);
+ self.font_family.hash(&mut hasher);
+ self.hash = hasher.finish()
+ }
+
+ /// (Servo does not handle MathML, so this just calls copy_font_size_from)
+ pub fn inherit_font_size_from(&mut self, parent: &Self,
+ _: Option<NonNegativeLength>,
+ _: &Device) {
+ self.copy_font_size_from(parent);
+ }
+ /// (Servo does not handle MathML, so this just calls set_font_size)
+ pub fn apply_font_size(&mut self,
+ v: longhands::font_size::computed_value::T,
+ _: &Self,
+ _: &Device) -> Option<NonNegativeLength> {
+ self.set_font_size(v);
+ None
+ }
+ /// (Servo does not handle MathML, so this does nothing)
+ pub fn apply_unconstrained_font_size(&mut self, _: NonNegativeLength) {
+ }
+
+ % elif style_struct.name == "Outline":
+ /// Whether the outline-width property is non-zero.
+ #[inline]
+ pub fn outline_has_nonzero_width(&self) -> bool {
+ use crate::Zero;
+ !self.outline_width.is_zero()
+ }
+ % elif style_struct.name == "Box":
+ /// Sets the display property, but without touching original_display,
+ /// except when the adjustment comes from root or item display fixups.
+ pub fn set_adjusted_display(
+ &mut self,
+ dpy: longhands::display::computed_value::T,
+ is_item_or_root: bool
+ ) {
+ self.display = dpy;
+ if is_item_or_root {
+ self.original_display = dpy;
+ }
+ }
+ % endif
+ }
+
+ % endfor
+}
+
+% for style_struct in data.active_style_structs():
+ impl style_structs::${style_struct.name} {
+ % for longhand in style_struct.longhands:
+ % if longhand.need_index:
+ /// Iterate over the values of ${longhand.name}.
+ #[allow(non_snake_case)]
+ #[inline]
+ pub fn ${longhand.ident}_iter(&self) -> ${longhand.camel_case}Iter {
+ ${longhand.camel_case}Iter {
+ style_struct: self,
+ current: 0,
+ max: self.${longhand.ident}_count(),
+ }
+ }
+
+ /// Get a value mod `index` for the property ${longhand.name}.
+ #[allow(non_snake_case)]
+ #[inline]
+ pub fn ${longhand.ident}_mod(&self, index: usize)
+ -> longhands::${longhand.ident}::computed_value::SingleComputedValue {
+ self.${longhand.ident}_at(index % self.${longhand.ident}_count())
+ }
+
+ /// Clone the computed value for the property.
+ #[allow(non_snake_case)]
+ #[inline]
+ #[cfg(feature = "gecko")]
+ pub fn clone_${longhand.ident}(
+ &self,
+ ) -> longhands::${longhand.ident}::computed_value::T {
+ longhands::${longhand.ident}::computed_value::List(
+ self.${longhand.ident}_iter().collect()
+ )
+ }
+ % endif
+ % endfor
+
+ % if style_struct.name == "UI":
+ /// Returns whether there is any animation specified with
+ /// animation-name other than `none`.
+ pub fn specifies_animations(&self) -> bool {
+ self.animation_name_iter().any(|name| !name.is_none())
+ }
+
+ /// Returns whether there are any transitions specified.
+ #[cfg(feature = "servo")]
+ pub fn specifies_transitions(&self) -> bool {
+ (0..self.transition_property_count()).any(|index| {
+ let combined_duration =
+ self.transition_duration_mod(index).seconds().max(0.) +
+ self.transition_delay_mod(index).seconds();
+ combined_duration > 0.
+ })
+ }
+
+ /// Returns whether there is any named progress timeline specified with
+ /// scroll-timeline-name other than `none`.
+ pub fn specifies_scroll_timelines(&self) -> bool {
+ self.scroll_timeline_name_iter().any(|name| !name.is_none())
+ }
+
+ /// Returns whether there is any named progress timeline specified with
+ /// view-timeline-name other than `none`.
+ pub fn specifies_view_timelines(&self) -> bool {
+ self.view_timeline_name_iter().any(|name| !name.is_none())
+ }
+
+ /// Returns true if animation properties are equal between styles, but without
+ /// considering keyframe data and animation-timeline.
+ #[cfg(feature = "servo")]
+ pub fn animations_equals(&self, other: &Self) -> bool {
+ self.animation_name_iter().eq(other.animation_name_iter()) &&
+ self.animation_delay_iter().eq(other.animation_delay_iter()) &&
+ self.animation_direction_iter().eq(other.animation_direction_iter()) &&
+ self.animation_duration_iter().eq(other.animation_duration_iter()) &&
+ self.animation_fill_mode_iter().eq(other.animation_fill_mode_iter()) &&
+ self.animation_iteration_count_iter().eq(other.animation_iteration_count_iter()) &&
+ self.animation_play_state_iter().eq(other.animation_play_state_iter()) &&
+ self.animation_timing_function_iter().eq(other.animation_timing_function_iter())
+ }
+
+ % elif style_struct.name == "Column":
+ /// Whether this is a multicol style.
+ #[cfg(feature = "servo")]
+ pub fn is_multicol(&self) -> bool {
+ !self.column_width.is_auto() || !self.column_count.is_auto()
+ }
+ % endif
+ }
+
+ % for longhand in style_struct.longhands:
+ % if longhand.need_index:
+ /// An iterator over the values of the ${longhand.name} properties.
+ pub struct ${longhand.camel_case}Iter<'a> {
+ style_struct: &'a style_structs::${style_struct.name},
+ current: usize,
+ max: usize,
+ }
+
+ impl<'a> Iterator for ${longhand.camel_case}Iter<'a> {
+ type Item = longhands::${longhand.ident}::computed_value::SingleComputedValue;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.current += 1;
+ if self.current <= self.max {
+ Some(self.style_struct.${longhand.ident}_at(self.current - 1))
+ } else {
+ None
+ }
+ }
+ }
+ % endif
+ % endfor
+% endfor
+
+
+#[cfg(feature = "gecko")]
+pub use super::gecko::{ComputedValues, ComputedValuesInner};
+
+#[cfg(feature = "servo")]
+#[cfg_attr(feature = "servo", derive(Clone, Debug))]
+/// Actual data of ComputedValues, to match up with Gecko
+pub struct ComputedValuesInner {
+ % for style_struct in data.active_style_structs():
+ ${style_struct.ident}: Arc<style_structs::${style_struct.name}>,
+ % endfor
+ custom_properties: crate::custom_properties::ComputedCustomProperties,
+
+ /// The writing mode of this computed values struct.
+ pub writing_mode: WritingMode,
+
+ /// The effective zoom value.
+ pub effective_zoom: Zoom,
+
+ /// A set of flags we use to store misc information regarding this style.
+ pub flags: ComputedValueFlags,
+
+ /// The rule node representing the ordered list of rules matched for this
+ /// node. Can be None for default values and text nodes. This is
+ /// essentially an optimization to avoid referencing the root rule node.
+ pub rules: Option<StrongRuleNode>,
+
+ /// The element's computed values if visited, only computed if there's a
+ /// relevant link for this element. A element's "relevant link" is the
+ /// element being matched if it is a link or the nearest ancestor link.
+ visited_style: Option<Arc<ComputedValues>>,
+}
+
+/// The struct that Servo uses to represent computed values.
+///
+/// This struct contains an immutable atomically-reference-counted pointer to
+/// every kind of style struct.
+///
+/// When needed, the structs may be copied in order to get mutated.
+#[cfg(feature = "servo")]
+#[cfg_attr(feature = "servo", derive(Clone, Debug))]
+pub struct ComputedValues {
+ /// The actual computed values
+ ///
+ /// In Gecko the outer ComputedValues is actually a ComputedStyle, whereas
+ /// ComputedValuesInner is the core set of computed values.
+ ///
+ /// We maintain this distinction in servo to reduce the amount of special
+ /// casing.
+ inner: ComputedValuesInner,
+
+ /// The pseudo-element that we're using.
+ pseudo: Option<PseudoElement>,
+}
+
+impl ComputedValues {
+ /// Returns the pseudo-element that this style represents.
+ #[cfg(feature = "servo")]
+ pub fn pseudo(&self) -> Option<<&PseudoElement> {
+ self.pseudo.as_ref()
+ }
+
+ /// Returns true if this is the style for a pseudo-element.
+ #[cfg(feature = "servo")]
+ pub fn is_pseudo_style(&self) -> bool {
+ self.pseudo().is_some()
+ }
+
+ /// Returns whether this style's display value is equal to contents.
+ pub fn is_display_contents(&self) -> bool {
+ self.clone_display().is_contents()
+ }
+
+ /// Gets a reference to the rule node. Panic if no rule node exists.
+ pub fn rules(&self) -> &StrongRuleNode {
+ self.rules.as_ref().unwrap()
+ }
+
+ /// Returns the visited rules, if applicable.
+ pub fn visited_rules(&self) -> Option<<&StrongRuleNode> {
+ self.visited_style().and_then(|s| s.rules.as_ref())
+ }
+
+ /// Gets a reference to the custom properties map (if one exists).
+ pub fn custom_properties(&self) -> &crate::custom_properties::ComputedCustomProperties {
+ &self.custom_properties
+ }
+
+ /// Returns whether we have the same custom properties as another style.
+ pub fn custom_properties_equal(&self, other: &Self) -> bool {
+ self.custom_properties() == other.custom_properties()
+ }
+
+% for prop in data.longhands:
+% if not prop.logical:
+ /// Gets the computed value of a given property.
+ #[inline(always)]
+ #[allow(non_snake_case)]
+ pub fn clone_${prop.ident}(
+ &self,
+ ) -> longhands::${prop.ident}::computed_value::T {
+ self.get_${prop.style_struct.ident.strip("_")}().clone_${prop.ident}()
+ }
+% endif
+% endfor
+
+ /// Writes the (resolved or computed) value of the given longhand as a string in `dest`.
+ ///
+ /// TODO(emilio): We should move all the special resolution from
+ /// nsComputedDOMStyle to ToResolvedValue instead.
+ pub fn computed_or_resolved_value(
+ &self,
+ property_id: LonghandId,
+ context: Option<<&resolved::Context>,
+ dest: &mut CssStringWriter,
+ ) -> fmt::Result {
+ use crate::values::resolved::ToResolvedValue;
+ let mut dest = CssWriter::new(dest);
+ let property_id = property_id.to_physical(self.writing_mode);
+ match property_id {
+ % for specified_type, props in groupby(data.longhands, key=lambda x: x.specified_type()):
+ <% props = list(props) %>
+ ${" |\n".join("LonghandId::{}".format(p.camel_case) for p in props)} => {
+ let value = match property_id {
+ % for prop in props:
+ % if not prop.logical:
+ LonghandId::${prop.camel_case} => self.clone_${prop.ident}(),
+ % endif
+ % endfor
+ _ => unsafe { debug_unreachable!() },
+ };
+ if let Some(c) = context {
+ value.to_resolved_value(c).to_css(&mut dest)
+ } else {
+ value.to_css(&mut dest)
+ }
+ }
+ % endfor
+ }
+ }
+
+ /// Returns the given longhand's resolved value as a property declaration.
+ pub fn computed_or_resolved_declaration(
+ &self,
+ property_id: LonghandId,
+ context: Option<<&resolved::Context>,
+ ) -> PropertyDeclaration {
+ use crate::values::resolved::ToResolvedValue;
+ use crate::values::computed::ToComputedValue;
+ let physical_property_id = property_id.to_physical(self.writing_mode);
+ match physical_property_id {
+ % for specified_type, props in groupby(data.longhands, key=lambda x: x.specified_type()):
+ <% props = list(props) %>
+ ${" |\n".join("LonghandId::{}".format(p.camel_case) for p in props)} => {
+ let mut computed_value = match physical_property_id {
+ % for prop in props:
+ % if not prop.logical:
+ LonghandId::${prop.camel_case} => self.clone_${prop.ident}(),
+ % endif
+ % endfor
+ _ => unsafe { debug_unreachable!() },
+ };
+ if let Some(c) = context {
+ let resolved = computed_value.to_resolved_value(c);
+ computed_value = ToResolvedValue::from_resolved_value(resolved);
+ }
+ let specified = ToComputedValue::from_computed_value(&computed_value);
+ % if props[0].boxed:
+ let specified = Box::new(specified);
+ % endif
+ % if len(props) == 1:
+ PropertyDeclaration::${props[0].camel_case}(specified)
+ % else:
+ unsafe {
+ let mut out = mem::MaybeUninit::uninit();
+ ptr::write(
+ out.as_mut_ptr() as *mut PropertyDeclarationVariantRepr<${specified_type}>,
+ PropertyDeclarationVariantRepr {
+ tag: property_id as u16,
+ value: specified,
+ },
+ );
+ out.assume_init()
+ }
+ % endif
+ }
+ % endfor
+ }
+ }
+
+ /// Resolves the currentColor keyword.
+ ///
+ /// Any color value from computed values (except for the 'color' property
+ /// itself) should go through this method.
+ ///
+ /// Usage example:
+ /// let top_color =
+ /// style.resolve_color(style.get_border().clone_border_top_color());
+ #[inline]
+ pub fn resolve_color(&self, color: computed::Color) -> crate::color::AbsoluteColor {
+ let current_color = self.get_inherited_text().clone_color();
+ color.resolve_to_absolute(&current_color)
+ }
+
+ /// Returns which longhand properties have different values in the two
+ /// ComputedValues.
+ #[cfg(feature = "gecko_debug")]
+ pub fn differing_properties(&self, other: &ComputedValues) -> LonghandIdSet {
+ let mut set = LonghandIdSet::new();
+ % for prop in data.longhands:
+ % if not prop.logical:
+ if self.clone_${prop.ident}() != other.clone_${prop.ident}() {
+ set.insert(LonghandId::${prop.camel_case});
+ }
+ % endif
+ % endfor
+ set
+ }
+
+ /// Create a `TransitionPropertyIterator` for this styles transition properties.
+ pub fn transition_properties<'a>(
+ &'a self
+ ) -> animated_properties::TransitionPropertyIterator<'a> {
+ animated_properties::TransitionPropertyIterator::from_style(self)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl ComputedValues {
+ /// Create a new refcounted `ComputedValues`
+ pub fn new(
+ pseudo: Option<<&PseudoElement>,
+ custom_properties: crate::custom_properties::ComputedCustomProperties,
+ writing_mode: WritingMode,
+ effective_zoom: computed::Zoom,
+ flags: ComputedValueFlags,
+ rules: Option<StrongRuleNode>,
+ visited_style: Option<Arc<ComputedValues>>,
+ % for style_struct in data.active_style_structs():
+ ${style_struct.ident}: Arc<style_structs::${style_struct.name}>,
+ % endfor
+ ) -> Arc<Self> {
+ Arc::new(Self {
+ inner: ComputedValuesInner {
+ custom_properties,
+ writing_mode,
+ rules,
+ visited_style,
+ effective_zoom,
+ flags,
+ % for style_struct in data.active_style_structs():
+ ${style_struct.ident},
+ % endfor
+ },
+ pseudo: pseudo.cloned(),
+ })
+ }
+
+ /// Get the initial computed values.
+ pub fn initial_values() -> &'static Self { &*INITIAL_SERVO_VALUES }
+
+ /// Serializes the computed value of this property as a string.
+ pub fn computed_value_to_string(&self, property: PropertyDeclarationId) -> String {
+ match property {
+ PropertyDeclarationId::Longhand(id) => {
+ let mut s = String::new();
+ self.get_longhand_property_value(
+ id,
+ &mut CssWriter::new(&mut s)
+ ).unwrap();
+ s
+ }
+ PropertyDeclarationId::Custom(name) => {
+ // FIXME(bug 1869476): This should use a stylist to determine
+ // whether the name corresponds to an inherited custom property
+ // and then choose the inherited/non_inherited map accordingly.
+ let p = &self.custom_properties;
+ let value = p
+ .inherited
+ .as_ref()
+ .and_then(|map| map.get(name))
+ .or_else(|| p.non_inherited.as_ref().and_then(|map| map.get(name)));
+ value.map_or(String::new(), |value| value.to_css_string())
+ }
+ }
+ }
+}
+
+#[cfg(feature = "servo")]
+impl ops::Deref for ComputedValues {
+ type Target = ComputedValuesInner;
+ fn deref(&self) -> &ComputedValuesInner {
+ &self.inner
+ }
+}
+
+#[cfg(feature = "servo")]
+impl ops::DerefMut for ComputedValues {
+ fn deref_mut(&mut self) -> &mut ComputedValuesInner {
+ &mut self.inner
+ }
+}
+
+#[cfg(feature = "servo")]
+impl ComputedValuesInner {
+ /// Returns the visited style, if any.
+ pub fn visited_style(&self) -> Option<<&ComputedValues> {
+ self.visited_style.as_deref()
+ }
+
+ % for style_struct in data.active_style_structs():
+ /// Clone the ${style_struct.name} struct.
+ #[inline]
+ pub fn clone_${style_struct.name_lower}(&self) -> Arc<style_structs::${style_struct.name}> {
+ self.${style_struct.ident}.clone()
+ }
+
+ /// Get a immutable reference to the ${style_struct.name} struct.
+ #[inline]
+ pub fn get_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} {
+ &self.${style_struct.ident}
+ }
+
+ /// Get a mutable reference to the ${style_struct.name} struct.
+ #[inline]
+ pub fn mutate_${style_struct.name_lower}(&mut self) -> &mut style_structs::${style_struct.name} {
+ Arc::make_mut(&mut self.${style_struct.ident})
+ }
+ % endfor
+
+ /// Gets a reference to the rule node. Panic if no rule node exists.
+ pub fn rules(&self) -> &StrongRuleNode {
+ self.rules.as_ref().unwrap()
+ }
+
+ #[inline]
+ /// Returns whether the "content" property for the given style is completely
+ /// ineffective, and would yield an empty `::before` or `::after`
+ /// pseudo-element.
+ pub fn ineffective_content_property(&self) -> bool {
+ use crate::values::generics::counters::Content;
+ match self.get_counters().content {
+ Content::Normal | Content::None => true,
+ Content::Items(ref items) => items.is_empty(),
+ }
+ }
+
+ /// Whether the current style or any of its ancestors is multicolumn.
+ #[inline]
+ pub fn can_be_fragmented(&self) -> bool {
+ self.flags.contains(ComputedValueFlags::CAN_BE_FRAGMENTED)
+ }
+
+ /// Whether the current style is multicolumn.
+ #[inline]
+ pub fn is_multicol(&self) -> bool {
+ self.get_column().is_multicol()
+ }
+
+ /// Get the logical computed inline size.
+ #[inline]
+ pub fn content_inline_size(&self) -> &computed::Size {
+ let position_style = self.get_position();
+ if self.writing_mode.is_vertical() {
+ &position_style.height
+ } else {
+ &position_style.width
+ }
+ }
+
+ /// Get the logical computed block size.
+ #[inline]
+ pub fn content_block_size(&self) -> &computed::Size {
+ let position_style = self.get_position();
+ if self.writing_mode.is_vertical() { &position_style.width } else { &position_style.height }
+ }
+
+ /// Get the logical computed min inline size.
+ #[inline]
+ pub fn min_inline_size(&self) -> &computed::Size {
+ let position_style = self.get_position();
+ if self.writing_mode.is_vertical() { &position_style.min_height } else { &position_style.min_width }
+ }
+
+ /// Get the logical computed min block size.
+ #[inline]
+ pub fn min_block_size(&self) -> &computed::Size {
+ let position_style = self.get_position();
+ if self.writing_mode.is_vertical() { &position_style.min_width } else { &position_style.min_height }
+ }
+
+ /// Get the logical computed max inline size.
+ #[inline]
+ pub fn max_inline_size(&self) -> &computed::MaxSize {
+ let position_style = self.get_position();
+ if self.writing_mode.is_vertical() { &position_style.max_height } else { &position_style.max_width }
+ }
+
+ /// Get the logical computed max block size.
+ #[inline]
+ pub fn max_block_size(&self) -> &computed::MaxSize {
+ let position_style = self.get_position();
+ if self.writing_mode.is_vertical() { &position_style.max_width } else { &position_style.max_height }
+ }
+
+ /// Get the logical computed padding for this writing mode.
+ #[inline]
+ pub fn logical_padding(&self) -> LogicalMargin<<&computed::LengthPercentage> {
+ let padding_style = self.get_padding();
+ LogicalMargin::from_physical(self.writing_mode, SideOffsets2D::new(
+ &padding_style.padding_top.0,
+ &padding_style.padding_right.0,
+ &padding_style.padding_bottom.0,
+ &padding_style.padding_left.0,
+ ))
+ }
+
+ /// Get the logical border width
+ #[inline]
+ pub fn border_width_for_writing_mode(&self, writing_mode: WritingMode) -> LogicalMargin<Au> {
+ let border_style = self.get_border();
+ LogicalMargin::from_physical(writing_mode, SideOffsets2D::new(
+ Au::from(border_style.border_top_width),
+ Au::from(border_style.border_right_width),
+ Au::from(border_style.border_bottom_width),
+ Au::from(border_style.border_left_width),
+ ))
+ }
+
+ /// Gets the logical computed border widths for this style.
+ #[inline]
+ pub fn logical_border_width(&self) -> LogicalMargin<Au> {
+ self.border_width_for_writing_mode(self.writing_mode)
+ }
+
+ /// Gets the logical computed margin from this style.
+ #[inline]
+ pub fn logical_margin(&self) -> LogicalMargin<<&computed::LengthPercentageOrAuto> {
+ let margin_style = self.get_margin();
+ LogicalMargin::from_physical(self.writing_mode, SideOffsets2D::new(
+ &margin_style.margin_top,
+ &margin_style.margin_right,
+ &margin_style.margin_bottom,
+ &margin_style.margin_left,
+ ))
+ }
+
+ /// Gets the logical position from this style.
+ #[inline]
+ pub fn logical_position(&self) -> LogicalMargin<<&computed::LengthPercentageOrAuto> {
+ // FIXME(SimonSapin): should be the writing mode of the containing block, maybe?
+ let position_style = self.get_position();
+ LogicalMargin::from_physical(self.writing_mode, SideOffsets2D::new(
+ &position_style.top,
+ &position_style.right,
+ &position_style.bottom,
+ &position_style.left,
+ ))
+ }
+
+ /// Return true if the effects force the transform style to be Flat
+ pub fn overrides_transform_style(&self) -> bool {
+ use crate::computed_values::mix_blend_mode::T as MixBlendMode;
+
+ let effects = self.get_effects();
+ // TODO(gw): Add clip-path, isolation, mask-image, mask-border-source when supported.
+ effects.opacity < 1.0 ||
+ !effects.filter.0.is_empty() ||
+ !effects.clip.is_auto() ||
+ effects.mix_blend_mode != MixBlendMode::Normal
+ }
+
+ /// <https://drafts.csswg.org/css-transforms/#grouping-property-values>
+ pub fn get_used_transform_style(&self) -> computed_values::transform_style::T {
+ use crate::computed_values::transform_style::T as TransformStyle;
+
+ let box_ = self.get_box();
+
+ if self.overrides_transform_style() {
+ TransformStyle::Flat
+ } else {
+ // Return the computed value if not overridden by the above exceptions
+ box_.transform_style
+ }
+ }
+
+ /// Whether given this transform value, the compositor would require a
+ /// layer.
+ pub fn transform_requires_layer(&self) -> bool {
+ use crate::values::generics::transform::TransformOperation;
+ // Check if the transform matrix is 2D or 3D
+ for transform in &*self.get_box().transform.0 {
+ match *transform {
+ TransformOperation::Perspective(..) => {
+ return true;
+ }
+ TransformOperation::Matrix3D(m) => {
+ // See http://dev.w3.org/csswg/css-transforms/#2d-matrix
+ if m.m31 != 0.0 || m.m32 != 0.0 ||
+ m.m13 != 0.0 || m.m23 != 0.0 ||
+ m.m43 != 0.0 || m.m14 != 0.0 ||
+ m.m24 != 0.0 || m.m34 != 0.0 ||
+ m.m33 != 1.0 || m.m44 != 1.0 {
+ return true;
+ }
+ }
+ TransformOperation::Translate3D(_, _, z) |
+ TransformOperation::TranslateZ(z) => {
+ if z.px() != 0. {
+ return true;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ // Neither perspective nor transform present
+ false
+ }
+}
+
+/// A reference to a style struct of the parent, or our own style struct.
+pub enum StyleStructRef<'a, T: 'static> {
+ /// A borrowed struct from the parent, for example, for inheriting style.
+ Borrowed(&'a T),
+ /// An owned struct, that we've already mutated.
+ Owned(UniqueArc<T>),
+ /// Temporarily vacated, will panic if accessed
+ Vacated,
+}
+
+impl<'a, T: 'a> StyleStructRef<'a, T>
+where
+ T: Clone,
+{
+ /// Ensure a mutable reference of this value exists, either cloning the
+ /// borrowed value, or returning the owned one.
+ pub fn mutate(&mut self) -> &mut T {
+ if let StyleStructRef::Borrowed(v) = *self {
+ *self = StyleStructRef::Owned(UniqueArc::new(v.clone()));
+ }
+
+ match *self {
+ StyleStructRef::Owned(ref mut v) => v,
+ StyleStructRef::Borrowed(..) => unreachable!(),
+ StyleStructRef::Vacated => panic!("Accessed vacated style struct")
+ }
+ }
+
+ /// Whether this is pointer-equal to the struct we're going to copy the
+ /// value from.
+ ///
+ /// This is used to avoid allocations when people write stuff like `font:
+ /// inherit` or such `all: initial`.
+ #[inline]
+ pub fn ptr_eq(&self, struct_to_copy_from: &T) -> bool {
+ match *self {
+ StyleStructRef::Owned(..) => false,
+ StyleStructRef::Borrowed(s) => {
+ s as *const T == struct_to_copy_from as *const T
+ }
+ StyleStructRef::Vacated => panic!("Accessed vacated style struct")
+ }
+ }
+
+ /// Extract a unique Arc from this struct, vacating it.
+ ///
+ /// The vacated state is a transient one, please put the Arc back
+ /// when done via `put()`. This function is to be used to separate
+ /// the struct being mutated from the computed context
+ pub fn take(&mut self) -> UniqueArc<T> {
+ use std::mem::replace;
+ let inner = replace(self, StyleStructRef::Vacated);
+
+ match inner {
+ StyleStructRef::Owned(arc) => arc,
+ StyleStructRef::Borrowed(s) => UniqueArc::new(s.clone()),
+ StyleStructRef::Vacated => panic!("Accessed vacated style struct"),
+ }
+ }
+
+ /// Replace vacated ref with an arc
+ pub fn put(&mut self, arc: UniqueArc<T>) {
+ debug_assert!(matches!(*self, StyleStructRef::Vacated));
+ *self = StyleStructRef::Owned(arc);
+ }
+
+ /// Get a mutable reference to the owned struct, or `None` if the struct
+ /// hasn't been mutated.
+ pub fn get_if_mutated(&mut self) -> Option<<&mut T> {
+ match *self {
+ StyleStructRef::Owned(ref mut v) => Some(v),
+ StyleStructRef::Borrowed(..) => None,
+ StyleStructRef::Vacated => panic!("Accessed vacated style struct")
+ }
+ }
+
+ /// Returns an `Arc` to the internal struct, constructing one if
+ /// appropriate.
+ pub fn build(self) -> Arc<T> {
+ match self {
+ StyleStructRef::Owned(v) => v.shareable(),
+ // SAFETY: We know all style structs are arc-allocated.
+ StyleStructRef::Borrowed(v) => unsafe { Arc::from_raw_addrefed(v) },
+ StyleStructRef::Vacated => panic!("Accessed vacated style struct")
+ }
+ }
+}
+
+impl<'a, T: 'a> ops::Deref for StyleStructRef<'a, T> {
+ type Target = T;
+
+ fn deref(&self) -> &T {
+ match *self {
+ StyleStructRef::Owned(ref v) => &**v,
+ StyleStructRef::Borrowed(v) => v,
+ StyleStructRef::Vacated => panic!("Accessed vacated style struct")
+ }
+ }
+}
+
+/// A type used to compute a struct with minimal overhead.
+///
+/// This allows holding references to the parent/default computed values without
+/// actually cloning them, until we either build the style, or mutate the
+/// inherited value.
+pub struct StyleBuilder<'a> {
+ /// The device we're using to compute style.
+ ///
+ /// This provides access to viewport unit ratios, etc.
+ pub device: &'a Device,
+
+ /// The stylist we're using to compute style except for media queries.
+ /// device is used in media queries instead.
+ pub stylist: Option<<&'a Stylist>,
+
+ /// The style we're inheriting from.
+ ///
+ /// This is effectively
+ /// `parent_style.unwrap_or(device.default_computed_values())`.
+ inherited_style: &'a ComputedValues,
+
+ /// The style we're getting reset structs from.
+ reset_style: &'a ComputedValues,
+
+ /// The rule node representing the ordered list of rules matched for this
+ /// node.
+ pub rules: Option<StrongRuleNode>,
+
+ /// The computed custom properties.
+ pub custom_properties: crate::custom_properties::ComputedCustomProperties,
+
+ /// Non-custom properties that are considered invalid at compute time
+ /// due to cyclic dependencies with custom properties.
+ /// e.g. `--foo: 1em; font-size: var(--foo)` where `--foo` is registered.
+ pub invalid_non_custom_properties: LonghandIdSet,
+
+ /// The pseudo-element this style will represent.
+ pub pseudo: Option<<&'a PseudoElement>,
+
+ /// Whether we have mutated any reset structs since the the last time
+ /// `clear_modified_reset` was called. This is used to tell whether the
+ /// `StyleAdjuster` did any work.
+ modified_reset: bool,
+
+ /// Whether this is the style for the root element.
+ pub is_root_element: bool,
+
+ /// The writing mode flags.
+ ///
+ /// TODO(emilio): Make private.
+ pub writing_mode: WritingMode,
+
+ /// The effective zoom.
+ pub effective_zoom: computed::Zoom,
+
+ /// Flags for the computed value.
+ pub flags: Cell<ComputedValueFlags>,
+
+ /// The element's style if visited, only computed if there's a relevant link
+ /// for this element. A element's "relevant link" is the element being
+ /// matched if it is a link or the nearest ancestor link.
+ pub visited_style: Option<Arc<ComputedValues>>,
+ % for style_struct in data.active_style_structs():
+ ${style_struct.ident}: StyleStructRef<'a, style_structs::${style_struct.name}>,
+ % endfor
+}
+
+impl<'a> StyleBuilder<'a> {
+ /// Trivially construct a `StyleBuilder`.
+ pub fn new(
+ device: &'a Device,
+ stylist: Option<<&'a Stylist>,
+ parent_style: Option<<&'a ComputedValues>,
+ pseudo: Option<<&'a PseudoElement>,
+ rules: Option<StrongRuleNode>,
+ is_root_element: bool,
+ ) -> Self {
+ let reset_style = device.default_computed_values();
+ let inherited_style = parent_style.unwrap_or(reset_style);
+
+ let flags = inherited_style.flags.inherited();
+ StyleBuilder {
+ device,
+ stylist,
+ inherited_style,
+ reset_style,
+ pseudo,
+ rules,
+ modified_reset: false,
+ is_root_element,
+ custom_properties: crate::custom_properties::ComputedCustomProperties::default(),
+ invalid_non_custom_properties: LonghandIdSet::default(),
+ writing_mode: inherited_style.writing_mode,
+ effective_zoom: inherited_style.effective_zoom,
+ flags: Cell::new(flags),
+ visited_style: None,
+ % for style_struct in data.active_style_structs():
+ % if style_struct.inherited:
+ ${style_struct.ident}: StyleStructRef::Borrowed(inherited_style.get_${style_struct.name_lower}()),
+ % else:
+ ${style_struct.ident}: StyleStructRef::Borrowed(reset_style.get_${style_struct.name_lower}()),
+ % endif
+ % endfor
+ }
+ }
+
+ /// NOTE(emilio): This is done so we can compute relative units with respect
+ /// to the parent style, but all the early properties / writing-mode / etc
+ /// are already set to the right ones on the kid.
+ ///
+ /// Do _not_ actually call this to construct a style, this should mostly be
+ /// used for animations.
+ pub fn for_animation(
+ device: &'a Device,
+ stylist: Option<<&'a Stylist>,
+ style_to_derive_from: &'a ComputedValues,
+ parent_style: Option<<&'a ComputedValues>,
+ ) -> Self {
+ let reset_style = device.default_computed_values();
+ let inherited_style = parent_style.unwrap_or(reset_style);
+ StyleBuilder {
+ device,
+ stylist,
+ inherited_style,
+ reset_style,
+ pseudo: None,
+ modified_reset: false,
+ is_root_element: false,
+ rules: None,
+ custom_properties: style_to_derive_from.custom_properties().clone(),
+ invalid_non_custom_properties: LonghandIdSet::default(),
+ writing_mode: style_to_derive_from.writing_mode,
+ effective_zoom: style_to_derive_from.effective_zoom,
+ flags: Cell::new(style_to_derive_from.flags),
+ visited_style: None,
+ % for style_struct in data.active_style_structs():
+ ${style_struct.ident}: StyleStructRef::Borrowed(
+ style_to_derive_from.get_${style_struct.name_lower}()
+ ),
+ % endfor
+ }
+ }
+
+ /// Copy the reset properties from `style`.
+ pub fn copy_reset_from(&mut self, style: &'a ComputedValues) {
+ % for style_struct in data.active_style_structs():
+ % if not style_struct.inherited:
+ self.${style_struct.ident} =
+ StyleStructRef::Borrowed(style.get_${style_struct.name_lower}());
+ % endif
+ % endfor
+ }
+
+ % for property in data.longhands:
+ % if not property.logical:
+ % if not property.style_struct.inherited:
+ /// Inherit `${property.ident}` from our parent style.
+ #[allow(non_snake_case)]
+ pub fn inherit_${property.ident}(&mut self) {
+ let inherited_struct =
+ self.inherited_style.get_${property.style_struct.name_lower}();
+
+ self.modified_reset = true;
+ self.add_flags(ComputedValueFlags::INHERITS_RESET_STYLE);
+
+ % if property.ident == "content":
+ self.add_flags(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE);
+ % endif
+
+ % if property.ident == "display":
+ self.add_flags(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE);
+ % endif
+
+ if self.${property.style_struct.ident}.ptr_eq(inherited_struct) {
+ return;
+ }
+
+ self.${property.style_struct.ident}.mutate()
+ .copy_${property.ident}_from(inherited_struct);
+ }
+ % else:
+ /// Reset `${property.ident}` to the initial value.
+ #[allow(non_snake_case)]
+ pub fn reset_${property.ident}(&mut self) {
+ let reset_struct =
+ self.reset_style.get_${property.style_struct.name_lower}();
+
+ if self.${property.style_struct.ident}.ptr_eq(reset_struct) {
+ return;
+ }
+
+ self.${property.style_struct.ident}.mutate()
+ .reset_${property.ident}(reset_struct);
+ }
+ % endif
+
+ % if not property.is_vector or property.simple_vector_bindings or engine in ["servo-2013", "servo-2020"]:
+ /// Set the `${property.ident}` to the computed value `value`.
+ #[allow(non_snake_case)]
+ pub fn set_${property.ident}(
+ &mut self,
+ value: longhands::${property.ident}::computed_value::T
+ ) {
+ % if not property.style_struct.inherited:
+ self.modified_reset = true;
+ % endif
+
+ self.${property.style_struct.ident}.mutate()
+ .set_${property.ident}(
+ value,
+ % if property.logical:
+ self.writing_mode,
+ % endif
+ );
+ }
+ % endif
+ % endif
+ % endfor
+ <% del property %>
+
+ /// Inherits style from the parent element, accounting for the default
+ /// computed values that need to be provided as well.
+ pub fn for_inheritance(
+ device: &'a Device,
+ stylist: Option<<&'a Stylist>,
+ parent: Option<<&'a ComputedValues>,
+ pseudo: Option<<&'a PseudoElement>,
+ ) -> Self {
+ // Rebuild the visited style from the parent, ensuring that it will also
+ // not have rules. This matches the unvisited style that will be
+ // produced by this builder. This assumes that the caller doesn't need
+ // to adjust or process visited style, so we can just build visited
+ // style here for simplicity.
+ let visited_style = parent.and_then(|parent| {
+ parent.visited_style().map(|style| {
+ Self::for_inheritance(
+ device,
+ stylist,
+ Some(style),
+ pseudo,
+ ).build()
+ })
+ });
+ let custom_properties = if let Some(p) = parent { p.custom_properties().clone() } else { crate::custom_properties::ComputedCustomProperties::default() };
+ let mut ret = Self::new(
+ device,
+ stylist,
+ parent,
+ pseudo,
+ /* rules = */ None,
+ /* is_root_element = */ false,
+ );
+ ret.custom_properties = custom_properties;
+ ret.visited_style = visited_style;
+ ret
+ }
+
+ /// Returns whether we have a visited style.
+ pub fn has_visited_style(&self) -> bool {
+ self.visited_style.is_some()
+ }
+
+ /// Returns whether we're a pseudo-elements style.
+ pub fn is_pseudo_element(&self) -> bool {
+ self.pseudo.map_or(false, |p| !p.is_anon_box())
+ }
+
+ /// Returns the style we're getting reset properties from.
+ pub fn default_style(&self) -> &'a ComputedValues {
+ self.reset_style
+ }
+
+ % for style_struct in data.active_style_structs():
+ /// Gets an immutable view of the current `${style_struct.name}` style.
+ pub fn get_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} {
+ &self.${style_struct.ident}
+ }
+
+ /// Gets a mutable view of the current `${style_struct.name}` style.
+ pub fn mutate_${style_struct.name_lower}(&mut self) -> &mut style_structs::${style_struct.name} {
+ % if not style_struct.inherited:
+ self.modified_reset = true;
+ % endif
+ self.${style_struct.ident}.mutate()
+ }
+
+ /// Gets a mutable view of the current `${style_struct.name}` style.
+ pub fn take_${style_struct.name_lower}(&mut self) -> UniqueArc<style_structs::${style_struct.name}> {
+ % if not style_struct.inherited:
+ self.modified_reset = true;
+ % endif
+ self.${style_struct.ident}.take()
+ }
+
+ /// Gets a mutable view of the current `${style_struct.name}` style.
+ pub fn put_${style_struct.name_lower}(&mut self, s: UniqueArc<style_structs::${style_struct.name}>) {
+ self.${style_struct.ident}.put(s)
+ }
+
+ /// Gets a mutable view of the current `${style_struct.name}` style,
+ /// only if it's been mutated before.
+ pub fn get_${style_struct.name_lower}_if_mutated(&mut self)
+ -> Option<<&mut style_structs::${style_struct.name}> {
+ self.${style_struct.ident}.get_if_mutated()
+ }
+
+ /// Reset the current `${style_struct.name}` style to its default value.
+ pub fn reset_${style_struct.name_lower}_struct(&mut self) {
+ self.${style_struct.ident} =
+ StyleStructRef::Borrowed(self.reset_style.get_${style_struct.name_lower}());
+ }
+ % endfor
+ <% del style_struct %>
+
+ /// Returns whether this computed style represents a floated object.
+ pub fn is_floating(&self) -> bool {
+ self.get_box().clone_float().is_floating()
+ }
+
+ /// Returns whether this computed style represents an absolutely-positioned
+ /// object.
+ pub fn is_absolutely_positioned(&self) -> bool {
+ self.get_box().clone_position().is_absolutely_positioned()
+ }
+
+ /// Whether this style has a top-layer style.
+ #[cfg(feature = "servo")]
+ pub fn in_top_layer(&self) -> bool {
+ matches!(self.get_box().clone__servo_top_layer(),
+ longhands::_servo_top_layer::computed_value::T::Top)
+ }
+
+ /// Whether this style has a top-layer style.
+ #[cfg(feature = "gecko")]
+ pub fn in_top_layer(&self) -> bool {
+ matches!(self.get_box().clone__moz_top_layer(),
+ longhands::_moz_top_layer::computed_value::T::Top)
+ }
+
+ /// Clears the "have any reset structs been modified" flag.
+ pub fn clear_modified_reset(&mut self) {
+ self.modified_reset = false;
+ }
+
+ /// Returns whether we have mutated any reset structs since the the last
+ /// time `clear_modified_reset` was called.
+ pub fn modified_reset(&self) -> bool {
+ self.modified_reset
+ }
+
+ /// Return the current flags.
+ #[inline]
+ pub fn flags(&self) -> ComputedValueFlags {
+ self.flags.get()
+ }
+
+ /// Add a flag to the current builder.
+ #[inline]
+ pub fn add_flags(&self, flag: ComputedValueFlags) {
+ let flags = self.flags() | flag;
+ self.flags.set(flags);
+ }
+
+ /// Removes a flag to the current builder.
+ #[inline]
+ pub fn remove_flags(&self, flag: ComputedValueFlags) {
+ let flags = self.flags() & !flag;
+ self.flags.set(flags);
+ }
+
+ /// Turns this `StyleBuilder` into a proper `ComputedValues` instance.
+ pub fn build(self) -> Arc<ComputedValues> {
+ ComputedValues::new(
+ self.pseudo,
+ self.custom_properties,
+ self.writing_mode,
+ self.effective_zoom,
+ self.flags.get(),
+ self.rules,
+ self.visited_style,
+ % for style_struct in data.active_style_structs():
+ self.${style_struct.ident}.build(),
+ % endfor
+ )
+ }
+
+ /// Get the custom properties map if necessary.
+ pub fn custom_properties(&self) -> &crate::custom_properties::ComputedCustomProperties {
+ &self.custom_properties
+ }
+
+
+ /// Get the inherited custom properties map.
+ pub fn inherited_custom_properties(&self) -> &crate::custom_properties::ComputedCustomProperties {
+ &self.inherited_style.custom_properties
+ }
+
+ /// Access to various information about our inherited styles. We don't
+ /// expose an inherited ComputedValues directly, because in the
+ /// ::first-line case some of the inherited information needs to come from
+ /// one ComputedValues instance and some from a different one.
+
+ /// Inherited writing-mode.
+ pub fn inherited_writing_mode(&self) -> &WritingMode {
+ &self.inherited_style.writing_mode
+ }
+
+ /// The effective zoom value that we should multiply absolute lengths by.
+ pub fn effective_zoom(&self) -> computed::Zoom {
+ self.effective_zoom
+ }
+
+ /// The zoom specified on this element.
+ pub fn specified_zoom(&self) -> computed::Zoom {
+ self.get_box().clone_zoom()
+ }
+
+ /// Inherited zoom.
+ pub fn inherited_effective_zoom(&self) -> computed::Zoom {
+ self.inherited_style.effective_zoom
+ }
+
+ /// The computed value flags of our parent.
+ #[inline]
+ pub fn get_parent_flags(&self) -> ComputedValueFlags {
+ self.inherited_style.flags
+ }
+
+ /// Calculate the line height, given the currently resolved line-height and font.
+ pub fn calc_line_height(
+ &self,
+ device: &Device,
+ line_height_base: LineHeightBase,
+ writing_mode: WritingMode,
+ ) -> computed::NonNegativeLength {
+ use crate::computed_value_flags::ComputedValueFlags;
+ let (font, flag) = match line_height_base {
+ LineHeightBase::CurrentStyle => (
+ self.get_font(),
+ ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS,
+ ),
+ LineHeightBase::InheritedStyle => (
+ self.get_parent_font(),
+ ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS,
+ ),
+ };
+ let line_height = font.clone_line_height();
+ if matches!(line_height, computed::LineHeight::Normal) {
+ self.add_flags(flag);
+ }
+ device.calc_line_height(&font, writing_mode, None)
+ }
+
+ /// And access to inherited style structs.
+ % for style_struct in data.active_style_structs():
+ /// Gets our inherited `${style_struct.name}`. We don't name these
+ /// accessors `inherited_${style_struct.name_lower}` because we already
+ /// have things like "box" vs "inherited_box" as struct names. Do the
+ /// next-best thing and call them `parent_${style_struct.name_lower}`
+ /// instead.
+ pub fn get_parent_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} {
+ self.inherited_style.get_${style_struct.name_lower}()
+ }
+ % endfor
+}
+
+#[cfg(feature = "servo")]
+pub use self::lazy_static_module::INITIAL_SERVO_VALUES;
+
+// Use a module to work around #[cfg] on lazy_static! not being applied to every generated item.
+#[cfg(feature = "servo")]
+#[allow(missing_docs)]
+mod lazy_static_module {
+ use crate::logical_geometry::WritingMode;
+ use crate::computed_value_flags::ComputedValueFlags;
+ use servo_arc::Arc;
+ use super::{ComputedValues, ComputedValuesInner, longhands, style_structs};
+
+ lazy_static! {
+ /// The initial values for all style structs as defined by the specification.
+ pub static ref INITIAL_SERVO_VALUES: ComputedValues = ComputedValues {
+ inner: ComputedValuesInner {
+ % for style_struct in data.active_style_structs():
+ ${style_struct.ident}: Arc::new(style_structs::${style_struct.name} {
+ % for longhand in style_struct.longhands:
+ % if not longhand.logical:
+ ${longhand.ident}: longhands::${longhand.ident}::get_initial_value(),
+ % endif
+ % endfor
+ % if style_struct.name == "InheritedText":
+ text_decorations_in_effect:
+ crate::values::computed::text::TextDecorationsInEffect::default(),
+ % endif
+ % if style_struct.name == "Font":
+ hash: 0,
+ % endif
+ % if style_struct.name == "Box":
+ original_display: longhands::display::get_initial_value(),
+ % endif
+ }),
+ % endfor
+ custom_properties,
+ writing_mode: WritingMode::empty(),
+ rules: None,
+ visited_style: None,
+ flags: ComputedValueFlags::empty(),
+ },
+ pseudo: None,
+ };
+ }
+}
+
+/// A per-longhand function that performs the CSS cascade for that longhand.
+pub type CascadePropertyFn =
+ unsafe extern "Rust" fn(
+ declaration: &PropertyDeclaration,
+ context: &mut computed::Context,
+ );
+
+/// A per-longhand array of functions to perform the CSS cascade on each of
+/// them, effectively doing virtual dispatch.
+pub static CASCADE_PROPERTY: [CascadePropertyFn; ${len(data.longhands)}] = [
+ % for property in data.longhands:
+ longhands::${property.ident}::cascade_property,
+ % endfor
+];
+
+/// See StyleAdjuster::adjust_for_border_width.
+pub fn adjust_border_width(style: &mut StyleBuilder) {
+ % for side in ["top", "right", "bottom", "left"]:
+ // Like calling to_computed_value, which wouldn't type check.
+ if style.get_border().clone_border_${side}_style().none_or_hidden() &&
+ style.get_border().border_${side}_has_nonzero_width() {
+ style.set_border_${side}_width(Au(0));
+ }
+ % endfor
+}
+
+/// An identifier for a given alias property.
+#[derive(Clone, Copy, Eq, PartialEq, MallocSizeOf)]
+#[repr(u16)]
+pub enum AliasId {
+ % for i, property in enumerate(data.all_aliases()):
+ /// ${property.name}
+ ${property.camel_case} = ${i},
+ % endfor
+}
+
+impl fmt::Debug for AliasId {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ let name = NonCustomPropertyId::from(*self).name();
+ formatter.write_str(name)
+ }
+}
+
+impl AliasId {
+ /// Returns the property we're aliasing, as a longhand or a shorthand.
+ #[inline]
+ pub fn aliased_property(self) -> NonCustomPropertyId {
+ static MAP: [NonCustomPropertyId; ${len(data.all_aliases())}] = [
+ % for alias in data.all_aliases():
+ % if alias.original.type() == "longhand":
+ NonCustomPropertyId::from_longhand(LonghandId::${alias.original.camel_case}),
+ % else:
+ <% assert alias.original.type() == "shorthand" %>
+ NonCustomPropertyId::from_shorthand(ShorthandId::${alias.original.camel_case}),
+ % endif
+ % endfor
+ ];
+ MAP[self as usize]
+ }
+}
+
+/// Call the given macro with tokens like this for each longhand and shorthand properties
+/// that is enabled in content:
+///
+/// ```
+/// [CamelCaseName, SetCamelCaseName, PropertyId::Longhand(LonghandId::CamelCaseName)],
+/// ```
+///
+/// NOTE(emilio): Callers are responsible to deal with prefs.
+#[macro_export]
+macro_rules! css_properties_accessors {
+ ($macro_name: ident) => {
+ $macro_name! {
+ % for kind, props in [("Longhand", data.longhands), ("Shorthand", data.shorthands)]:
+ % for property in props:
+ % if property.enabled_in_content():
+ % for prop in [property] + property.aliases:
+ % if '-' in prop.name:
+ [${prop.ident.capitalize()}, Set${prop.ident.capitalize()},
+ PropertyId::${kind}(${kind}Id::${property.camel_case})],
+ % endif
+ [${prop.camel_case}, Set${prop.camel_case},
+ PropertyId::${kind}(${kind}Id::${property.camel_case})],
+ % endfor
+ % endif
+ % endfor
+ % endfor
+ }
+ }
+}
+
+/// Call the given macro with tokens like this for each longhand properties:
+///
+/// ```
+/// { snake_case_ident }
+/// ```
+#[macro_export]
+macro_rules! longhand_properties_idents {
+ ($macro_name: ident) => {
+ $macro_name! {
+ % for property in data.longhands:
+ { ${property.ident} }
+ % endfor
+ }
+ }
+}
+
+// Large pages generate tens of thousands of ComputedValues.
+size_of_test!(ComputedValues, 240);
+// FFI relies on this.
+size_of_test!(Option<Arc<ComputedValues>>, 8);
+
+// There are two reasons for this test to fail:
+//
+// * Your changes made a specified value type for a given property go
+// over the threshold. In that case, you should try to shrink it again
+// or, if not possible, mark the property as boxed in the property
+// definition.
+//
+// * Your changes made a specified value type smaller, so that it no
+// longer needs to be boxed. In this case you just need to remove
+// boxed=True from the property definition. Nice job!
+#[cfg(target_pointer_width = "64")]
+#[allow(dead_code)] // https://github.com/rust-lang/rust/issues/96952
+const BOX_THRESHOLD: usize = 24;
+% for longhand in data.longhands:
+#[cfg(target_pointer_width = "64")]
+% if longhand.boxed:
+const_assert!(std::mem::size_of::<longhands::${longhand.ident}::SpecifiedValue>() > BOX_THRESHOLD);
+% else:
+const_assert!(std::mem::size_of::<longhands::${longhand.ident}::SpecifiedValue>() <= BOX_THRESHOLD);
+% endif
+% endfor
+
+% if engine in ["servo-2013", "servo-2020"]:
+% for effect_name in ["repaint", "reflow_out_of_flow", "reflow", "rebuild_and_reflow_inline", "rebuild_and_reflow"]:
+ macro_rules! restyle_damage_${effect_name} {
+ ($old: ident, $new: ident, $damage: ident, [ $($effect:expr),* ]) => ({
+ if
+ % for style_struct in data.active_style_structs():
+ % for longhand in style_struct.longhands:
+ % if effect_name in longhand.servo_restyle_damage.split() and not longhand.logical:
+ $old.get_${style_struct.name_lower}().${longhand.ident} !=
+ $new.get_${style_struct.name_lower}().${longhand.ident} ||
+ % endif
+ % endfor
+ % endfor
+
+ false {
+ $damage.insert($($effect)|*);
+ true
+ } else {
+ false
+ }
+ })
+ }
+% endfor
+% endif
diff --git a/servo/components/style/properties/shorthands/background.mako.rs b/servo/components/style/properties/shorthands/background.mako.rs
new file mode 100644
index 0000000000..08838233f6
--- /dev/null
+++ b/servo/components/style/properties/shorthands/background.mako.rs
@@ -0,0 +1,289 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+// TODO: other background-* properties
+<%helpers:shorthand name="background"
+ engines="gecko servo-2013 servo-2020"
+ sub_properties="background-color background-position-x background-position-y background-repeat
+ background-attachment background-image background-size background-origin
+ background-clip"
+ spec="https://drafts.csswg.org/css-backgrounds/#the-background">
+ use crate::properties::longhands::{background_position_x, background_position_y, background_repeat};
+ use crate::properties::longhands::{background_attachment, background_color, background_image, background_size, background_origin};
+ use crate::properties::longhands::background_clip;
+ use crate::properties::longhands::background_clip::single_value::computed_value::T as Clip;
+ use crate::properties::longhands::background_origin::single_value::computed_value::T as Origin;
+ use crate::values::specified::{AllowQuirks, Color, Position, PositionComponent};
+ use crate::parser::Parse;
+
+ // FIXME(emilio): Should be the same type!
+ impl From<background_origin::single_value::SpecifiedValue> for background_clip::single_value::SpecifiedValue {
+ fn from(origin: background_origin::single_value::SpecifiedValue) ->
+ background_clip::single_value::SpecifiedValue {
+ match origin {
+ background_origin::single_value::SpecifiedValue::ContentBox =>
+ background_clip::single_value::SpecifiedValue::ContentBox,
+ background_origin::single_value::SpecifiedValue::PaddingBox =>
+ background_clip::single_value::SpecifiedValue::PaddingBox,
+ background_origin::single_value::SpecifiedValue::BorderBox =>
+ background_clip::single_value::SpecifiedValue::BorderBox,
+ }
+ }
+ }
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut background_color = None;
+
+ % for name in "image position_x position_y repeat size attachment origin clip".split():
+ // Vec grows from 0 to 4 by default on first push(). So allocate with
+ // capacity 1, so in the common case of only one item we don't way
+ // overallocate, then shrink. Note that we always push at least one
+ // item if parsing succeeds.
+ let mut background_${name} = Vec::with_capacity(1);
+ % endfor
+ input.parse_comma_separated(|input| {
+ // background-color can only be in the last element, so if it
+ // is parsed anywhere before, the value is invalid.
+ if background_color.is_some() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ % for name in "image position repeat size attachment origin clip".split():
+ let mut ${name} = None;
+ % endfor
+ loop {
+ if background_color.is_none() {
+ if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) {
+ background_color = Some(value);
+ continue
+ }
+ }
+ if position.is_none() {
+ if let Ok(value) = input.try_parse(|input| {
+ Position::parse_three_value_quirky(context, input, AllowQuirks::No)
+ }) {
+ position = Some(value);
+
+ // Parse background size, if applicable.
+ size = input.try_parse(|input| {
+ input.expect_delim('/')?;
+ background_size::single_value::parse(context, input)
+ }).ok();
+
+ continue
+ }
+ }
+ % for name in "image repeat attachment origin clip".split():
+ if ${name}.is_none() {
+ if let Ok(value) = input.try_parse(|input| background_${name}::single_value
+ ::parse(context, input)) {
+ ${name} = Some(value);
+ continue
+ }
+ }
+ % endfor
+ break
+ }
+ if clip.is_none() {
+ if let Some(origin) = origin {
+ clip = Some(background_clip::single_value::SpecifiedValue::from(origin));
+ }
+ }
+ let mut any = false;
+ % for name in "image position repeat size attachment origin clip".split():
+ any = any || ${name}.is_some();
+ % endfor
+ any = any || background_color.is_some();
+ if any {
+ if let Some(position) = position {
+ background_position_x.push(position.horizontal);
+ background_position_y.push(position.vertical);
+ } else {
+ background_position_x.push(PositionComponent::zero());
+ background_position_y.push(PositionComponent::zero());
+ }
+ % for name in "image repeat size attachment origin clip".split():
+ if let Some(bg_${name}) = ${name} {
+ background_${name}.push(bg_${name});
+ } else {
+ background_${name}.push(background_${name}::single_value
+ ::get_initial_specified_value());
+ }
+ % endfor
+ Ok(())
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ })?;
+
+ Ok(expanded! {
+ background_color: background_color.unwrap_or(Color::transparent()),
+ % for name in "image position_x position_y repeat size attachment origin clip".split():
+ background_${name}: background_${name}::SpecifiedValue(background_${name}.into()),
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let len = self.background_image.0.len();
+ // There should be at least one declared value
+ if len == 0 {
+ return Ok(());
+ }
+
+ // If a value list length is differs then we don't do a shorthand serialization.
+ // The exceptions to this is color which appears once only and is serialized
+ // with the last item.
+ % for name in "image position_x position_y size repeat origin clip attachment".split():
+ if len != self.background_${name}.0.len() {
+ return Ok(());
+ }
+ % endfor
+
+ for i in 0..len {
+ % for name in "image position_x position_y repeat size attachment origin clip".split():
+ let ${name} = &self.background_${name}.0[i];
+ % endfor
+
+ if i != 0 {
+ dest.write_str(", ")?;
+ }
+
+ let mut wrote_value = false;
+
+ if i == len - 1 {
+ if *self.background_color != background_color::get_initial_specified_value() {
+ self.background_color.to_css(dest)?;
+ wrote_value = true;
+ }
+ }
+
+ if *image != background_image::single_value::get_initial_specified_value() {
+ if wrote_value {
+ dest.write_char(' ')?;
+ }
+ image.to_css(dest)?;
+ wrote_value = true;
+ }
+
+ // Size is only valid after a position so when there is a
+ // non-initial size we must also serialize position
+ if *position_x != PositionComponent::zero() ||
+ *position_y != PositionComponent::zero() ||
+ *size != background_size::single_value::get_initial_specified_value()
+ {
+ if wrote_value {
+ dest.write_char(' ')?;
+ }
+
+ Position {
+ horizontal: position_x.clone(),
+ vertical: position_y.clone()
+ }.to_css(dest)?;
+
+ wrote_value = true;
+
+ if *size != background_size::single_value::get_initial_specified_value() {
+ dest.write_str(" / ")?;
+ size.to_css(dest)?;
+ }
+ }
+
+ % for name in "repeat attachment".split():
+ if *${name} != background_${name}::single_value::get_initial_specified_value() {
+ if wrote_value {
+ dest.write_char(' ')?;
+ }
+ ${name}.to_css(dest)?;
+ wrote_value = true;
+ }
+ % endfor
+
+ if *origin != Origin::PaddingBox || *clip != Clip::BorderBox {
+ if wrote_value {
+ dest.write_char(' ')?;
+ }
+ origin.to_css(dest)?;
+ if *clip != From::from(*origin) {
+ dest.write_char(' ')?;
+ clip.to_css(dest)?;
+ }
+
+ wrote_value = true;
+ }
+
+ if !wrote_value {
+ image.to_css(dest)?;
+ }
+ }
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand name="background-position"
+ engines="gecko servo-2013 servo-2020"
+ flags="SHORTHAND_IN_GETCS"
+ sub_properties="background-position-x background-position-y"
+ spec="https://drafts.csswg.org/css-backgrounds-4/#the-background-position">
+ use crate::properties::longhands::{background_position_x, background_position_y};
+ use crate::values::specified::AllowQuirks;
+ use crate::values::specified::position::Position;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ // Vec grows from 0 to 4 by default on first push(). So allocate with
+ // capacity 1, so in the common case of only one item we don't way
+ // overallocate, then shrink. Note that we always push at least one
+ // item if parsing succeeds.
+ let mut position_x = Vec::with_capacity(1);
+ let mut position_y = Vec::with_capacity(1);
+ let mut any = false;
+
+ input.parse_comma_separated(|input| {
+ let value = Position::parse_three_value_quirky(context, input, AllowQuirks::Yes)?;
+ position_x.push(value.horizontal);
+ position_y.push(value.vertical);
+ any = true;
+ Ok(())
+ })?;
+ if !any {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(expanded! {
+ background_position_x: background_position_x::SpecifiedValue(position_x.into()),
+ background_position_y: background_position_y::SpecifiedValue(position_y.into()),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let len = self.background_position_x.0.len();
+ if len == 0 || len != self.background_position_y.0.len() {
+ return Ok(());
+ }
+ for i in 0..len {
+ Position {
+ horizontal: self.background_position_x.0[i].clone(),
+ vertical: self.background_position_y.0[i].clone()
+ }.to_css(dest)?;
+
+ if i < len - 1 {
+ dest.write_str(", ")?;
+ }
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/border.mako.rs b/servo/components/style/properties/shorthands/border.mako.rs
new file mode 100644
index 0000000000..c6a87f3197
--- /dev/null
+++ b/servo/components/style/properties/shorthands/border.mako.rs
@@ -0,0 +1,491 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import to_rust_ident, ALL_SIDES, PHYSICAL_SIDES, maybe_moz_logical_alias %>
+
+${helpers.four_sides_shorthand(
+ "border-color",
+ "border-%s-color",
+ "specified::Color::parse",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-backgrounds/#border-color",
+ allow_quirks="Yes",
+)}
+
+${helpers.four_sides_shorthand(
+ "border-style",
+ "border-%s-style",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-backgrounds/#border-style",
+)}
+
+<%helpers:shorthand
+ name="border-width"
+ engines="gecko servo-2013 servo-2020"
+ sub_properties="${
+ ' '.join('border-%s-width' % side
+ for side in PHYSICAL_SIDES)}"
+ spec="https://drafts.csswg.org/css-backgrounds/#border-width">
+ use crate::values::generics::rect::Rect;
+ use crate::values::specified::{AllowQuirks, BorderSideWidth};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let rect = Rect::parse_with(context, input, |_, i| {
+ BorderSideWidth::parse_quirky(context, i, AllowQuirks::Yes)
+ })?;
+ Ok(expanded! {
+ border_top_width: rect.0,
+ border_right_width: rect.1,
+ border_bottom_width: rect.2,
+ border_left_width: rect.3,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ % for side in PHYSICAL_SIDES:
+ let ${side} = &self.border_${side}_width;
+ % endfor
+ Rect::new(top, right, bottom, left).to_css(dest)
+ }
+ }
+</%helpers:shorthand>
+
+
+pub fn parse_border<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<(specified::Color, specified::BorderStyle, specified::BorderSideWidth), ParseError<'i>> {
+ use crate::values::specified::{Color, BorderStyle, BorderSideWidth};
+ let _unused = context;
+ let mut color = None;
+ let mut style = None;
+ let mut width = None;
+ let mut any = false;
+ loop {
+ if width.is_none() {
+ if let Ok(value) = input.try_parse(|i| BorderSideWidth::parse(context, i)) {
+ width = Some(value);
+ any = true;
+ }
+ }
+ if style.is_none() {
+ if let Ok(value) = input.try_parse(BorderStyle::parse) {
+ style = Some(value);
+ any = true;
+ continue
+ }
+ }
+ if color.is_none() {
+ if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) {
+ color = Some(value);
+ any = true;
+ continue
+ }
+ }
+ break
+ }
+ if !any {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ Ok((color.unwrap_or(Color::CurrentColor), style.unwrap_or(BorderStyle::None), width.unwrap_or(BorderSideWidth::medium())))
+}
+
+% for side, logical in ALL_SIDES:
+ <%
+ spec = "https://drafts.csswg.org/css-backgrounds/#border-%s" % side
+ if logical:
+ spec = "https://drafts.csswg.org/css-logical-props/#propdef-border-%s" % side
+ %>
+ <%helpers:shorthand
+ name="border-${side}"
+ engines="gecko servo-2013 servo-2020"
+ sub_properties="${' '.join(
+ 'border-%s-%s' % (side, prop)
+ for prop in ['width', 'style', 'color']
+ )}"
+ aliases="${maybe_moz_logical_alias(engine, (side, logical), '-moz-border-%s')}"
+ spec="${spec}">
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let (color, style, width) = super::parse_border(context, input)?;
+ Ok(expanded! {
+ border_${to_rust_ident(side)}_color: color,
+ border_${to_rust_ident(side)}_style: style,
+ border_${to_rust_ident(side)}_width: width
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ crate::values::specified::border::serialize_directional_border(
+ dest,
+ self.border_${to_rust_ident(side)}_width,
+ self.border_${to_rust_ident(side)}_style,
+ self.border_${to_rust_ident(side)}_color
+ )
+ }
+ }
+
+ </%helpers:shorthand>
+% endfor
+
+<%helpers:shorthand name="border"
+ engines="gecko servo-2013 servo-2020"
+ sub_properties="${' '.join('border-%s-%s' % (side, prop)
+ for side in PHYSICAL_SIDES for prop in ['width', 'style', 'color']
+ )}
+ ${' '.join('border-image-%s' % name
+ for name in ['outset', 'repeat', 'slice', 'source', 'width'])}"
+ derive_value_info="False"
+ spec="https://drafts.csswg.org/css-backgrounds/#border">
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::properties::longhands::{border_image_outset, border_image_repeat, border_image_slice};
+ use crate::properties::longhands::{border_image_source, border_image_width};
+
+ let (color, style, width) = super::parse_border(context, input)?;
+ Ok(expanded! {
+ % for side in PHYSICAL_SIDES:
+ border_${side}_color: color.clone(),
+ border_${side}_style: style,
+ border_${side}_width: width.clone(),
+ % endfor
+
+ // The ‘border’ shorthand resets ‘border-image’ to its initial value.
+ // See https://drafts.csswg.org/css-backgrounds-3/#the-border-shorthands
+ % for name in "outset repeat slice source width".split():
+ border_image_${name}: border_image_${name}::get_initial_specified_value(),
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ use crate::properties::longhands;
+
+ // If any of the border-image longhands differ from their initial specified values we should not
+ // invoke serialize_directional_border(), so there is no point in continuing on to compute all_equal.
+ % for name in "outset repeat slice source width".split():
+ if *self.border_image_${name} != longhands::border_image_${name}::get_initial_specified_value() {
+ return Ok(());
+ }
+ % endfor
+
+ let all_equal = {
+ % for side in PHYSICAL_SIDES:
+ let border_${side}_width = self.border_${side}_width;
+ let border_${side}_style = self.border_${side}_style;
+ let border_${side}_color = self.border_${side}_color;
+ % endfor
+
+ border_top_width == border_right_width &&
+ border_right_width == border_bottom_width &&
+ border_bottom_width == border_left_width &&
+
+ border_top_style == border_right_style &&
+ border_right_style == border_bottom_style &&
+ border_bottom_style == border_left_style &&
+
+ border_top_color == border_right_color &&
+ border_right_color == border_bottom_color &&
+ border_bottom_color == border_left_color
+ };
+
+ // If all longhands are all present, then all sides should be the same,
+ // so we can just one set of color/style/width
+ if !all_equal {
+ return Ok(())
+ }
+ crate::values::specified::border::serialize_directional_border(
+ dest,
+ self.border_${side}_width,
+ self.border_${side}_style,
+ self.border_${side}_color
+ )
+ }
+ }
+
+ // Just use the same as border-left. The border shorthand can't accept
+ // any value that the sub-shorthand couldn't.
+ <%
+ border_left = "<crate::properties::shorthands::border_left::Longhands as SpecifiedValueInfo>"
+ %>
+ impl SpecifiedValueInfo for Longhands {
+ const SUPPORTED_TYPES: u8 = ${border_left}::SUPPORTED_TYPES;
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ ${border_left}::collect_completion_keywords(f);
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="border-radius"
+ engines="gecko servo-2013 servo-2020"
+ sub_properties="${' '.join(
+ 'border-%s-radius' % (corner)
+ for corner in ['top-left', 'top-right', 'bottom-right', 'bottom-left']
+ )}"
+ extra_prefixes="webkit"
+ spec="https://drafts.csswg.org/css-backgrounds/#border-radius"
+>
+ use crate::values::generics::rect::Rect;
+ use crate::values::generics::border::BorderCornerRadius;
+ use crate::values::specified::border::BorderRadius;
+ use crate::parser::Parse;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let radii = BorderRadius::parse(context, input)?;
+ Ok(expanded! {
+ border_top_left_radius: radii.top_left,
+ border_top_right_radius: radii.top_right,
+ border_bottom_right_radius: radii.bottom_right,
+ border_bottom_left_radius: radii.bottom_left,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let LonghandsToSerialize {
+ border_top_left_radius: &BorderCornerRadius(ref tl),
+ border_top_right_radius: &BorderCornerRadius(ref tr),
+ border_bottom_right_radius: &BorderCornerRadius(ref br),
+ border_bottom_left_radius: &BorderCornerRadius(ref bl),
+ } = *self;
+
+
+ let widths = Rect::new(tl.width(), tr.width(), br.width(), bl.width());
+ let heights = Rect::new(tl.height(), tr.height(), br.height(), bl.height());
+
+ BorderRadius::serialize_rects(widths, heights, dest)
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="border-image"
+ engines="gecko servo-2013"
+ sub_properties="border-image-outset
+ border-image-repeat border-image-slice border-image-source border-image-width"
+ extra_prefixes="moz:layout.css.prefixes.border-image webkit"
+ spec="https://drafts.csswg.org/css-backgrounds-3/#border-image"
+>
+ use crate::properties::longhands::{border_image_outset, border_image_repeat, border_image_slice};
+ use crate::properties::longhands::{border_image_source, border_image_width};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ % for name in "outset repeat slice source width".split():
+ let mut ${name} = border_image_${name}::get_initial_specified_value();
+ % endfor
+ let mut any = false;
+ let mut parsed_slice = false;
+ let mut parsed_source = false;
+ let mut parsed_repeat = false;
+ loop {
+ if !parsed_slice {
+ if let Ok(value) = input.try_parse(|input| border_image_slice::parse(context, input)) {
+ parsed_slice = true;
+ any = true;
+ slice = value;
+ // Parse border image width and outset, if applicable.
+ let maybe_width_outset: Result<_, ParseError> = input.try_parse(|input| {
+ input.expect_delim('/')?;
+
+ // Parse border image width, if applicable.
+ let w = input.try_parse(|input| border_image_width::parse(context, input)).ok();
+
+ // Parse border image outset if applicable.
+ let o = input.try_parse(|input| {
+ input.expect_delim('/')?;
+ border_image_outset::parse(context, input)
+ }).ok();
+ if w.is_none() && o.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ Ok((w, o))
+ });
+ if let Ok((w, o)) = maybe_width_outset {
+ if let Some(w) = w {
+ width = w;
+ }
+ if let Some(o) = o {
+ outset = o;
+ }
+ }
+ continue;
+ }
+ }
+ % for name in "source repeat".split():
+ if !parsed_${name} {
+ if let Ok(value) = input.try_parse(|input| border_image_${name}::parse(context, input)) {
+ ${name} = value;
+ parsed_${name} = true;
+ any = true;
+ continue
+ }
+ }
+ % endfor
+ break
+ }
+ if !any {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ Ok(expanded! {
+ % for name in "outset repeat slice source width".split():
+ border_image_${name}: ${name},
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let mut has_any = false;
+ % for name in "source slice outset width repeat".split():
+ let has_${name} = *self.border_image_${name} != border_image_${name}::get_initial_specified_value();
+ has_any = has_any || has_${name};
+ % endfor
+ if has_source || !has_any {
+ self.border_image_source.to_css(dest)?;
+ if !has_any {
+ return Ok(());
+ }
+ }
+ let needs_slice = has_slice || has_width || has_outset;
+ if needs_slice {
+ if has_source {
+ dest.write_char(' ')?;
+ }
+ self.border_image_slice.to_css(dest)?;
+ if has_width || has_outset {
+ dest.write_str(" /")?;
+ if has_width {
+ dest.write_char(' ')?;
+ self.border_image_width.to_css(dest)?;
+ }
+ if has_outset {
+ dest.write_str(" / ")?;
+ self.border_image_outset.to_css(dest)?;
+ }
+ }
+ }
+ if has_repeat {
+ if has_source || needs_slice {
+ dest.write_char(' ')?;
+ }
+ self.border_image_repeat.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+% for axis in ["block", "inline"]:
+ % for prop in ["width", "style", "color"]:
+ <%
+ spec = "https://drafts.csswg.org/css-logical/#propdef-border-%s-%s" % (axis, prop)
+ %>
+ <%helpers:shorthand
+ engines="gecko servo-2013 servo-2020"
+ name="border-${axis}-${prop}"
+ sub_properties="${' '.join(
+ 'border-%s-%s-%s' % (axis, side, prop)
+ for side in ['start', 'end']
+ )}"
+ spec="${spec}">
+
+ use crate::properties::longhands::border_${axis}_start_${prop};
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let start_value = border_${axis}_start_${prop}::parse(context, input)?;
+ let end_value =
+ input.try_parse(|input| border_${axis}_start_${prop}::parse(context, input))
+ .unwrap_or_else(|_| start_value.clone());
+
+ Ok(expanded! {
+ border_${axis}_start_${prop}: start_value,
+ border_${axis}_end_${prop}: end_value,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.border_${axis}_start_${prop}.to_css(dest)?;
+
+ if self.border_${axis}_end_${prop} != self.border_${axis}_start_${prop} {
+ dest.write_char(' ')?;
+ self.border_${axis}_end_${prop}.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+ }
+ </%helpers:shorthand>
+ % endfor
+% endfor
+
+% for axis in ["block", "inline"]:
+ <%
+ spec = "https://drafts.csswg.org/css-logical/#propdef-border-%s" % (axis)
+ %>
+ <%helpers:shorthand
+ name="border-${axis}"
+ engines="gecko servo-2013 servo-2020"
+ sub_properties="${' '.join(
+ 'border-%s-%s-width' % (axis, side)
+ for side in ['start', 'end']
+ )} ${' '.join(
+ 'border-%s-%s-style' % (axis, side)
+ for side in ['start', 'end']
+ )} ${' '.join(
+ 'border-%s-%s-color' % (axis, side)
+ for side in ['start', 'end']
+ )}"
+ spec="${spec}">
+
+ use crate::properties::shorthands::border_${axis}_start;
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let start_value = border_${axis}_start::parse_value(context, input)?;
+ Ok(expanded! {
+ border_${axis}_start_width: start_value.border_${axis}_start_width.clone(),
+ border_${axis}_end_width: start_value.border_${axis}_start_width,
+ border_${axis}_start_style: start_value.border_${axis}_start_style.clone(),
+ border_${axis}_end_style: start_value.border_${axis}_start_style,
+ border_${axis}_start_color: start_value.border_${axis}_start_color.clone(),
+ border_${axis}_end_color: start_value.border_${axis}_start_color,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ crate::values::specified::border::serialize_directional_border(
+ dest,
+ self.border_${axis}_start_width,
+ self.border_${axis}_start_style,
+ self.border_${axis}_start_color
+ )
+ }
+ }
+ </%helpers:shorthand>
+% endfor
diff --git a/servo/components/style/properties/shorthands/box.mako.rs b/servo/components/style/properties/shorthands/box.mako.rs
new file mode 100644
index 0000000000..f644687dc0
--- /dev/null
+++ b/servo/components/style/properties/shorthands/box.mako.rs
@@ -0,0 +1,253 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+${helpers.two_properties_shorthand(
+ "overflow",
+ "overflow-x",
+ "overflow-y",
+ engines="gecko servo-2013 servo-2020",
+ flags="SHORTHAND_IN_GETCS",
+ spec="https://drafts.csswg.org/css-overflow/#propdef-overflow",
+)}
+
+${helpers.two_properties_shorthand(
+ "overflow-clip-box",
+ "overflow-clip-box-block",
+ "overflow-clip-box-inline",
+ engines="gecko",
+ enabled_in="ua",
+ gecko_pref="layout.css.overflow-clip-box.enabled",
+ spec="Internal, may be standardized in the future "
+ "(https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-box)",
+)}
+
+${helpers.two_properties_shorthand(
+ "overscroll-behavior",
+ "overscroll-behavior-x",
+ "overscroll-behavior-y",
+ engines="gecko",
+ gecko_pref="layout.css.overscroll-behavior.enabled",
+ spec="https://wicg.github.io/overscroll-behavior/#overscroll-behavior-properties",
+)}
+
+<%helpers:shorthand
+ engines="gecko"
+ name="container"
+ sub_properties="container-name container-type"
+ gecko_pref="layout.css.container-queries.enabled"
+ enabled_in="ua"
+ spec="https://drafts.csswg.org/css-contain-3/#container-shorthand"
+>
+ use crate::values::specified::box_::{ContainerName, ContainerType};
+ pub fn parse_value<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::parser::Parse;
+ // See https://github.com/w3c/csswg-drafts/issues/7180 for why we don't
+ // match the spec.
+ let container_name = ContainerName::parse(context, input)?;
+ let container_type = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+ ContainerType::parse(input)?
+ } else {
+ ContainerType::Normal
+ };
+ Ok(expanded! {
+ container_name: container_name,
+ container_type: container_type,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.container_name.to_css(dest)?;
+ if !self.container_type.is_normal() {
+ dest.write_str(" / ")?;
+ self.container_type.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ engines="gecko"
+ name="page-break-before"
+ flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND"
+ sub_properties="break-before"
+ spec="https://drafts.csswg.org/css-break-3/#page-break-properties"
+>
+ pub fn parse_value<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::values::specified::box_::BreakBetween;
+ Ok(expanded! {
+ break_before: BreakBetween::parse_legacy(context, input)?,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.break_before.to_css_legacy(dest)
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ engines="gecko"
+ name="page-break-after"
+ flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND"
+ sub_properties="break-after"
+ spec="https://drafts.csswg.org/css-break-3/#page-break-properties"
+>
+ pub fn parse_value<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::values::specified::box_::BreakBetween;
+ Ok(expanded! {
+ break_after: BreakBetween::parse_legacy(context, input)?,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.break_after.to_css_legacy(dest)
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ engines="gecko"
+ name="page-break-inside"
+ flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND"
+ sub_properties="break-inside"
+ spec="https://drafts.csswg.org/css-break-3/#page-break-properties"
+>
+ pub fn parse_value<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::values::specified::box_::BreakWithin;
+ Ok(expanded! {
+ break_inside: BreakWithin::parse_legacy(context, input)?,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.break_inside.to_css_legacy(dest)
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand name="offset"
+ engines="gecko"
+ sub_properties="offset-path offset-distance offset-rotate offset-anchor
+ offset-position"
+ spec="https://drafts.fxtf.org/motion-1/#offset-shorthand">
+ use crate::parser::Parse;
+ use crate::values::specified::motion::{OffsetPath, OffsetPosition, OffsetRotate};
+ use crate::values::specified::{LengthPercentage, PositionOrAuto};
+ use crate::Zero;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let offset_position =
+ if static_prefs::pref!("layout.css.motion-path-offset-position.enabled") {
+ input.try_parse(|i| OffsetPosition::parse(context, i)).ok()
+ } else {
+ None
+ };
+
+ let offset_path = input.try_parse(|i| OffsetPath::parse(context, i)).ok();
+
+ // Must have one of [offset-position, offset-path].
+ // FIXME: The syntax is out-of-date after the update of the spec.
+ // https://github.com/w3c/fxtf-drafts/issues/515
+ if offset_position.is_none() && offset_path.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let mut offset_distance = None;
+ let mut offset_rotate = None;
+ // offset-distance and offset-rotate are grouped with offset-path.
+ if offset_path.is_some() {
+ loop {
+ if offset_distance.is_none() {
+ if let Ok(value) = input.try_parse(|i| LengthPercentage::parse(context, i)) {
+ offset_distance = Some(value);
+ }
+ }
+
+ if offset_rotate.is_none() {
+ if let Ok(value) = input.try_parse(|i| OffsetRotate::parse(context, i)) {
+ offset_rotate = Some(value);
+ continue;
+ }
+ }
+ break;
+ }
+ }
+
+ let offset_anchor = input.try_parse(|i| {
+ i.expect_delim('/')?;
+ PositionOrAuto::parse(context, i)
+ }).ok();
+
+ Ok(expanded! {
+ offset_position: offset_position.unwrap_or(OffsetPosition::normal()),
+ offset_path: offset_path.unwrap_or(OffsetPath::none()),
+ offset_distance: offset_distance.unwrap_or(LengthPercentage::zero()),
+ offset_rotate: offset_rotate.unwrap_or(OffsetRotate::auto()),
+ offset_anchor: offset_anchor.unwrap_or(PositionOrAuto::auto()),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ if let Some(offset_position) = self.offset_position {
+ // The basic concept is: we must serialize offset-position or offset-path group.
+ // offset-path group means "offset-path offset-distance offset-rotate".
+ let must_serialize_path = *self.offset_path != OffsetPath::None
+ || (!self.offset_distance.is_zero() || !self.offset_rotate.is_auto());
+ let position_is_default = matches!(offset_position, OffsetPosition::Normal);
+ if !position_is_default || !must_serialize_path {
+ offset_position.to_css(dest)?;
+ }
+
+ if must_serialize_path {
+ if !position_is_default {
+ dest.write_char(' ')?;
+ }
+ self.offset_path.to_css(dest)?;
+ }
+ } else {
+ // If the pref is off, we always show offset-path.
+ self.offset_path.to_css(dest)?;
+ }
+
+ if !self.offset_distance.is_zero() {
+ dest.write_char(' ')?;
+ self.offset_distance.to_css(dest)?;
+ }
+
+ if !self.offset_rotate.is_auto() {
+ dest.write_char(' ')?;
+ self.offset_rotate.to_css(dest)?;
+ }
+
+ if *self.offset_anchor != PositionOrAuto::auto() {
+ dest.write_str(" / ")?;
+ self.offset_anchor.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/column.mako.rs b/servo/components/style/properties/shorthands/column.mako.rs
new file mode 100644
index 0000000000..3740775a7e
--- /dev/null
+++ b/servo/components/style/properties/shorthands/column.mako.rs
@@ -0,0 +1,115 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%helpers:shorthand name="columns"
+ engines="gecko servo-2013"
+ sub_properties="column-width column-count"
+ servo_2013_pref="layout.columns.enabled",
+ spec="https://drafts.csswg.org/css-multicol/#propdef-columns">
+ use crate::properties::longhands::{column_count, column_width};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut column_count = None;
+ let mut column_width = None;
+ let mut autos = 0;
+
+ loop {
+ if input.try_parse(|input| input.expect_ident_matching("auto")).is_ok() {
+ // Leave the options to None, 'auto' is the initial value.
+ autos += 1;
+ continue
+ }
+
+ if column_count.is_none() {
+ if let Ok(value) = input.try_parse(|input| column_count::parse(context, input)) {
+ column_count = Some(value);
+ continue
+ }
+ }
+
+ if column_width.is_none() {
+ if let Ok(value) = input.try_parse(|input| column_width::parse(context, input)) {
+ column_width = Some(value);
+ continue
+ }
+ }
+
+ break
+ }
+
+ let values = autos + column_count.iter().len() + column_width.iter().len();
+ if values == 0 || values > 2 {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(expanded! {
+ column_count: unwrap_or_initial!(column_count),
+ column_width: unwrap_or_initial!(column_width),
+ })
+ }
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ if self.column_width.is_auto() {
+ return self.column_count.to_css(dest)
+ }
+ self.column_width.to_css(dest)?;
+ if !self.column_count.is_auto() {
+ dest.write_char(' ')?;
+ self.column_count.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="column-rule"
+ engines="gecko"
+ sub_properties="column-rule-width column-rule-style column-rule-color"
+ derive_serialize="True"
+ spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule"
+>
+ use crate::properties::longhands::{column_rule_width, column_rule_style};
+ use crate::properties::longhands::column_rule_color;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ % for name in "width style color".split():
+ let mut column_rule_${name} = None;
+ % endfor
+ let mut any = false;
+
+ loop {
+ % for name in "width style color".split():
+ if column_rule_${name}.is_none() {
+ if let Ok(value) = input.try_parse(|input|
+ column_rule_${name}::parse(context, input)) {
+ column_rule_${name} = Some(value);
+ any = true;
+ continue
+ }
+ }
+ % endfor
+
+ break
+ }
+ if any {
+ Ok(expanded! {
+ column_rule_width: unwrap_or_initial!(column_rule_width),
+ column_rule_style: unwrap_or_initial!(column_rule_style),
+ column_rule_color: unwrap_or_initial!(column_rule_color),
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/font.mako.rs b/servo/components/style/properties/shorthands/font.mako.rs
new file mode 100644
index 0000000000..17dcf9d926
--- /dev/null
+++ b/servo/components/style/properties/shorthands/font.mako.rs
@@ -0,0 +1,542 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import SYSTEM_FONT_LONGHANDS %>
+
+<%helpers:shorthand
+ name="font"
+ engines="gecko servo-2013 servo-2020"
+ sub_properties="
+ font-style
+ font-variant-caps
+ font-weight
+ font-stretch
+ font-size
+ line-height
+ font-family
+ ${'font-size-adjust' if engine == 'gecko' else ''}
+ ${'font-kerning' if engine == 'gecko' else ''}
+ ${'font-optical-sizing' if engine == 'gecko' else ''}
+ ${'font-variant-alternates' if engine == 'gecko' else ''}
+ ${'font-variant-east-asian' if engine == 'gecko' else ''}
+ ${'font-variant-emoji' if engine == 'gecko' else ''}
+ ${'font-variant-ligatures' if engine == 'gecko' else ''}
+ ${'font-variant-numeric' if engine == 'gecko' else ''}
+ ${'font-variant-position' if engine == 'gecko' else ''}
+ ${'font-language-override' if engine == 'gecko' else ''}
+ ${'font-feature-settings' if engine == 'gecko' else ''}
+ ${'font-variation-settings' if engine == 'gecko' else ''}
+ "
+ derive_value_info="False"
+ spec="https://drafts.csswg.org/css-fonts-3/#propdef-font"
+>
+ use crate::computed_values::font_variant_caps::T::SmallCaps;
+ use crate::parser::Parse;
+ use crate::properties::longhands::{font_family, font_style, font_size, font_weight, font_stretch};
+ use crate::properties::longhands::font_variant_caps;
+ use crate::values::specified::font::LineHeight;
+ use crate::values::specified::{FontSize, FontWeight};
+ use crate::values::specified::font::{FontStretch, FontStretchKeyword};
+ #[cfg(feature = "gecko")]
+ use crate::values::specified::font::SystemFont;
+
+ <%
+ gecko_sub_properties = "kerning language_override size_adjust \
+ variant_alternates variant_east_asian \
+ variant_emoji variant_ligatures \
+ variant_numeric variant_position \
+ feature_settings variation_settings \
+ optical_sizing".split()
+ %>
+ % if engine == "gecko":
+ % for prop in gecko_sub_properties:
+ use crate::properties::longhands::font_${prop};
+ % endfor
+ % endif
+ use self::font_family::SpecifiedValue as FontFamily;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut nb_normals = 0;
+ let mut style = None;
+ let mut variant_caps = None;
+ let mut weight = None;
+ let mut stretch = None;
+ let size;
+ % if engine == "gecko":
+ if let Ok(sys) = input.try_parse(|i| SystemFont::parse(context, i)) {
+ return Ok(expanded! {
+ % for name in SYSTEM_FONT_LONGHANDS:
+ ${name}: ${name}::SpecifiedValue::system_font(sys),
+ % endfor
+ line_height: LineHeight::normal(),
+ % for name in gecko_sub_properties + ["variant_caps"]:
+ font_${name}: font_${name}::get_initial_specified_value(),
+ % endfor
+ })
+ }
+ % endif
+ loop {
+ // Special-case 'normal' because it is valid in each of
+ // font-style, font-weight, font-variant and font-stretch.
+ // Leaves the values to None, 'normal' is the initial value for each of them.
+ if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
+ nb_normals += 1;
+ continue;
+ }
+ if style.is_none() {
+ if let Ok(value) = input.try_parse(|input| font_style::parse(context, input)) {
+ style = Some(value);
+ continue
+ }
+ }
+ if weight.is_none() {
+ if let Ok(value) = input.try_parse(|input| font_weight::parse(context, input)) {
+ weight = Some(value);
+ continue
+ }
+ }
+ if variant_caps.is_none() {
+ // The only variant-caps value allowed is small-caps (from CSS2); the added values
+ // defined by CSS Fonts 3 and later are not accepted.
+ // https://www.w3.org/TR/css-fonts-4/#font-prop
+ if input.try_parse(|input| input.expect_ident_matching("small-caps")).is_ok() {
+ variant_caps = Some(SmallCaps);
+ continue
+ }
+ }
+ if stretch.is_none() {
+ if let Ok(value) = input.try_parse(FontStretchKeyword::parse) {
+ stretch = Some(FontStretch::Keyword(value));
+ continue
+ }
+ }
+ size = Some(FontSize::parse(context, input)?);
+ break
+ }
+
+ let size = match size {
+ Some(s) => s,
+ None => {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ };
+
+ let line_height = if input.try_parse(|input| input.expect_delim('/')).is_ok() {
+ Some(LineHeight::parse(context, input)?)
+ } else {
+ None
+ };
+
+ #[inline]
+ fn count<T>(opt: &Option<T>) -> u8 {
+ if opt.is_some() { 1 } else { 0 }
+ }
+
+ if (count(&style) + count(&weight) + count(&variant_caps) + count(&stretch) + nb_normals) > 4 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ let family = FontFamily::parse(context, input)?;
+ Ok(expanded! {
+ % for name in "style weight stretch variant_caps".split():
+ font_${name}: unwrap_or_initial!(font_${name}, ${name}),
+ % endfor
+ font_size: size,
+ line_height: line_height.unwrap_or(LineHeight::normal()),
+ font_family: family,
+ % if engine == "gecko":
+ % for name in gecko_sub_properties:
+ font_${name}: font_${name}::get_initial_specified_value(),
+ % endfor
+ % endif
+ })
+ }
+
+ % if engine == "gecko":
+ enum CheckSystemResult {
+ AllSystem(SystemFont),
+ SomeSystem,
+ None
+ }
+ % endif
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ % if engine == "gecko":
+ match self.check_system() {
+ CheckSystemResult::AllSystem(sys) => return sys.to_css(dest),
+ CheckSystemResult::SomeSystem => return Ok(()),
+ CheckSystemResult::None => {}
+ }
+ % endif
+
+ % if engine == "gecko":
+ if let Some(v) = self.font_optical_sizing {
+ if v != &font_optical_sizing::get_initial_specified_value() {
+ return Ok(());
+ }
+ }
+ if let Some(v) = self.font_variation_settings {
+ if v != &font_variation_settings::get_initial_specified_value() {
+ return Ok(());
+ }
+ }
+ if let Some(v) = self.font_variant_emoji {
+ if v != &font_variant_emoji::get_initial_specified_value() {
+ return Ok(());
+ }
+ }
+
+ % for name in gecko_sub_properties:
+ % if name != "optical_sizing" and name != "variation_settings" and name != "variant_emoji":
+ if self.font_${name} != &font_${name}::get_initial_specified_value() {
+ return Ok(());
+ }
+ % endif
+ % endfor
+ % endif
+
+ // Only font-stretch keywords are allowed as part as the font
+ // shorthand.
+ let font_stretch = match *self.font_stretch {
+ FontStretch::Keyword(kw) => kw,
+ FontStretch::Stretch(percentage) => {
+ match FontStretchKeyword::from_percentage(percentage.0.get()) {
+ Some(kw) => kw,
+ None => return Ok(()),
+ }
+ }
+ FontStretch::System(..) => return Ok(()),
+ };
+
+ // The only variant-caps value allowed in the shorthand is small-caps (from CSS2);
+ // the added values defined by CSS Fonts 3 and later are not supported.
+ // https://www.w3.org/TR/css-fonts-4/#font-prop
+ if self.font_variant_caps != &font_variant_caps::get_initial_specified_value() &&
+ *self.font_variant_caps != SmallCaps {
+ return Ok(());
+ }
+
+ % for name in "style variant_caps".split():
+ if self.font_${name} != &font_${name}::get_initial_specified_value() {
+ self.font_${name}.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+ % endfor
+
+ // The initial specified font-weight value of 'normal' computes as a number (400),
+ // not to the keyword, so we need to check for that as well in order to properly
+ // serialize the computed style.
+ if self.font_weight != &FontWeight::normal() &&
+ self.font_weight != &FontWeight::from_gecko_keyword(400) {
+ self.font_weight.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+
+ if font_stretch != FontStretchKeyword::Normal {
+ font_stretch.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+
+ self.font_size.to_css(dest)?;
+
+ if *self.line_height != LineHeight::normal() {
+ dest.write_str(" / ")?;
+ self.line_height.to_css(dest)?;
+ }
+
+ dest.write_char(' ')?;
+ self.font_family.to_css(dest)?;
+
+ Ok(())
+ }
+ }
+
+ impl<'a> LonghandsToSerialize<'a> {
+ % if engine == "gecko":
+ /// Check if some or all members are system fonts
+ fn check_system(&self) -> CheckSystemResult {
+ let mut sys = None;
+ let mut all = true;
+
+ % for prop in SYSTEM_FONT_LONGHANDS:
+ % if prop == "font_optical_sizing" or prop == "font_variation_settings":
+ if let Some(value) = self.${prop} {
+ % else:
+ {
+ let value = self.${prop};
+ % endif
+ match value.get_system() {
+ Some(s) => {
+ debug_assert!(sys.is_none() || s == sys.unwrap());
+ sys = Some(s);
+ }
+ None => {
+ all = false;
+ }
+ }
+ }
+ % endfor
+ if self.line_height != &LineHeight::normal() {
+ all = false
+ }
+ if all {
+ CheckSystemResult::AllSystem(sys.unwrap())
+ } else if sys.is_some() {
+ CheckSystemResult::SomeSystem
+ } else {
+ CheckSystemResult::None
+ }
+ }
+ % endif
+ }
+
+ <%
+ subprops_for_value_info = ["font_style", "font_weight", "font_stretch",
+ "font_variant_caps", "font_size", "font_family"]
+ subprops_for_value_info = [
+ "<longhands::{}::SpecifiedValue as SpecifiedValueInfo>".format(p)
+ for p in subprops_for_value_info
+ ]
+ %>
+ impl SpecifiedValueInfo for Longhands {
+ const SUPPORTED_TYPES: u8 = 0
+ % for p in subprops_for_value_info:
+ | ${p}::SUPPORTED_TYPES
+ % endfor
+ ;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ % for p in subprops_for_value_info:
+ ${p}::collect_completion_keywords(f);
+ % endfor
+ <SystemFont as SpecifiedValueInfo>::collect_completion_keywords(f);
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand name="font-variant"
+ engines="gecko servo-2013"
+ flags="SHORTHAND_IN_GETCS"
+ sub_properties="font-variant-caps
+ ${'font-variant-alternates' if engine == 'gecko' else ''}
+ ${'font-variant-east-asian' if engine == 'gecko' else ''}
+ ${'font-variant-emoji' if engine == 'gecko' else ''}
+ ${'font-variant-ligatures' if engine == 'gecko' else ''}
+ ${'font-variant-numeric' if engine == 'gecko' else ''}
+ ${'font-variant-position' if engine == 'gecko' else ''}"
+ spec="https://drafts.csswg.org/css-fonts-3/#propdef-font-variant">
+% if engine == 'gecko':
+ <% sub_properties = "ligatures caps alternates numeric east_asian position emoji".split() %>
+% else:
+ <% sub_properties = ["caps"] %>
+% endif
+
+% for prop in sub_properties:
+ use crate::properties::longhands::font_variant_${prop};
+% endfor
+ #[allow(unused_imports)]
+ use crate::values::specified::FontVariantLigatures;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ % for prop in sub_properties:
+ let mut ${prop} = None;
+ % endfor
+
+ if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() {
+ // Leave the values to None, 'normal' is the initial value for all the sub properties.
+ } else if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+ // The 'none' value sets 'font-variant-ligatures' to 'none' and resets all other sub properties
+ // to their initial value.
+ % if engine == "gecko":
+ ligatures = Some(FontVariantLigatures::NONE);
+ % endif
+ } else {
+ let mut has_custom_value: bool = false;
+ loop {
+ if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() ||
+ input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ % for prop in sub_properties:
+ if ${prop}.is_none() {
+ if let Ok(value) = input.try_parse(|i| font_variant_${prop}::parse(context, i)) {
+ has_custom_value = true;
+ ${prop} = Some(value);
+ continue
+ }
+ }
+ % endfor
+
+ break
+ }
+
+ if !has_custom_value {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+
+ Ok(expanded! {
+ % for prop in sub_properties:
+ font_variant_${prop}: unwrap_or_initial!(font_variant_${prop}, ${prop}),
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ #[allow(unused_assignments)]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+
+ let has_none_ligatures =
+ % if engine == "gecko":
+ self.font_variant_ligatures == &FontVariantLigatures::NONE;
+ % else:
+ false;
+ % endif
+
+ const TOTAL_SUBPROPS: usize = ${len(sub_properties)};
+ let mut nb_normals = 0;
+ % for prop in sub_properties:
+ % if prop == "emoji":
+ if let Some(value) = self.font_variant_${prop} {
+ % else:
+ {
+ let value = self.font_variant_${prop};
+ % endif
+ if value == &font_variant_${prop}::get_initial_specified_value() {
+ nb_normals += 1;
+ }
+ }
+ % if prop == "emoji":
+ else {
+ // The property was disabled, so we count it as 'normal' for the purpose
+ // of deciding how the shorthand can be serialized.
+ nb_normals += 1;
+ }
+ % endif
+ % endfor
+
+
+ if nb_normals > 0 && nb_normals == TOTAL_SUBPROPS {
+ dest.write_str("normal")?;
+ } else if has_none_ligatures {
+ if nb_normals == TOTAL_SUBPROPS - 1 {
+ // Serialize to 'none' if 'font-variant-ligatures' is set to 'none' and all other
+ // font feature properties are reset to their initial value.
+ dest.write_str("none")?;
+ } else {
+ return Ok(())
+ }
+ } else {
+ let mut has_any = false;
+ % for prop in sub_properties:
+ % if prop == "emoji":
+ if let Some(value) = self.font_variant_${prop} {
+ % else:
+ {
+ let value = self.font_variant_${prop};
+ % endif
+ if value != &font_variant_${prop}::get_initial_specified_value() {
+ if has_any {
+ dest.write_char(' ')?;
+ }
+ has_any = true;
+ value.to_css(dest)?;
+ }
+ }
+ % endfor
+ }
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand name="font-synthesis"
+ engines="gecko"
+ flags="SHORTHAND_IN_GETCS"
+ sub_properties="font-synthesis-weight font-synthesis-style font-synthesis-small-caps font-synthesis-position"
+ derive_value_info="False"
+ spec="https://drafts.csswg.org/css-fonts-3/#propdef-font-variant">
+ <% sub_properties = ["weight", "style", "small_caps", "position"] %>
+
+ use crate::values::specified::FontSynthesis;
+
+ pub fn parse_value<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ % for prop in sub_properties:
+ let mut ${prop} = FontSynthesis::None;
+ % endfor
+
+ if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+ // Leave all the individual values as None
+ } else {
+ let mut has_custom_value = false;
+ while !input.is_exhausted() {
+ try_match_ident_ignore_ascii_case! { input,
+ % for prop in sub_properties:
+ "${prop.replace('_', '-')}" if ${prop} == FontSynthesis::None => {
+ has_custom_value = true;
+ ${prop} = FontSynthesis::Auto;
+ continue;
+ },
+ % endfor
+ }
+ }
+ if !has_custom_value {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ Ok(expanded! {
+ % for prop in sub_properties:
+ font_synthesis_${prop}: ${prop},
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let mut has_any = false;
+
+ % for prop in sub_properties:
+ if self.font_synthesis_${prop} == &FontSynthesis::Auto {
+ if has_any {
+ dest.write_char(' ')?;
+ }
+ has_any = true;
+ dest.write_str("${prop.replace('_', '-')}")?;
+ }
+ % endfor
+
+ if !has_any {
+ dest.write_str("none")?;
+ }
+
+ Ok(())
+ }
+ }
+
+ // The shorthand takes the sub-property names of the longhands, and not the
+ // 'auto' keyword like they do, so we can't automatically derive this.
+ impl SpecifiedValueInfo for Longhands {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&[
+ "none",
+ % for prop in sub_properties:
+ "${prop.replace('_', '-')}",
+ % endfor
+ ]);
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/inherited_svg.mako.rs b/servo/components/style/properties/shorthands/inherited_svg.mako.rs
new file mode 100644
index 0000000000..f29e78a69f
--- /dev/null
+++ b/servo/components/style/properties/shorthands/inherited_svg.mako.rs
@@ -0,0 +1,38 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%helpers:shorthand
+ name="marker"
+ engines="gecko"
+ sub_properties="marker-start marker-end marker-mid"
+ spec="https://svgwg.org/svg2-draft/painting.html#MarkerShorthand"
+>
+ use crate::values::specified::url::UrlOrNone;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::parser::Parse;
+ let url = UrlOrNone::parse(context, input)?;
+
+ Ok(expanded! {
+ marker_start: url.clone(),
+ marker_mid: url.clone(),
+ marker_end: url,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ if self.marker_start == self.marker_mid && self.marker_mid == self.marker_end {
+ self.marker_start.to_css(dest)
+ } else {
+ Ok(())
+ }
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/inherited_text.mako.rs b/servo/components/style/properties/shorthands/inherited_text.mako.rs
new file mode 100644
index 0000000000..d470553e42
--- /dev/null
+++ b/servo/components/style/properties/shorthands/inherited_text.mako.rs
@@ -0,0 +1,254 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%helpers:shorthand
+ name="text-emphasis"
+ engines="gecko"
+ sub_properties="text-emphasis-style text-emphasis-color"
+ derive_serialize="True"
+ spec="https://drafts.csswg.org/css-text-decor-3/#text-emphasis-property"
+>
+ use crate::properties::longhands::{text_emphasis_color, text_emphasis_style};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut color = None;
+ let mut style = None;
+
+ loop {
+ if color.is_none() {
+ if let Ok(value) = input.try_parse(|input| text_emphasis_color::parse(context, input)) {
+ color = Some(value);
+ continue
+ }
+ }
+ if style.is_none() {
+ if let Ok(value) = input.try_parse(|input| text_emphasis_style::parse(context, input)) {
+ style = Some(value);
+ continue
+ }
+ }
+ break
+ }
+ if color.is_some() || style.is_some() {
+ Ok(expanded! {
+ text_emphasis_color: unwrap_or_initial!(text_emphasis_color, color),
+ text_emphasis_style: unwrap_or_initial!(text_emphasis_style, style),
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="text-wrap"
+ engines="gecko"
+ sub_properties="text-wrap-mode text-wrap-style"
+ spec="https://www.w3.org/TR/css-text-4/#text-wrap"
+>
+ use crate::properties::longhands::{text_wrap_mode, text_wrap_style};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut mode = None;
+ let mut style = None;
+
+ loop {
+ if mode.is_none() {
+ if let Ok(value) = input.try_parse(|input| text_wrap_mode::parse(context, input)) {
+ mode = Some(value);
+ continue
+ }
+ }
+ if style.is_none() {
+ if let Ok(value) = input.try_parse(|input| text_wrap_style::parse(context, input)) {
+ style = Some(value);
+ continue
+ }
+ }
+ break
+ }
+ if mode.is_some() || style.is_some() {
+ Ok(expanded! {
+ text_wrap_mode: unwrap_or_initial!(text_wrap_mode, mode),
+ text_wrap_style: unwrap_or_initial!(text_wrap_style, style),
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ use text_wrap_mode::computed_value::T as Mode;
+ use text_wrap_style::computed_value::T as Style;
+
+ if matches!(self.text_wrap_style, None | Some(&Style::Auto)) {
+ return self.text_wrap_mode.to_css(dest);
+ }
+
+ if *self.text_wrap_mode != Mode::Wrap {
+ self.text_wrap_mode.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+
+ self.text_wrap_style.to_css(dest)
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="white-space"
+ engines="gecko"
+ sub_properties="text-wrap-mode white-space-collapse"
+ spec="https://www.w3.org/TR/css-text-4/#white-space-property"
+>
+ use crate::properties::longhands::{text_wrap_mode, white_space_collapse};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use white_space_collapse::computed_value::T as Collapse;
+ use text_wrap_mode::computed_value::T as Wrap;
+
+ fn parse_special_shorthands<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Longhands, ParseError<'i>> {
+ let (mode, collapse) = try_match_ident_ignore_ascii_case! { input,
+ "normal" => (Wrap::Wrap, Collapse::Collapse),
+ "pre" => (Wrap::Nowrap, Collapse::Preserve),
+ "pre-wrap" => (Wrap::Wrap, Collapse::Preserve),
+ "pre-line" => (Wrap::Wrap, Collapse::PreserveBreaks),
+ // TODO: deprecate/remove -moz-pre-space; the white-space-collapse: preserve-spaces value
+ // should serve this purpose?
+ "-moz-pre-space" => (Wrap::Wrap, Collapse::PreserveSpaces),
+ };
+ Ok(expanded! {
+ text_wrap_mode: mode,
+ white_space_collapse: collapse,
+ })
+ }
+
+ if let Ok(result) = input.try_parse(parse_special_shorthands) {
+ return Ok(result);
+ }
+
+ let mut wrap = None;
+ let mut collapse = None;
+
+ loop {
+ if wrap.is_none() {
+ if let Ok(value) = input.try_parse(|input| text_wrap_mode::parse(context, input)) {
+ wrap = Some(value);
+ continue
+ }
+ }
+ if collapse.is_none() {
+ if let Ok(value) = input.try_parse(|input| white_space_collapse::parse(context, input)) {
+ collapse = Some(value);
+ continue
+ }
+ }
+ break
+ }
+
+ if wrap.is_some() || collapse.is_some() {
+ Ok(expanded! {
+ text_wrap_mode: unwrap_or_initial!(text_wrap_mode, wrap),
+ white_space_collapse: unwrap_or_initial!(white_space_collapse, collapse),
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ use white_space_collapse::computed_value::T as Collapse;
+ use text_wrap_mode::computed_value::T as Wrap;
+
+ match *self.text_wrap_mode {
+ Wrap::Wrap => {
+ match *self.white_space_collapse {
+ Collapse::Collapse => return dest.write_str("normal"),
+ Collapse::Preserve => return dest.write_str("pre-wrap"),
+ Collapse::PreserveBreaks => return dest.write_str("pre-line"),
+ Collapse::PreserveSpaces => return dest.write_str("-moz-pre-space"),
+ _ => (),
+ }
+ },
+ Wrap::Nowrap => {
+ if let Collapse::Preserve = *self.white_space_collapse {
+ return dest.write_str("pre");
+ }
+ },
+ }
+
+ let mut has_value = false;
+ if *self.white_space_collapse != Collapse::Collapse {
+ self.white_space_collapse.to_css(dest)?;
+ has_value = true;
+ }
+
+ if *self.text_wrap_mode != Wrap::Wrap {
+ if has_value {
+ dest.write_char(' ')?;
+ }
+ self.text_wrap_mode.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+// CSS Compatibility
+// https://compat.spec.whatwg.org/
+<%helpers:shorthand name="-webkit-text-stroke"
+ engines="gecko"
+ sub_properties="-webkit-text-stroke-width
+ -webkit-text-stroke-color"
+ derive_serialize="True"
+ spec="https://compat.spec.whatwg.org/#the-webkit-text-stroke">
+ use crate::properties::longhands::{_webkit_text_stroke_color, _webkit_text_stroke_width};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut color = None;
+ let mut width = None;
+ loop {
+ if color.is_none() {
+ if let Ok(value) = input.try_parse(|input| _webkit_text_stroke_color::parse(context, input)) {
+ color = Some(value);
+ continue
+ }
+ }
+
+ if width.is_none() {
+ if let Ok(value) = input.try_parse(|input| _webkit_text_stroke_width::parse(context, input)) {
+ width = Some(value);
+ continue
+ }
+ }
+ break
+ }
+
+ if color.is_some() || width.is_some() {
+ Ok(expanded! {
+ _webkit_text_stroke_color: unwrap_or_initial!(_webkit_text_stroke_color, color),
+ _webkit_text_stroke_width: unwrap_or_initial!(_webkit_text_stroke_width, width),
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/list.mako.rs b/servo/components/style/properties/shorthands/list.mako.rs
new file mode 100644
index 0000000000..2e234e3d8f
--- /dev/null
+++ b/servo/components/style/properties/shorthands/list.mako.rs
@@ -0,0 +1,137 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%helpers:shorthand name="list-style"
+ engines="gecko servo-2013 servo-2020"
+ sub_properties="list-style-position list-style-image list-style-type"
+ spec="https://drafts.csswg.org/css-lists/#propdef-list-style">
+ use crate::properties::longhands::{list_style_image, list_style_position, list_style_type};
+ use crate::values::specified::Image;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ // `none` is ambiguous until we've finished parsing the shorthands, so we count the number
+ // of times we see it.
+ let mut nones = 0u8;
+ let (mut image, mut position, mut list_style_type, mut any) = (None, None, None, false);
+ loop {
+ if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+ nones = nones + 1;
+ if nones > 2 {
+ return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("none".into())))
+ }
+ any = true;
+ continue
+ }
+
+ if image.is_none() {
+ if let Ok(value) = input.try_parse(|input| list_style_image::parse(context, input)) {
+ image = Some(value);
+ any = true;
+ continue
+ }
+ }
+
+ if position.is_none() {
+ if let Ok(value) = input.try_parse(|input| list_style_position::parse(context, input)) {
+ position = Some(value);
+ any = true;
+ continue
+ }
+ }
+
+ // list-style-type must be checked the last, because it accepts
+ // arbitrary identifier for custom counter style, and thus may
+ // affect values of list-style-position.
+ if list_style_type.is_none() {
+ if let Ok(value) = input.try_parse(|input| list_style_type::parse(context, input)) {
+ list_style_type = Some(value);
+ any = true;
+ continue
+ }
+ }
+ break
+ }
+
+ let position = unwrap_or_initial!(list_style_position, position);
+
+ // If there are two `none`s, then we can't have a type or image; if there is one `none`,
+ // then we can't have both a type *and* an image; if there is no `none` then we're fine as
+ // long as we parsed something.
+ use self::list_style_type::SpecifiedValue as ListStyleType;
+ match (any, nones, list_style_type, image) {
+ (true, 2, None, None) => {
+ Ok(expanded! {
+ list_style_position: position,
+ list_style_image: Image::None,
+ list_style_type: ListStyleType::None,
+ })
+ }
+ (true, 1, None, Some(image)) => {
+ Ok(expanded! {
+ list_style_position: position,
+ list_style_image: image,
+ list_style_type: ListStyleType::None,
+ })
+ }
+ (true, 1, Some(list_style_type), None) => {
+ Ok(expanded! {
+ list_style_position: position,
+ list_style_image: Image::None,
+ list_style_type: list_style_type,
+ })
+ }
+ (true, 1, None, None) => {
+ Ok(expanded! {
+ list_style_position: position,
+ list_style_image: Image::None,
+ list_style_type: ListStyleType::None,
+ })
+ }
+ (true, 0, list_style_type, image) => {
+ Ok(expanded! {
+ list_style_position: position,
+ list_style_image: unwrap_or_initial!(list_style_image, image),
+ list_style_type: unwrap_or_initial!(list_style_type),
+ })
+ }
+ _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ use longhands::list_style_position::SpecifiedValue as ListStylePosition;
+ use longhands::list_style_type::SpecifiedValue as ListStyleType;
+ use longhands::list_style_image::SpecifiedValue as ListStyleImage;
+ let mut have_one_non_initial_value = false;
+ if self.list_style_position != &ListStylePosition::Outside {
+ self.list_style_position.to_css(dest)?;
+ have_one_non_initial_value = true;
+ }
+ if self.list_style_image != &ListStyleImage::None {
+ if have_one_non_initial_value {
+ dest.write_char(' ')?;
+ }
+ self.list_style_image.to_css(dest)?;
+ have_one_non_initial_value = true;
+ }
+ if self.list_style_type != &ListStyleType::disc() {
+ if have_one_non_initial_value {
+ dest.write_char(' ')?;
+ }
+ self.list_style_type.to_css(dest)?;
+ have_one_non_initial_value = true;
+ }
+ if !have_one_non_initial_value {
+ self.list_style_position.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/margin.mako.rs b/servo/components/style/properties/shorthands/margin.mako.rs
new file mode 100644
index 0000000000..6b5bf7e467
--- /dev/null
+++ b/servo/components/style/properties/shorthands/margin.mako.rs
@@ -0,0 +1,60 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+<% from data import DEFAULT_RULES_AND_PAGE %>
+
+${helpers.four_sides_shorthand(
+ "margin",
+ "margin-%s",
+ "specified::LengthPercentageOrAuto::parse",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-box/#propdef-margin",
+ rule_types_allowed=DEFAULT_RULES_AND_PAGE,
+ allow_quirks="Yes",
+)}
+
+${helpers.two_properties_shorthand(
+ "margin-block",
+ "margin-block-start",
+ "margin-block-end",
+ "specified::LengthPercentageOrAuto::parse",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-logical/#propdef-margin-block"
+)}
+
+${helpers.two_properties_shorthand(
+ "margin-inline",
+ "margin-inline-start",
+ "margin-inline-end",
+ "specified::LengthPercentageOrAuto::parse",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-logical/#propdef-margin-inline"
+)}
+
+${helpers.four_sides_shorthand(
+ "scroll-margin",
+ "scroll-margin-%s",
+ "specified::Length::parse",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin",
+)}
+
+${helpers.two_properties_shorthand(
+ "scroll-margin-block",
+ "scroll-margin-block-start",
+ "scroll-margin-block-end",
+ "specified::Length::parse",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin-block",
+)}
+
+${helpers.two_properties_shorthand(
+ "scroll-margin-inline",
+ "scroll-margin-inline-start",
+ "scroll-margin-inline-end",
+ "specified::Length::parse",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin-inline",
+)}
diff --git a/servo/components/style/properties/shorthands/outline.mako.rs b/servo/components/style/properties/shorthands/outline.mako.rs
new file mode 100644
index 0000000000..6ee8ed22c9
--- /dev/null
+++ b/servo/components/style/properties/shorthands/outline.mako.rs
@@ -0,0 +1,80 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%helpers:shorthand name="outline"
+ engines="gecko servo-2013"
+ sub_properties="outline-color outline-style outline-width"
+ spec="https://drafts.csswg.org/css-ui/#propdef-outline">
+ use crate::properties::longhands::{outline_color, outline_width, outline_style};
+ use crate::values::specified;
+ use crate::parser::Parse;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let _unused = context;
+ let mut color = None;
+ let mut style = None;
+ let mut width = None;
+ let mut any = false;
+ loop {
+ if color.is_none() {
+ if let Ok(value) = input.try_parse(|i| specified::Color::parse(context, i)) {
+ color = Some(value);
+ any = true;
+ continue
+ }
+ }
+ if style.is_none() {
+ if let Ok(value) = input.try_parse(|input| outline_style::parse(context, input)) {
+ style = Some(value);
+ any = true;
+ continue
+ }
+ }
+ if width.is_none() {
+ if let Ok(value) = input.try_parse(|input| outline_width::parse(context, input)) {
+ width = Some(value);
+ any = true;
+ continue
+ }
+ }
+ break
+ }
+ if any {
+ Ok(expanded! {
+ outline_color: unwrap_or_initial!(outline_color, color),
+ outline_style: unwrap_or_initial!(outline_style, style),
+ outline_width: unwrap_or_initial!(outline_width, width),
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let mut wrote_value = false;
+
+ % for name in "color style width".split():
+ if *self.outline_${name} != outline_${name}::get_initial_specified_value() {
+ if wrote_value {
+ dest.write_char(' ')?;
+ }
+ self.outline_${name}.to_css(dest)?;
+ wrote_value = true;
+ }
+ % endfor
+
+ if !wrote_value {
+ self.outline_style.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/padding.mako.rs b/servo/components/style/properties/shorthands/padding.mako.rs
new file mode 100644
index 0000000000..11ddfed3b1
--- /dev/null
+++ b/servo/components/style/properties/shorthands/padding.mako.rs
@@ -0,0 +1,58 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+${helpers.four_sides_shorthand(
+ "padding",
+ "padding-%s",
+ "specified::NonNegativeLengthPercentage::parse",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-box-3/#propdef-padding",
+ allow_quirks="Yes",
+)}
+
+${helpers.two_properties_shorthand(
+ "padding-block",
+ "padding-block-start",
+ "padding-block-end",
+ "specified::NonNegativeLengthPercentage::parse",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-logical/#propdef-padding-block"
+)}
+
+${helpers.two_properties_shorthand(
+ "padding-inline",
+ "padding-inline-start",
+ "padding-inline-end",
+ "specified::NonNegativeLengthPercentage::parse",
+ engines="gecko servo-2013 servo-2020",
+ spec="https://drafts.csswg.org/css-logical/#propdef-padding-inline"
+)}
+
+${helpers.four_sides_shorthand(
+ "scroll-padding",
+ "scroll-padding-%s",
+ "specified::NonNegativeLengthPercentageOrAuto::parse",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding"
+)}
+
+${helpers.two_properties_shorthand(
+ "scroll-padding-block",
+ "scroll-padding-block-start",
+ "scroll-padding-block-end",
+ "specified::NonNegativeLengthPercentageOrAuto::parse",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding-block"
+)}
+
+${helpers.two_properties_shorthand(
+ "scroll-padding-inline",
+ "scroll-padding-inline-start",
+ "scroll-padding-inline-end",
+ "specified::NonNegativeLengthPercentageOrAuto::parse",
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding-inline"
+)}
diff --git a/servo/components/style/properties/shorthands/position.mako.rs b/servo/components/style/properties/shorthands/position.mako.rs
new file mode 100644
index 0000000000..ed7df5e27a
--- /dev/null
+++ b/servo/components/style/properties/shorthands/position.mako.rs
@@ -0,0 +1,891 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%helpers:shorthand name="flex-flow"
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ sub_properties="flex-direction flex-wrap"
+ extra_prefixes="webkit"
+ spec="https://drafts.csswg.org/css-flexbox/#flex-flow-property">
+ use crate::properties::longhands::{flex_direction, flex_wrap};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut direction = None;
+ let mut wrap = None;
+ loop {
+ if direction.is_none() {
+ if let Ok(value) = input.try_parse(|input| flex_direction::parse(context, input)) {
+ direction = Some(value);
+ continue
+ }
+ }
+ if wrap.is_none() {
+ if let Ok(value) = input.try_parse(|input| flex_wrap::parse(context, input)) {
+ wrap = Some(value);
+ continue
+ }
+ }
+ break
+ }
+
+ if direction.is_none() && wrap.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ Ok(expanded! {
+ flex_direction: unwrap_or_initial!(flex_direction, direction),
+ flex_wrap: unwrap_or_initial!(flex_wrap, wrap),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ if *self.flex_direction == flex_direction::get_initial_specified_value() &&
+ *self.flex_wrap != flex_wrap::get_initial_specified_value() {
+ return self.flex_wrap.to_css(dest)
+ }
+ self.flex_direction.to_css(dest)?;
+ if *self.flex_wrap != flex_wrap::get_initial_specified_value() {
+ dest.write_char(' ')?;
+ self.flex_wrap.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand name="flex"
+ engines="gecko servo-2013 servo-2020",
+ servo_2020_pref="layout.flexbox.enabled",
+ sub_properties="flex-grow flex-shrink flex-basis"
+ extra_prefixes="webkit"
+ derive_serialize="True"
+ spec="https://drafts.csswg.org/css-flexbox/#flex-property">
+ use crate::parser::Parse;
+ use crate::values::specified::NonNegativeNumber;
+ use crate::properties::longhands::flex_basis::SpecifiedValue as FlexBasis;
+
+ fn parse_flexibility<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(NonNegativeNumber, Option<NonNegativeNumber>),ParseError<'i>> {
+ let grow = NonNegativeNumber::parse(context, input)?;
+ let shrink = input.try_parse(|i| NonNegativeNumber::parse(context, i)).ok();
+ Ok((grow, shrink))
+ }
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut grow = None;
+ let mut shrink = None;
+ let mut basis = None;
+
+ if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() {
+ return Ok(expanded! {
+ flex_grow: NonNegativeNumber::new(0.0),
+ flex_shrink: NonNegativeNumber::new(0.0),
+ flex_basis: FlexBasis::auto(),
+ })
+ }
+ loop {
+ if grow.is_none() {
+ if let Ok((flex_grow, flex_shrink)) = input.try_parse(|i| parse_flexibility(context, i)) {
+ grow = Some(flex_grow);
+ shrink = flex_shrink;
+ continue
+ }
+ }
+ if basis.is_none() {
+ if let Ok(value) = input.try_parse(|input| FlexBasis::parse(context, input)) {
+ basis = Some(value);
+ continue
+ }
+ }
+ break
+ }
+
+ if grow.is_none() && basis.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ Ok(expanded! {
+ flex_grow: grow.unwrap_or(NonNegativeNumber::new(1.0)),
+ flex_shrink: shrink.unwrap_or(NonNegativeNumber::new(1.0)),
+ // Per spec, this should be SpecifiedValue::zero(), but all
+ // browsers currently agree on using `0%`. This is a spec
+ // change which hasn't been adopted by browsers:
+ // https://github.com/w3c/csswg-drafts/commit/2c446befdf0f686217905bdd7c92409f6bd3921b
+ flex_basis: basis.unwrap_or(FlexBasis::zero_percent()),
+ })
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="gap"
+ engines="gecko"
+ aliases="grid-gap"
+ sub_properties="row-gap column-gap"
+ spec="https://drafts.csswg.org/css-align-3/#gap-shorthand"
+>
+ use crate::properties::longhands::{row_gap, column_gap};
+
+ pub fn parse_value<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>)
+ -> Result<Longhands, ParseError<'i>> {
+ let r_gap = row_gap::parse(context, input)?;
+ let c_gap = input.try_parse(|input| column_gap::parse(context, input)).unwrap_or(r_gap.clone());
+
+ Ok(expanded! {
+ row_gap: r_gap,
+ column_gap: c_gap,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ if self.row_gap == self.column_gap {
+ self.row_gap.to_css(dest)
+ } else {
+ self.row_gap.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.column_gap.to_css(dest)
+ }
+ }
+ }
+
+</%helpers:shorthand>
+
+% for kind in ["row", "column"]:
+<%helpers:shorthand
+ name="grid-${kind}"
+ sub_properties="grid-${kind}-start grid-${kind}-end"
+ engines="gecko",
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid-${kind}"
+>
+ use crate::values::specified::GridLine;
+ use crate::parser::Parse;
+ use crate::Zero;
+
+ // NOTE: Since both the shorthands have the same code, we should (re-)use code from one to implement
+ // the other. This might not be a big deal for now, but we should consider looking into this in the future
+ // to limit the amount of code generated.
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let start = input.try_parse(|i| GridLine::parse(context, i))?;
+ let end = if input.try_parse(|i| i.expect_delim('/')).is_ok() {
+ GridLine::parse(context, input)?
+ } else {
+ let mut line = GridLine::auto();
+ if start.line_num.is_zero() && !start.is_span {
+ line.ident = start.ident.clone(); // ident from start value should be taken
+ }
+
+ line
+ };
+
+ Ok(expanded! {
+ grid_${kind}_start: start,
+ grid_${kind}_end: end,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ // Return the shortest possible serialization of the `grid-${kind}-[start/end]` values.
+ // This function exploits the opportunities to omit the end value per this spec text:
+ //
+ // https://drafts.csswg.org/css-grid/#propdef-grid-column
+ // "When the second value is omitted, if the first value is a <custom-ident>,
+ // the grid-row-end/grid-column-end longhand is also set to that <custom-ident>;
+ // otherwise, it is set to auto."
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.grid_${kind}_start.to_css(dest)?;
+ if self.grid_${kind}_start.can_omit(self.grid_${kind}_end) {
+ return Ok(()); // the end value is redundant
+ }
+ dest.write_str(" / ")?;
+ self.grid_${kind}_end.to_css(dest)
+ }
+ }
+</%helpers:shorthand>
+% endfor
+
+<%helpers:shorthand
+ name="grid-area"
+ engines="gecko"
+ sub_properties="grid-row-start grid-row-end grid-column-start grid-column-end"
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid-area"
+>
+ use crate::values::specified::GridLine;
+ use crate::parser::Parse;
+ use crate::Zero;
+
+ // The code is the same as `grid-{row,column}` except that this can have four values at most.
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ fn line_with_ident_from(other: &GridLine) -> GridLine {
+ let mut this = GridLine::auto();
+ if other.line_num.is_zero() && !other.is_span {
+ this.ident = other.ident.clone();
+ }
+
+ this
+ }
+
+ let row_start = input.try_parse(|i| GridLine::parse(context, i))?;
+ let (column_start, row_end, column_end) = if input.try_parse(|i| i.expect_delim('/')).is_ok() {
+ let column_start = GridLine::parse(context, input)?;
+ let (row_end, column_end) = if input.try_parse(|i| i.expect_delim('/')).is_ok() {
+ let row_end = GridLine::parse(context, input)?;
+ let column_end = if input.try_parse(|i| i.expect_delim('/')).is_ok() {
+ GridLine::parse(context, input)?
+ } else { // grid-column-end has not been given
+ line_with_ident_from(&column_start)
+ };
+
+ (row_end, column_end)
+ } else { // grid-row-start and grid-column-start has been given
+ let row_end = line_with_ident_from(&row_start);
+ let column_end = line_with_ident_from(&column_start);
+ (row_end, column_end)
+ };
+
+ (column_start, row_end, column_end)
+ } else { // only grid-row-start is given
+ let line = line_with_ident_from(&row_start);
+ (line.clone(), line.clone(), line)
+ };
+
+ Ok(expanded! {
+ grid_row_start: row_start,
+ grid_row_end: row_end,
+ grid_column_start: column_start,
+ grid_column_end: column_end,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ // Return the shortest possible serialization of the `grid-[column/row]-[start/end]` values.
+ // This function exploits the opportunities to omit trailing values per this spec text:
+ //
+ // https://drafts.csswg.org/css-grid/#propdef-grid-area
+ // "If four <grid-line> values are specified, grid-row-start is set to the first value,
+ // grid-column-start is set to the second value, grid-row-end is set to the third value,
+ // and grid-column-end is set to the fourth value.
+ //
+ // When grid-column-end is omitted, if grid-column-start is a <custom-ident>,
+ // grid-column-end is set to that <custom-ident>; otherwise, it is set to auto.
+ //
+ // When grid-row-end is omitted, if grid-row-start is a <custom-ident>, grid-row-end is
+ // set to that <custom-ident>; otherwise, it is set to auto.
+ //
+ // When grid-column-start is omitted, if grid-row-start is a <custom-ident>, all four
+ // longhands are set to that value. Otherwise, it is set to auto."
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.grid_row_start.to_css(dest)?;
+ let mut trailing_values = 3;
+ if self.grid_column_start.can_omit(self.grid_column_end) {
+ trailing_values -= 1;
+ if self.grid_row_start.can_omit(self.grid_row_end) {
+ trailing_values -= 1;
+ if self.grid_row_start.can_omit(self.grid_column_start) {
+ trailing_values -= 1;
+ }
+ }
+ }
+ let values = [&self.grid_column_start, &self.grid_row_end, &self.grid_column_end];
+ for value in values.iter().take(trailing_values) {
+ dest.write_str(" / ")?;
+ value.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="grid-template"
+ engines="gecko"
+ sub_properties="grid-template-rows grid-template-columns grid-template-areas"
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid-template"
+>
+ use crate::parser::Parse;
+ use servo_arc::Arc;
+ use crate::values::generics::grid::{TrackSize, TrackList};
+ use crate::values::generics::grid::{TrackListValue, concat_serialize_idents};
+ use crate::values::specified::{GridTemplateComponent, GenericGridTemplateComponent};
+ use crate::values::specified::grid::parse_line_names;
+ use crate::values::specified::position::{GridTemplateAreas, TemplateAreasParser, TemplateAreasArc};
+
+ /// Parsing for `<grid-template>` shorthand (also used by `grid` shorthand).
+ pub fn parse_grid_template<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(GridTemplateComponent, GridTemplateComponent, GridTemplateAreas), ParseError<'i>> {
+ // Other shorthand sub properties also parse the `none` keyword and this shorthand
+ // should know after this keyword there is nothing to parse. Otherwise it gets
+ // confused and rejects the sub properties that contains `none`.
+ <% keywords = {
+ "none": "GenericGridTemplateComponent::None",
+ }
+ %>
+ % for keyword, rust_type in keywords.items():
+ if let Ok(x) = input.try_parse(|i| {
+ if i.try_parse(|i| i.expect_ident_matching("${keyword}")).is_ok() {
+ if !i.is_exhausted() {
+ return Err(());
+ }
+ return Ok((${rust_type}, ${rust_type}, GridTemplateAreas::None));
+ }
+ Err(())
+ }) {
+ return Ok(x);
+ }
+ % endfor
+
+ let first_line_names = input.try_parse(parse_line_names).unwrap_or_default();
+ let mut areas_parser = TemplateAreasParser::default();
+ if areas_parser.try_parse_string(input).is_ok() {
+ let mut values = vec![];
+ let mut line_names = vec![];
+ line_names.push(first_line_names);
+ loop {
+ let size = input.try_parse(|i| TrackSize::parse(context, i)).unwrap_or_default();
+ values.push(TrackListValue::TrackSize(size));
+ let mut names = input.try_parse(parse_line_names).unwrap_or_default();
+ let more_names = input.try_parse(parse_line_names);
+
+ match areas_parser.try_parse_string(input) {
+ Ok(()) => {
+ if let Ok(v) = more_names {
+ // We got `[names] [more_names] "string"` - merge the two name lists.
+ let mut names_vec = names.into_vec();
+ names_vec.extend(v.into_iter());
+ names = names_vec.into();
+ }
+ line_names.push(names);
+ },
+ Err(e) => {
+ if more_names.is_ok() {
+ // We've parsed `"string" [names] [more_names]` but then failed to parse another `"string"`.
+ // The grammar doesn't allow two trailing `<line-names>` so this is an invalid value.
+ return Err(e);
+ }
+ // only the named area determines whether we should bail out
+ line_names.push(names);
+ break
+ },
+ };
+ }
+
+ if line_names.len() == values.len() {
+ // should be one longer than track sizes
+ line_names.push(Default::default());
+ }
+
+ let template_areas = areas_parser.finish()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?;
+ let template_rows = TrackList {
+ values: values.into(),
+ line_names: line_names.into(),
+ auto_repeat_index: std::usize::MAX,
+ };
+
+ let template_cols = if input.try_parse(|i| i.expect_delim('/')).is_ok() {
+ let value = GridTemplateComponent::parse_without_none(context, input)?;
+ if let GenericGridTemplateComponent::TrackList(ref list) = value {
+ if !list.is_explicit() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+
+ value
+ } else {
+ GridTemplateComponent::default()
+ };
+
+ Ok((
+ GenericGridTemplateComponent::TrackList(Box::new(template_rows)),
+ template_cols,
+ GridTemplateAreas::Areas(TemplateAreasArc(Arc::new(template_areas)))
+ ))
+ } else {
+ let mut template_rows = GridTemplateComponent::parse(context, input)?;
+ if let GenericGridTemplateComponent::TrackList(ref mut list) = template_rows {
+ // Fist line names are parsed already and it shouldn't be parsed again.
+ // If line names are not empty, that means given property value is not acceptable
+ if list.line_names[0].is_empty() {
+ list.line_names[0] = first_line_names; // won't panic
+ } else {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ input.expect_delim('/')?;
+ Ok((template_rows, GridTemplateComponent::parse(context, input)?, GridTemplateAreas::None))
+ }
+ }
+
+ #[inline]
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let (rows, columns, areas) = parse_grid_template(context, input)?;
+ Ok(expanded! {
+ grid_template_rows: rows,
+ grid_template_columns: columns,
+ grid_template_areas: areas,
+ })
+ }
+
+ /// Serialization for `<grid-template>` shorthand (also used by `grid` shorthand).
+ pub fn serialize_grid_template<W>(
+ template_rows: &GridTemplateComponent,
+ template_columns: &GridTemplateComponent,
+ template_areas: &GridTemplateAreas,
+ dest: &mut CssWriter<W>,
+ ) -> fmt::Result
+ where
+ W: Write {
+ match *template_areas {
+ GridTemplateAreas::None => {
+ if template_rows.is_initial() && template_columns.is_initial() {
+ return GridTemplateComponent::default().to_css(dest);
+ }
+ template_rows.to_css(dest)?;
+ dest.write_str(" / ")?;
+ template_columns.to_css(dest)
+ },
+ GridTemplateAreas::Areas(ref areas) => {
+ // The length of template-area and template-rows values should be equal.
+ if areas.0.strings.len() != template_rows.track_list_len() {
+ return Ok(());
+ }
+
+ let track_list = match *template_rows {
+ GenericGridTemplateComponent::TrackList(ref list) => {
+ // We should fail if there is a `repeat` function.
+ // `grid` and `grid-template` shorthands doesn't accept
+ // that. Only longhand accepts.
+ if !list.is_explicit() {
+ return Ok(());
+ }
+ list
+ },
+ // Others template components shouldn't exist with normal shorthand values.
+ // But if we need to serialize a group of longhand sub-properties for
+ // the shorthand, we should be able to return empty string instead of crashing.
+ _ => return Ok(()),
+ };
+
+ // We need to check some values that longhand accepts but shorthands don't.
+ match *template_columns {
+ // We should fail if there is a `repeat` function. `grid` and
+ // `grid-template` shorthands doesn't accept that. Only longhand accepts that.
+ GenericGridTemplateComponent::TrackList(ref list) => {
+ if !list.is_explicit() {
+ return Ok(());
+ }
+ },
+ // Also the shorthands don't accept subgrids unlike longhand.
+ // We should fail without an error here.
+ GenericGridTemplateComponent::Subgrid(_) => {
+ return Ok(());
+ },
+ _ => {},
+ }
+
+ let mut names_iter = track_list.line_names.iter();
+ for (((i, string), names), value) in areas.0.strings.iter().enumerate()
+ .zip(&mut names_iter)
+ .zip(track_list.values.iter()) {
+ if i > 0 {
+ dest.write_char(' ')?;
+ }
+
+ if !names.is_empty() {
+ concat_serialize_idents("[", "] ", names, " ", dest)?;
+ }
+
+ string.to_css(dest)?;
+
+ // If the track size is the initial value then it's redundant here.
+ if !value.is_initial() {
+ dest.write_char(' ')?;
+ value.to_css(dest)?;
+ }
+ }
+
+ if let Some(names) = names_iter.next() {
+ concat_serialize_idents(" [", "]", names, " ", dest)?;
+ }
+
+ if let GenericGridTemplateComponent::TrackList(ref list) = *template_columns {
+ dest.write_str(" / ")?;
+ list.to_css(dest)?;
+ }
+
+ Ok(())
+ },
+ }
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ serialize_grid_template(
+ self.grid_template_rows,
+ self.grid_template_columns,
+ self.grid_template_areas,
+ dest
+ )
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="grid"
+ engines="gecko"
+ sub_properties="grid-template-rows grid-template-columns grid-template-areas
+ grid-auto-rows grid-auto-columns grid-auto-flow"
+ spec="https://drafts.csswg.org/css-grid/#propdef-grid"
+>
+ use crate::parser::Parse;
+ use crate::properties::longhands::{grid_auto_columns, grid_auto_rows, grid_auto_flow};
+ use crate::values::generics::grid::GridTemplateComponent;
+ use crate::values::specified::{GenericGridTemplateComponent, ImplicitGridTracks};
+ use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas};
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let mut temp_rows = GridTemplateComponent::default();
+ let mut temp_cols = GridTemplateComponent::default();
+ let mut temp_areas = GridTemplateAreas::None;
+ let mut auto_rows = ImplicitGridTracks::default();
+ let mut auto_cols = ImplicitGridTracks::default();
+ let mut flow = grid_auto_flow::get_initial_value();
+
+ fn parse_auto_flow<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ is_row: bool,
+ ) -> Result<GridAutoFlow, ParseError<'i>> {
+ let mut track = None;
+ let mut dense = GridAutoFlow::empty();
+
+ for _ in 0..2 {
+ if input.try_parse(|i| i.expect_ident_matching("auto-flow")).is_ok() {
+ track = if is_row {
+ Some(GridAutoFlow::ROW)
+ } else {
+ Some(GridAutoFlow::COLUMN)
+ };
+ } else if input.try_parse(|i| i.expect_ident_matching("dense")).is_ok() {
+ dense = GridAutoFlow::DENSE
+ } else {
+ break
+ }
+ }
+
+ if track.is_some() {
+ Ok(track.unwrap() | dense)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+
+ if let Ok((rows, cols, areas)) = input.try_parse(|i| super::grid_template::parse_grid_template(context, i)) {
+ temp_rows = rows;
+ temp_cols = cols;
+ temp_areas = areas;
+ } else if let Ok(rows) = input.try_parse(|i| GridTemplateComponent::parse(context, i)) {
+ temp_rows = rows;
+ input.expect_delim('/')?;
+ flow = parse_auto_flow(input, false)?;
+ auto_cols = input.try_parse(|i| grid_auto_columns::parse(context, i)).unwrap_or_default();
+ } else {
+ flow = parse_auto_flow(input, true)?;
+ auto_rows = input.try_parse(|i| grid_auto_rows::parse(context, i)).unwrap_or_default();
+ input.expect_delim('/')?;
+ temp_cols = GridTemplateComponent::parse(context, input)?;
+ }
+
+ Ok(expanded! {
+ grid_template_rows: temp_rows,
+ grid_template_columns: temp_cols,
+ grid_template_areas: temp_areas,
+ grid_auto_rows: auto_rows,
+ grid_auto_columns: auto_cols,
+ grid_auto_flow: flow,
+ })
+ }
+
+ impl<'a> LonghandsToSerialize<'a> {
+ /// Returns true if other sub properties except template-{rows,columns} are initial.
+ fn is_grid_template(&self) -> bool {
+ self.grid_auto_rows.is_initial() &&
+ self.grid_auto_columns.is_initial() &&
+ *self.grid_auto_flow == grid_auto_flow::get_initial_value()
+ }
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ if self.is_grid_template() {
+ return super::grid_template::serialize_grid_template(
+ self.grid_template_rows,
+ self.grid_template_columns,
+ self.grid_template_areas,
+ dest
+ );
+ }
+
+ if *self.grid_template_areas != GridTemplateAreas::None {
+ // No other syntax can set the template areas, so fail to
+ // serialize.
+ return Ok(());
+ }
+
+ if self.grid_auto_flow.contains(GridAutoFlow::COLUMN) {
+ // It should fail to serialize if other branch of the if condition's values are set.
+ if !self.grid_auto_rows.is_initial() ||
+ !self.grid_template_columns.is_initial() {
+ return Ok(());
+ }
+
+ // It should fail to serialize if template-rows value is not Explicit.
+ if let GenericGridTemplateComponent::TrackList(ref list) = *self.grid_template_rows {
+ if !list.is_explicit() {
+ return Ok(());
+ }
+ }
+
+ self.grid_template_rows.to_css(dest)?;
+ dest.write_str(" / auto-flow")?;
+ if self.grid_auto_flow.contains(GridAutoFlow::DENSE) {
+ dest.write_str(" dense")?;
+ }
+
+ if !self.grid_auto_columns.is_initial() {
+ dest.write_char(' ')?;
+ self.grid_auto_columns.to_css(dest)?;
+ }
+
+ return Ok(());
+ }
+
+ // It should fail to serialize if other branch of the if condition's values are set.
+ if !self.grid_auto_columns.is_initial() ||
+ !self.grid_template_rows.is_initial() {
+ return Ok(());
+ }
+
+ // It should fail to serialize if template-column value is not Explicit.
+ if let GenericGridTemplateComponent::TrackList(ref list) = *self.grid_template_columns {
+ if !list.is_explicit() {
+ return Ok(());
+ }
+ }
+
+ dest.write_str("auto-flow")?;
+ if self.grid_auto_flow.contains(GridAutoFlow::DENSE) {
+ dest.write_str(" dense")?;
+ }
+
+ if !self.grid_auto_rows.is_initial() {
+ dest.write_char(' ')?;
+ self.grid_auto_rows.to_css(dest)?;
+ }
+
+ dest.write_str(" / ")?;
+ self.grid_template_columns.to_css(dest)?;
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="place-content"
+ engines="gecko"
+ sub_properties="align-content justify-content"
+ spec="https://drafts.csswg.org/css-align/#propdef-place-content"
+>
+ use crate::values::specified::align::{AlignContent, JustifyContent, ContentDistribution, AxisDirection};
+
+ pub fn parse_value<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let align_content =
+ ContentDistribution::parse(input, AxisDirection::Block)?;
+
+ let justify_content = input.try_parse(|input| {
+ ContentDistribution::parse(input, AxisDirection::Inline)
+ });
+
+ let justify_content = match justify_content {
+ Ok(v) => v,
+ Err(..) => {
+ // https://drafts.csswg.org/css-align-3/#place-content:
+ //
+ // The second value is assigned to justify-content; if
+ // omitted, it is copied from the first value, unless that
+ // value is a <baseline-position> in which case it is
+ // defaulted to start.
+ //
+ if !align_content.is_baseline_position() {
+ align_content
+ } else {
+ ContentDistribution::start()
+ }
+ }
+ };
+
+ Ok(expanded! {
+ align_content: AlignContent(align_content),
+ justify_content: JustifyContent(justify_content),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.align_content.to_css(dest)?;
+ if self.align_content.0 != self.justify_content.0 {
+ dest.write_char(' ')?;
+ self.justify_content.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="place-self"
+ engines="gecko"
+ sub_properties="align-self justify-self"
+ spec="https://drafts.csswg.org/css-align/#place-self-property"
+>
+ use crate::values::specified::align::{AlignSelf, JustifySelf, SelfAlignment, AxisDirection};
+
+ pub fn parse_value<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let align = SelfAlignment::parse(input, AxisDirection::Block)?;
+ let justify = input.try_parse(|input| SelfAlignment::parse(input, AxisDirection::Inline));
+
+ let justify = match justify {
+ Ok(v) => v,
+ Err(..) => {
+ debug_assert!(align.is_valid_on_both_axes());
+ align
+ }
+ };
+
+ Ok(expanded! {
+ align_self: AlignSelf(align),
+ justify_self: JustifySelf(justify),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.align_self.to_css(dest)?;
+ if self.align_self.0 != self.justify_self.0 {
+ dest.write_char(' ')?;
+ self.justify_self.to_css(dest)?;
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ name="place-items"
+ engines="gecko"
+ sub_properties="align-items justify-items"
+ spec="https://drafts.csswg.org/css-align/#place-items-property"
+>
+ use crate::values::specified::align::{AlignItems, JustifyItems};
+ use crate::parser::Parse;
+
+ impl From<AlignItems> for JustifyItems {
+ fn from(align: AlignItems) -> JustifyItems {
+ JustifyItems(align.0)
+ }
+ }
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ let align = AlignItems::parse(context, input)?;
+ let justify =
+ input.try_parse(|input| JustifyItems::parse(context, input))
+ .unwrap_or_else(|_| JustifyItems::from(align));
+
+ Ok(expanded! {
+ align_items: align,
+ justify_items: justify,
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ self.align_items.to_css(dest)?;
+ if self.align_items.0 != self.justify_items.0 {
+ dest.write_char(' ')?;
+ self.justify_items.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+// See https://github.com/w3c/csswg-drafts/issues/3525 for the quirks stuff.
+${helpers.four_sides_shorthand(
+ "inset",
+ "%s",
+ "specified::LengthPercentageOrAuto::parse",
+ engines="gecko servo-2013",
+ spec="https://drafts.csswg.org/css-logical/#propdef-inset",
+ allow_quirks="No",
+)}
+
+${helpers.two_properties_shorthand(
+ "inset-block",
+ "inset-block-start",
+ "inset-block-end",
+ "specified::LengthPercentageOrAuto::parse",
+ engines="gecko servo-2013",
+ spec="https://drafts.csswg.org/css-logical/#propdef-inset-block"
+)}
+
+${helpers.two_properties_shorthand(
+ "inset-inline",
+ "inset-inline-start",
+ "inset-inline-end",
+ "specified::LengthPercentageOrAuto::parse",
+ engines="gecko servo-2013",
+ spec="https://drafts.csswg.org/css-logical/#propdef-inset-inline"
+)}
+
+${helpers.two_properties_shorthand(
+ "contain-intrinsic-size",
+ "contain-intrinsic-width",
+ "contain-intrinsic-height",
+ engines="gecko",
+ gecko_pref="layout.css.contain-intrinsic-size.enabled",
+ spec="https://drafts.csswg.org/css-sizing-4/#intrinsic-size-override",
+)}
diff --git a/servo/components/style/properties/shorthands/svg.mako.rs b/servo/components/style/properties/shorthands/svg.mako.rs
new file mode 100644
index 0000000000..cf34b116ee
--- /dev/null
+++ b/servo/components/style/properties/shorthands/svg.mako.rs
@@ -0,0 +1,287 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%helpers:shorthand name="mask" engines="gecko" extra_prefixes="webkit"
+ flags="SHORTHAND_IN_GETCS"
+ sub_properties="mask-mode mask-repeat mask-clip mask-origin mask-composite mask-position-x
+ mask-position-y mask-size mask-image"
+ spec="https://drafts.fxtf.org/css-masking/#propdef-mask">
+ use crate::properties::longhands::{mask_mode, mask_repeat, mask_clip, mask_origin, mask_composite, mask_position_x,
+ mask_position_y};
+ use crate::properties::longhands::{mask_size, mask_image};
+ use crate::values::specified::{Position, PositionComponent};
+ use crate::parser::Parse;
+
+ // FIXME(emilio): These two mask types should be the same!
+ impl From<mask_origin::single_value::SpecifiedValue> for mask_clip::single_value::SpecifiedValue {
+ fn from(origin: mask_origin::single_value::SpecifiedValue) -> mask_clip::single_value::SpecifiedValue {
+ match origin {
+ mask_origin::single_value::SpecifiedValue::ContentBox =>
+ mask_clip::single_value::SpecifiedValue::ContentBox,
+ mask_origin::single_value::SpecifiedValue::PaddingBox =>
+ mask_clip::single_value::SpecifiedValue::PaddingBox ,
+ mask_origin::single_value::SpecifiedValue::BorderBox =>
+ mask_clip::single_value::SpecifiedValue::BorderBox,
+ % if engine == "gecko":
+ mask_origin::single_value::SpecifiedValue::FillBox =>
+ mask_clip::single_value::SpecifiedValue::FillBox ,
+ mask_origin::single_value::SpecifiedValue::StrokeBox =>
+ mask_clip::single_value::SpecifiedValue::StrokeBox,
+ mask_origin::single_value::SpecifiedValue::ViewBox=>
+ mask_clip::single_value::SpecifiedValue::ViewBox,
+ % endif
+ }
+ }
+ }
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ % for name in "image mode position_x position_y size repeat origin clip composite".split():
+ // Vec grows from 0 to 4 by default on first push(). So allocate with
+ // capacity 1, so in the common case of only one item we don't way
+ // overallocate, then shrink. Note that we always push at least one
+ // item if parsing succeeds.
+ let mut mask_${name} = Vec::with_capacity(1);
+ % endfor
+
+ input.parse_comma_separated(|input| {
+ % for name in "image mode position size repeat origin clip composite".split():
+ let mut ${name} = None;
+ % endfor
+ loop {
+ if image.is_none() {
+ if let Ok(value) = input.try_parse(|input| mask_image::single_value
+ ::parse(context, input)) {
+ image = Some(value);
+ continue
+ }
+ }
+ if position.is_none() {
+ if let Ok(value) = input.try_parse(|input| Position::parse(context, input)) {
+ position = Some(value);
+
+ // Parse mask size, if applicable.
+ size = input.try_parse(|input| {
+ input.expect_delim('/')?;
+ mask_size::single_value::parse(context, input)
+ }).ok();
+
+ continue
+ }
+ }
+ % for name in "repeat origin clip composite mode".split():
+ if ${name}.is_none() {
+ if let Ok(value) = input.try_parse(|input| mask_${name}::single_value
+ ::parse(context, input)) {
+ ${name} = Some(value);
+ continue
+ }
+ }
+ % endfor
+ break
+ }
+ if clip.is_none() {
+ if let Some(origin) = origin {
+ clip = Some(mask_clip::single_value::SpecifiedValue::from(origin));
+ }
+ }
+ let mut any = false;
+ % for name in "image mode position size repeat origin clip composite".split():
+ any = any || ${name}.is_some();
+ % endfor
+ if any {
+ if let Some(position) = position {
+ mask_position_x.push(position.horizontal);
+ mask_position_y.push(position.vertical);
+ } else {
+ mask_position_x.push(PositionComponent::zero());
+ mask_position_y.push(PositionComponent::zero());
+ }
+ % for name in "image mode size repeat origin clip composite".split():
+ if let Some(m_${name}) = ${name} {
+ mask_${name}.push(m_${name});
+ } else {
+ mask_${name}.push(mask_${name}::single_value
+ ::get_initial_specified_value());
+ }
+ % endfor
+ Ok(())
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ })?;
+
+ Ok(expanded! {
+ % for name in "image mode position_x position_y size repeat origin clip composite".split():
+ mask_${name}: mask_${name}::SpecifiedValue(mask_${name}.into()),
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ use crate::properties::longhands::mask_origin::single_value::computed_value::T as Origin;
+ use crate::properties::longhands::mask_clip::single_value::computed_value::T as Clip;
+ use style_traits::values::SequenceWriter;
+
+ let len = self.mask_image.0.len();
+ if len == 0 {
+ return Ok(());
+ }
+ % for name in "mode position_x position_y size repeat origin clip composite".split():
+ if self.mask_${name}.0.len() != len {
+ return Ok(());
+ }
+ % endfor
+
+ // For each <mask-layer>, we serialize it according to the following order:
+ // <mask-layer> =
+ // <mask-reference> ||
+ // <position> [ / <bg-size> ]? ||
+ // <repeat-style> ||
+ // <geometry-box> ||
+ // [ <geometry-box> | no-clip ] ||
+ // <compositing-operator> ||
+ // <masking-mode>
+ // https://drafts.fxtf.org/css-masking-1/#the-mask
+ for i in 0..len {
+ if i > 0 {
+ dest.write_str(", ")?;
+ }
+
+ % for name in "image mode position_x position_y size repeat origin clip composite".split():
+ let ${name} = &self.mask_${name}.0[i];
+ % endfor
+
+ let mut has_other = false;
+ % for name in "image mode size repeat composite".split():
+ let has_${name} =
+ *${name} != mask_${name}::single_value::get_initial_specified_value();
+ has_other |= has_${name};
+ % endfor
+ let has_position = *position_x != PositionComponent::zero()
+ || *position_y != PositionComponent::zero();
+ let has_origin = *origin != Origin::BorderBox;
+ let has_clip = *clip != Clip::BorderBox;
+
+ // If all are initial values, we serialize mask-image.
+ if !has_other && !has_position && !has_origin && !has_clip {
+ return image.to_css(dest);
+ }
+
+ let mut writer = SequenceWriter::new(dest, " ");
+ // <mask-reference>
+ if has_image {
+ writer.item(image)?;
+ }
+
+ // <position> [ / <bg-size> ]?
+ if has_position || has_size {
+ writer.item(&Position {
+ horizontal: position_x.clone(),
+ vertical: position_y.clone()
+ })?;
+
+ if has_size {
+ writer.raw_item("/")?;
+ writer.item(size)?;
+ }
+ }
+
+ // <repeat-style>
+ if has_repeat {
+ writer.item(repeat)?;
+ }
+
+ // <geometry-box>
+ if has_origin {
+ writer.item(origin)?;
+ }
+
+ // [ <geometry-box> | no-clip ]
+ if has_clip && *clip != From::from(*origin) {
+ writer.item(clip)?;
+ }
+
+ // <compositing-operator>
+ if has_composite {
+ writer.item(composite)?;
+ }
+
+ // <masking-mode>
+ if has_mode {
+ writer.item(mode)?;
+ }
+ }
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand name="mask-position" engines="gecko" extra_prefixes="webkit"
+ flags="SHORTHAND_IN_GETCS"
+ sub_properties="mask-position-x mask-position-y"
+ spec="https://drafts.csswg.org/css-masks-4/#the-mask-position">
+ use crate::properties::longhands::{mask_position_x,mask_position_y};
+ use crate::values::specified::position::Position;
+ use crate::parser::Parse;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ // Vec grows from 0 to 4 by default on first push(). So allocate with
+ // capacity 1, so in the common case of only one item we don't way
+ // overallocate, then shrink. Note that we always push at least one
+ // item if parsing succeeds.
+ let mut position_x = Vec::with_capacity(1);
+ let mut position_y = Vec::with_capacity(1);
+ let mut any = false;
+
+ input.parse_comma_separated(|input| {
+ let value = Position::parse(context, input)?;
+ position_x.push(value.horizontal);
+ position_y.push(value.vertical);
+ any = true;
+ Ok(())
+ })?;
+
+ if !any {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+
+ Ok(expanded! {
+ mask_position_x: mask_position_x::SpecifiedValue(position_x.into()),
+ mask_position_y: mask_position_y::SpecifiedValue(position_y.into()),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let len = self.mask_position_x.0.len();
+ if len == 0 || self.mask_position_y.0.len() != len {
+ return Ok(());
+ }
+
+ for i in 0..len {
+ Position {
+ horizontal: self.mask_position_x.0[i].clone(),
+ vertical: self.mask_position_y.0[i].clone()
+ }.to_css(dest)?;
+
+ if i < len - 1 {
+ dest.write_str(", ")?;
+ }
+ }
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/text.mako.rs b/servo/components/style/properties/shorthands/text.mako.rs
new file mode 100644
index 0000000000..5b071be2c4
--- /dev/null
+++ b/servo/components/style/properties/shorthands/text.mako.rs
@@ -0,0 +1,120 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+<%helpers:shorthand name="text-decoration"
+ engines="gecko servo-2013 servo-2020"
+ flags="SHORTHAND_IN_GETCS"
+ sub_properties="text-decoration-line
+ ${' text-decoration-style text-decoration-color text-decoration-thickness' if engine == 'gecko' else ''}"
+ spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration">
+ % if engine == "gecko":
+ use crate::values::specified;
+ use crate::properties::longhands::{text_decoration_style, text_decoration_color, text_decoration_thickness};
+ % endif
+ use crate::properties::longhands::text_decoration_line;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ % if engine == "gecko":
+ let (mut line, mut style, mut color, mut thickness, mut any) = (None, None, None, None, false);
+ % else:
+ let (mut line, mut any) = (None, false);
+ % endif
+
+ loop {
+ macro_rules! parse_component {
+ ($value:ident, $module:ident) => (
+ if $value.is_none() {
+ if let Ok(value) = input.try_parse(|input| $module::parse(context, input)) {
+ $value = Some(value);
+ any = true;
+ continue;
+ }
+ }
+ )
+ }
+
+ parse_component!(line, text_decoration_line);
+
+ % if engine == "gecko":
+ parse_component!(style, text_decoration_style);
+ parse_component!(color, text_decoration_color);
+ parse_component!(thickness, text_decoration_thickness);
+ % endif
+
+ break;
+ }
+
+ if !any {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(expanded! {
+ text_decoration_line: unwrap_or_initial!(text_decoration_line, line),
+
+ % if engine == "gecko":
+ text_decoration_style: unwrap_or_initial!(text_decoration_style, style),
+ text_decoration_color: unwrap_or_initial!(text_decoration_color, color),
+ text_decoration_thickness: unwrap_or_initial!(text_decoration_thickness, thickness),
+ % endif
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ #[allow(unused)]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ use crate::values::specified::TextDecorationLine;
+
+ let (is_solid_style, is_current_color, is_auto_thickness) =
+ (
+ % if engine == "gecko":
+ *self.text_decoration_style == text_decoration_style::SpecifiedValue::Solid,
+ *self.text_decoration_color == specified::Color::CurrentColor,
+ self.text_decoration_thickness.is_auto()
+ % else:
+ true, true, true
+ % endif
+ );
+
+ let mut has_value = false;
+ let is_none = *self.text_decoration_line == TextDecorationLine::none();
+ if (is_solid_style && is_current_color && is_auto_thickness) || !is_none {
+ self.text_decoration_line.to_css(dest)?;
+ has_value = true;
+ }
+
+ if !is_auto_thickness {
+ if has_value {
+ dest.write_char(' ')?;
+ }
+ self.text_decoration_thickness.to_css(dest)?;
+ has_value = true;
+ }
+
+ % if engine == "gecko":
+ if !is_solid_style {
+ if has_value {
+ dest.write_char(' ')?;
+ }
+ self.text_decoration_style.to_css(dest)?;
+ has_value = true;
+ }
+
+ if !is_current_color {
+ if has_value {
+ dest.write_char(' ')?;
+ }
+ self.text_decoration_color.to_css(dest)?;
+ has_value = true;
+ }
+ % endif
+
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties/shorthands/ui.mako.rs b/servo/components/style/properties/shorthands/ui.mako.rs
new file mode 100644
index 0000000000..1fdb5965fc
--- /dev/null
+++ b/servo/components/style/properties/shorthands/ui.mako.rs
@@ -0,0 +1,444 @@
+/* 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/. */
+
+<%namespace name="helpers" file="/helpers.mako.rs" />
+
+macro_rules! try_parse_one {
+ ($context: expr, $input: expr, $var: ident, $prop_module: ident) => {
+ if $var.is_none() {
+ if let Ok(value) = $input.try_parse(|i| {
+ $prop_module::single_value::parse($context, i)
+ }) {
+ $var = Some(value);
+ continue;
+ }
+ }
+ };
+}
+
+<%helpers:shorthand name="transition"
+ engines="gecko servo-2013 servo-2020"
+ extra_prefixes="moz:layout.css.prefixes.transitions webkit"
+ sub_properties="transition-property transition-duration
+ transition-timing-function
+ transition-delay"
+ spec="https://drafts.csswg.org/css-transitions/#propdef-transition">
+ use crate::parser::Parse;
+ % for prop in "delay duration property timing_function".split():
+ use crate::properties::longhands::transition_${prop};
+ % endfor
+ use crate::values::specified::TransitionProperty;
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ struct SingleTransition {
+ % for prop in "property duration timing_function delay".split():
+ transition_${prop}: transition_${prop}::SingleSpecifiedValue,
+ % endfor
+ }
+
+ fn parse_one_transition<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ first: bool,
+ ) -> Result<SingleTransition,ParseError<'i>> {
+ % for prop in "property duration timing_function delay".split():
+ let mut ${prop} = None;
+ % endfor
+
+ let mut parsed = 0;
+ loop {
+ parsed += 1;
+
+ try_parse_one!(context, input, duration, transition_duration);
+ try_parse_one!(context, input, timing_function, transition_timing_function);
+ try_parse_one!(context, input, delay, transition_delay);
+ // Must check 'transition-property' after 'transition-timing-function' since
+ // 'transition-property' accepts any keyword.
+ if property.is_none() {
+ if let Ok(value) = input.try_parse(|i| TransitionProperty::parse(context, i)) {
+ property = Some(value);
+ continue;
+ }
+
+ // 'none' is not a valid value for <single-transition-property>,
+ // so it's only acceptable as the first item.
+ if first && input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ property = Some(TransitionProperty::none());
+ continue;
+ }
+ }
+
+ parsed -= 1;
+ break
+ }
+
+ if parsed != 0 {
+ Ok(SingleTransition {
+ % for prop in "property duration timing_function delay".split():
+ transition_${prop}: ${prop}.unwrap_or_else(transition_${prop}::single_value
+ ::get_initial_specified_value),
+ % endfor
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+
+ % for prop in "property duration timing_function delay".split():
+ let mut ${prop}s = Vec::new();
+ % endfor
+
+ let mut first = true;
+ let mut has_transition_property_none = false;
+ let results = input.parse_comma_separated(|i| {
+ if has_transition_property_none {
+ // If you specify transition-property: none, multiple items are invalid.
+ return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ let transition = parse_one_transition(context, i, first)?;
+ first = false;
+ has_transition_property_none = transition.transition_property.is_none();
+ Ok(transition)
+ })?;
+ for result in results {
+ % for prop in "property duration timing_function delay".split():
+ ${prop}s.push(result.transition_${prop});
+ % endfor
+ }
+
+ Ok(expanded! {
+ % for prop in "property duration timing_function delay".split():
+ transition_${prop}: transition_${prop}::SpecifiedValue(${prop}s.into()),
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ use crate::Zero;
+ use style_traits::values::SequenceWriter;
+
+ let property_len = self.transition_property.0.len();
+
+ // There are two cases that we can do shorthand serialization:
+ // * when all value lists have the same length, or
+ // * when transition-property is none, and other value lists have exactly one item.
+ if property_len == 0 {
+ % for name in "duration delay timing_function".split():
+ if self.transition_${name}.0.len() != 1 {
+ return Ok(());
+ }
+ % endfor
+ } else {
+ % for name in "duration delay timing_function".split():
+ if self.transition_${name}.0.len() != property_len {
+ return Ok(());
+ }
+ % endfor
+ }
+
+ // Representative length.
+ let len = self.transition_duration.0.len();
+
+ for i in 0..len {
+ if i != 0 {
+ dest.write_str(", ")?;
+ }
+
+ let has_duration = !self.transition_duration.0[i].is_zero();
+ let has_timing = !self.transition_timing_function.0[i].is_ease();
+ let has_delay = !self.transition_delay.0[i].is_zero();
+ let has_any = has_duration || has_timing || has_delay;
+
+ let mut writer = SequenceWriter::new(dest, " ");
+
+ if property_len == 0 {
+ writer.raw_item("none")?;
+ } else if !self.transition_property.0[i].is_all() || !has_any {
+ writer.item(&self.transition_property.0[i])?;
+ }
+
+ // In order to avoid ambiguity, we have to serialize duration if we have delay.
+ if has_duration || has_delay {
+ writer.item(&self.transition_duration.0[i])?;
+ }
+
+ if has_timing {
+ writer.item(&self.transition_timing_function.0[i])?;
+ }
+
+ if has_delay {
+ writer.item(&self.transition_delay.0[i])?;
+ }
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand name="animation"
+ engines="gecko servo-2013 servo-2020"
+ extra_prefixes="moz:layout.css.prefixes.animations webkit"
+ sub_properties="animation-name animation-duration
+ animation-timing-function animation-delay
+ animation-iteration-count animation-direction
+ animation-fill-mode animation-play-state animation-timeline"
+ rule_types_allowed="Style"
+ spec="https://drafts.csswg.org/css-animations/#propdef-animation">
+ <%
+ props = "name timeline duration timing_function delay iteration_count \
+ direction fill_mode play_state".split()
+ %>
+ % for prop in props:
+ use crate::properties::longhands::animation_${prop};
+ % endfor
+
+ pub fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ struct SingleAnimation {
+ % for prop in props:
+ animation_${prop}: animation_${prop}::SingleSpecifiedValue,
+ % endfor
+ }
+
+ fn parse_one_animation<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<SingleAnimation, ParseError<'i>> {
+ % for prop in props:
+ let mut ${prop} = None;
+ % endfor
+
+ let mut parsed = 0;
+ // NB: Name must be the last one here so that keywords valid for other
+ // longhands are not interpreted as names.
+ //
+ // Also, duration must be before delay, see
+ // https://drafts.csswg.org/css-animations/#typedef-single-animation
+ loop {
+ parsed += 1;
+ try_parse_one!(context, input, duration, animation_duration);
+ try_parse_one!(context, input, timing_function, animation_timing_function);
+ try_parse_one!(context, input, delay, animation_delay);
+ try_parse_one!(context, input, iteration_count, animation_iteration_count);
+ try_parse_one!(context, input, direction, animation_direction);
+ try_parse_one!(context, input, fill_mode, animation_fill_mode);
+ try_parse_one!(context, input, play_state, animation_play_state);
+ try_parse_one!(context, input, name, animation_name);
+ if static_prefs::pref!("layout.css.scroll-driven-animations.enabled") {
+ try_parse_one!(context, input, timeline, animation_timeline);
+ }
+
+ parsed -= 1;
+ break
+ }
+
+ // If nothing is parsed, this is an invalid entry.
+ if parsed == 0 {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(SingleAnimation {
+ % for prop in props:
+ animation_${prop}: ${prop}.unwrap_or_else(animation_${prop}::single_value
+ ::get_initial_specified_value),
+ % endfor
+ })
+ }
+ }
+
+ % for prop in props:
+ let mut ${prop}s = vec![];
+ % endfor
+
+ let results = input.parse_comma_separated(|i| parse_one_animation(context, i))?;
+ for result in results.into_iter() {
+ % for prop in props:
+ ${prop}s.push(result.animation_${prop});
+ % endfor
+ }
+
+ Ok(expanded! {
+ % for prop in props:
+ animation_${prop}: animation_${prop}::SpecifiedValue(${prop}s.into()),
+ % endfor
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ let len = self.animation_name.0.len();
+ // There should be at least one declared value
+ if len == 0 {
+ return Ok(());
+ }
+
+ // If any value list length is differs then we don't do a shorthand serialization
+ // either.
+ % for name in props[2:]:
+ if len != self.animation_${name}.0.len() {
+ return Ok(())
+ }
+ % endfor
+
+ // If the preference of animation-timeline is disabled, `self.animation_timeline` is
+ // None.
+ if self.animation_timeline.map_or(false, |v| len != v.0.len()) {
+ return Ok(());
+ }
+
+ for i in 0..len {
+ if i != 0 {
+ dest.write_str(", ")?;
+ }
+
+ % for name in props[2:]:
+ self.animation_${name}.0[i].to_css(dest)?;
+ dest.write_char(' ')?;
+ % endfor
+
+ self.animation_name.0[i].to_css(dest)?;
+
+ // Based on the spec, the default values of other properties must be output in at
+ // least the cases necessary to distinguish an animation-name. The serialization
+ // order of animation-timeline is always later than animation-name, so it's fine
+ // to not serialize it if it is the default value. It's still possible to
+ // distinguish them (because we always serialize animation-name).
+ // https://drafts.csswg.org/css-animations-1/#animation
+ // https://drafts.csswg.org/css-animations-2/#typedef-single-animation
+ //
+ // Note: it's also fine to always serialize this. However, it seems Blink
+ // doesn't serialize default animation-timeline now, so we follow the same rule.
+ if let Some(ref timeline) = self.animation_timeline {
+ if !timeline.0[i].is_auto() {
+ dest.write_char(' ')?;
+ timeline.0[i].to_css(dest)?;
+ }
+ }
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+<%helpers:shorthand
+ engines="gecko"
+ name="scroll-timeline"
+ sub_properties="scroll-timeline-name scroll-timeline-axis"
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand"
+>
+ pub fn parse_value<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::properties::longhands::{scroll_timeline_axis, scroll_timeline_name};
+
+ let mut names = Vec::with_capacity(1);
+ let mut axes = Vec::with_capacity(1);
+ input.parse_comma_separated(|input| {
+ let name = scroll_timeline_name::single_value::parse(context, input)?;
+ let axis = input.try_parse(|i| scroll_timeline_axis::single_value::parse(context, i));
+
+ names.push(name);
+ axes.push(axis.unwrap_or_default());
+
+ Ok(())
+ })?;
+
+ Ok(expanded! {
+ scroll_timeline_name: scroll_timeline_name::SpecifiedValue(names.into()),
+ scroll_timeline_axis: scroll_timeline_axis::SpecifiedValue(axes.into()),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ // If any value list length is differs then we don't do a shorthand serialization
+ // either.
+ let len = self.scroll_timeline_name.0.len();
+ if len != self.scroll_timeline_axis.0.len() {
+ return Ok(());
+ }
+
+ for i in 0..len {
+ if i != 0 {
+ dest.write_str(", ")?;
+ }
+
+ self.scroll_timeline_name.0[i].to_css(dest)?;
+
+ if self.scroll_timeline_axis.0[i] != Default::default() {
+ dest.write_char(' ')?;
+ self.scroll_timeline_axis.0[i].to_css(dest)?;
+ }
+
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
+
+// Note: view-timeline shorthand doesn't take view-timeline-inset into account.
+<%helpers:shorthand
+ engines="gecko"
+ name="view-timeline"
+ sub_properties="view-timeline-name view-timeline-axis"
+ gecko_pref="layout.css.scroll-driven-animations.enabled",
+ spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand"
+>
+ pub fn parse_value<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Longhands, ParseError<'i>> {
+ use crate::properties::longhands::{view_timeline_axis, view_timeline_name};
+
+ let mut names = Vec::with_capacity(1);
+ let mut axes = Vec::with_capacity(1);
+ input.parse_comma_separated(|input| {
+ let name = view_timeline_name::single_value::parse(context, input)?;
+ let axis = input.try_parse(|i| view_timeline_axis::single_value::parse(context, i));
+
+ names.push(name);
+ axes.push(axis.unwrap_or_default());
+
+ Ok(())
+ })?;
+
+ Ok(expanded! {
+ view_timeline_name: view_timeline_name::SpecifiedValue(names.into()),
+ view_timeline_axis: view_timeline_axis::SpecifiedValue(axes.into()),
+ })
+ }
+
+ impl<'a> ToCss for LonghandsToSerialize<'a> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write {
+ // If any value list length is differs then we don't do a shorthand serialization
+ // either.
+ let len = self.view_timeline_name.0.len();
+ if len != self.view_timeline_axis.0.len() {
+ return Ok(());
+ }
+
+ for i in 0..len {
+ if i != 0 {
+ dest.write_str(", ")?;
+ }
+
+ self.view_timeline_name.0[i].to_css(dest)?;
+
+ if self.view_timeline_axis.0[i] != Default::default() {
+ dest.write_char(' ')?;
+ self.view_timeline_axis.0[i].to_css(dest)?;
+ }
+
+ }
+ Ok(())
+ }
+ }
+</%helpers:shorthand>
diff --git a/servo/components/style/properties_and_values/mod.rs b/servo/components/style/properties_and_values/mod.rs
new file mode 100644
index 0000000000..5b5e219d59
--- /dev/null
+++ b/servo/components/style/properties_and_values/mod.rs
@@ -0,0 +1,12 @@
+/* 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/. */
+
+//! Properties and Values
+//!
+//! https://drafts.css-houdini.org/css-properties-values-api-1/
+
+pub mod registry;
+pub mod rule;
+pub mod syntax;
+pub mod value;
diff --git a/servo/components/style/properties_and_values/registry.rs b/servo/components/style/properties_and_values/registry.rs
new file mode 100644
index 0000000000..e3cd552c9c
--- /dev/null
+++ b/servo/components/style/properties_and_values/registry.rs
@@ -0,0 +1,104 @@
+/* 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/. */
+
+//! Registered custom properties.
+
+use super::rule::{Inherits, InitialValue, PropertyRuleName};
+use super::syntax::Descriptor;
+use crate::selector_map::PrecomputedHashMap;
+use crate::stylesheets::UrlExtraData;
+use crate::Atom;
+use cssparser::SourceLocation;
+
+/// The metadata of a custom property registration that we need to do the cascade properly.
+#[derive(Debug, Clone, MallocSizeOf)]
+pub struct PropertyRegistrationData {
+ /// The syntax of the property.
+ pub syntax: Descriptor,
+ /// Whether the property inherits.
+ pub inherits: Inherits,
+ /// The initial value. Only missing for universal syntax.
+ #[ignore_malloc_size_of = "Arc"]
+ pub initial_value: Option<InitialValue>,
+}
+
+static UNREGISTERED: PropertyRegistrationData = PropertyRegistrationData {
+ syntax: Descriptor::universal(),
+ inherits: Inherits::True,
+ initial_value: None,
+};
+
+impl PropertyRegistrationData {
+ /// The data for an unregistered property.
+ pub fn unregistered() -> &'static Self {
+ &UNREGISTERED
+ }
+
+ /// Returns whether this property inherits.
+ #[inline]
+ pub fn inherits(&self) -> bool {
+ self.inherits == Inherits::True
+ }
+}
+
+/// A computed, already-validated property registration.
+/// <https://drafts.css-houdini.org/css-properties-values-api-1/#custom-property-registration>
+#[derive(Debug, Clone, MallocSizeOf)]
+pub struct PropertyRegistration {
+ /// The custom property name.
+ pub name: PropertyRuleName,
+ /// The actual information about the property.
+ pub data: PropertyRegistrationData,
+ /// The url data that is used to parse and compute the registration's initial value. Note that
+ /// it's not the url data that should be used to parse other values. Other values should use
+ /// the data of the style sheet where they came from.
+ pub url_data: UrlExtraData,
+ /// The source location of this registration, if it comes from a CSS rule.
+ pub source_location: SourceLocation,
+}
+
+impl PropertyRegistration {
+ /// Returns whether this property inherits.
+ #[inline]
+ pub fn inherits(&self) -> bool {
+ self.data.inherits == Inherits::True
+ }
+}
+
+/// The script registry of custom properties.
+/// <https://drafts.css-houdini.org/css-properties-values-api-1/#dom-window-registeredpropertyset-slot>
+#[derive(Default)]
+pub struct ScriptRegistry {
+ properties: PrecomputedHashMap<Atom, PropertyRegistration>,
+}
+
+impl ScriptRegistry {
+ /// Gets an already-registered custom property via script.
+ #[inline]
+ pub fn get(&self, name: &Atom) -> Option<&PropertyRegistration> {
+ self.properties.get(name)
+ }
+
+ /// Gets already-registered custom properties via script.
+ #[inline]
+ pub fn properties(&self) -> &PrecomputedHashMap<Atom, PropertyRegistration> {
+ &self.properties
+ }
+
+ /// Register a given property. As per
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#the-registerproperty-function>
+ /// we don't allow overriding the registration.
+ #[inline]
+ pub fn register(&mut self, registration: PropertyRegistration) {
+ let name = registration.name.0.clone();
+ let old = self.properties.insert(name, registration);
+ debug_assert!(old.is_none(), "Already registered? Should be an error");
+ }
+
+ /// Returns the properties hashmap.
+ #[inline]
+ pub fn get_all(&self) -> &PrecomputedHashMap<Atom, PropertyRegistration> {
+ &self.properties
+ }
+}
diff --git a/servo/components/style/properties_and_values/rule.rs b/servo/components/style/properties_and_values/rule.rs
new file mode 100644
index 0000000000..96617eccce
--- /dev/null
+++ b/servo/components/style/properties_and_values/rule.rs
@@ -0,0 +1,348 @@
+/* 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/. */
+
+//! The [`@property`] at-rule.
+//!
+//! https://drafts.css-houdini.org/css-properties-values-api-1/#at-property-rule
+
+use super::{
+ registry::{PropertyRegistration, PropertyRegistrationData},
+ syntax::Descriptor,
+ value::{AllowComputationallyDependent, SpecifiedValue as SpecifiedRegisteredValue},
+};
+use crate::custom_properties::{Name as CustomPropertyName, SpecifiedValue};
+use crate::error_reporting::ContextualParseError;
+use crate::parser::{Parse, ParserContext};
+use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::values::{computed, serialize_atom_name};
+use cssparser::{
+ AtRuleParser, BasicParseErrorKind, CowRcStr, DeclarationParser, ParseErrorKind, Parser,
+ ParserInput, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation,
+};
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use selectors::parser::SelectorParseErrorKind;
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+use to_shmem::{SharedMemoryBuilder, ToShmem};
+
+/// Parse the block inside a `@property` rule.
+///
+/// Valid `@property` rules result in a registered custom property, as if `registerProperty()` had
+/// been called with equivalent parameters.
+pub fn parse_property_block<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ name: PropertyRuleName,
+ source_location: SourceLocation,
+) -> Result<PropertyRegistration, ParseError<'i>> {
+ let mut descriptors = PropertyDescriptors::default();
+ let mut parser = PropertyRuleParser {
+ context,
+ descriptors: &mut descriptors,
+ };
+ let mut iter = RuleBodyParser::new(input, &mut parser);
+ while let Some(declaration) = iter.next() {
+ if !context.error_reporting_enabled() {
+ continue;
+ }
+ if let Err((error, slice)) = declaration {
+ let location = error.location;
+ let error = if matches!(
+ error.kind,
+ ParseErrorKind::Custom(StyleParseErrorKind::PropertySyntaxField(_))
+ ) {
+ // If the provided string is not a valid syntax string (if it
+ // returns failure when consume a syntax definition is called on
+ // it), the descriptor is invalid and must be ignored.
+ ContextualParseError::UnsupportedValue(slice, error)
+ } else {
+ // Unknown descriptors are invalid and ignored, but do not
+ // invalidate the @property rule.
+ ContextualParseError::UnsupportedPropertyDescriptor(slice, error)
+ };
+ context.log_css_error(location, error);
+ }
+ }
+
+ // https://drafts.css-houdini.org/css-properties-values-api-1/#the-syntax-descriptor:
+ //
+ // The syntax descriptor is required for the @property rule to be valid; if it’s
+ // missing, the @property rule is invalid.
+ let Some(syntax) = descriptors.syntax else {
+ return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid));
+ };
+
+ // https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor:
+ //
+ // The inherits descriptor is required for the @property rule to be valid; if it’s
+ // missing, the @property rule is invalid.
+ let Some(inherits) = descriptors.inherits else {
+ return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid));
+ };
+
+ if PropertyRegistration::validate_initial_value(&syntax, descriptors.initial_value.as_deref())
+ .is_err()
+ {
+ return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid));
+ }
+
+ Ok(PropertyRegistration {
+ name,
+ data: PropertyRegistrationData {
+ syntax,
+ inherits,
+ initial_value: descriptors.initial_value,
+ },
+ url_data: context.url_data.clone(),
+ source_location,
+ })
+}
+
+struct PropertyRuleParser<'a, 'b: 'a> {
+ context: &'a ParserContext<'b>,
+ descriptors: &'a mut PropertyDescriptors,
+}
+
+/// Default methods reject all at rules.
+impl<'a, 'b, 'i> AtRuleParser<'i> for PropertyRuleParser<'a, 'b> {
+ type Prelude = ();
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> QualifiedRuleParser<'i> for PropertyRuleParser<'a, 'b> {
+ type Prelude = ();
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>>
+ for PropertyRuleParser<'a, 'b>
+{
+ fn parse_qualified(&self) -> bool {
+ false
+ }
+ fn parse_declarations(&self) -> bool {
+ true
+ }
+}
+
+macro_rules! property_descriptors {
+ (
+ $( #[$doc: meta] $name: tt $ident: ident: $ty: ty, )*
+ ) => {
+ /// Data inside a `@property` rule.
+ ///
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#at-property-rule>
+ #[derive(Clone, Debug, Default, PartialEq)]
+ struct PropertyDescriptors {
+ $(
+ #[$doc]
+ $ident: Option<$ty>,
+ )*
+ }
+
+ impl PropertyRegistration {
+ fn decl_to_css(&self, dest: &mut CssStringWriter) -> fmt::Result {
+ $(
+ let $ident = Option::<&$ty>::from(&self.data.$ident);
+ if let Some(ref value) = $ident {
+ dest.write_str(concat!($name, ": "))?;
+ value.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str("; ")?;
+ }
+ )*
+ Ok(())
+ }
+ }
+
+ impl<'a, 'b, 'i> DeclarationParser<'i> for PropertyRuleParser<'a, 'b> {
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_value<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ match_ignore_ascii_case! { &*name,
+ $(
+ $name => {
+ // DeclarationParser also calls parse_entirely so we’d normally not need
+ // to, but in this case we do because we set the value as a side effect
+ // rather than returning it.
+ let value = input.parse_entirely(|i| Parse::parse(self.context, i))?;
+ self.descriptors.$ident = Some(value)
+ },
+ )*
+ _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+property_descriptors! {
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#the-syntax-descriptor>
+ "syntax" syntax: Descriptor,
+
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor>
+ "inherits" inherits: Inherits,
+
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#initial-value-descriptor>
+ "initial-value" initial_value: InitialValue,
+}
+
+/// Errors that can happen when registering a property.
+#[allow(missing_docs)]
+pub enum PropertyRegistrationError {
+ NoInitialValue,
+ InvalidInitialValue,
+ InitialValueNotComputationallyIndependent,
+}
+
+impl PropertyRegistration {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, _: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ MallocSizeOf::size_of(self, ops)
+ }
+
+ /// Computes the value of the computationally independent initial value.
+ pub fn compute_initial_value(
+ &self,
+ computed_context: &computed::Context,
+ ) -> Result<InitialValue, ()> {
+ let Some(ref initial) = self.data.initial_value else {
+ return Err(());
+ };
+
+ if self.data.syntax.is_universal() {
+ return Ok(Arc::clone(initial));
+ }
+
+ let mut input = ParserInput::new(initial.css_text());
+ let mut input = Parser::new(&mut input);
+ input.skip_whitespace();
+
+ match SpecifiedRegisteredValue::compute(
+ &mut input,
+ &self.data,
+ &self.url_data,
+ computed_context,
+ AllowComputationallyDependent::No,
+ ) {
+ Ok(computed) => Ok(Arc::new(computed)),
+ Err(_) => Err(()),
+ }
+ }
+
+ /// Performs syntax validation as per the initial value descriptor.
+ /// https://drafts.css-houdini.org/css-properties-values-api-1/#initial-value-descriptor
+ pub fn validate_initial_value(
+ syntax: &Descriptor,
+ initial_value: Option<&SpecifiedValue>,
+ ) -> Result<(), PropertyRegistrationError> {
+ use crate::properties::CSSWideKeyword;
+ // If the value of the syntax descriptor is the universal syntax definition, then the
+ // initial-value descriptor is optional. If omitted, the initial value of the property is
+ // the guaranteed-invalid value.
+ if syntax.is_universal() && initial_value.is_none() {
+ return Ok(());
+ }
+
+ // Otherwise, if the value of the syntax descriptor is not the universal syntax definition,
+ // the following conditions must be met for the @property rule to be valid:
+
+ // The initial-value descriptor must be present.
+ let Some(initial) = initial_value else {
+ return Err(PropertyRegistrationError::NoInitialValue);
+ };
+
+ // A value that references the environment or other variables is not computationally
+ // independent.
+ if initial.has_references() {
+ return Err(PropertyRegistrationError::InitialValueNotComputationallyIndependent);
+ }
+
+ let mut input = ParserInput::new(initial.css_text());
+ let mut input = Parser::new(&mut input);
+ input.skip_whitespace();
+
+ // The initial-value cannot include CSS-wide keywords.
+ if input.try_parse(CSSWideKeyword::parse).is_ok() {
+ return Err(PropertyRegistrationError::InitialValueNotComputationallyIndependent);
+ }
+
+ match SpecifiedRegisteredValue::parse(
+ &mut input,
+ syntax,
+ &initial.url_data,
+ AllowComputationallyDependent::No,
+ ) {
+ Ok(_) => {},
+ Err(_) => return Err(PropertyRegistrationError::InvalidInitialValue),
+ }
+
+ Ok(())
+ }
+}
+
+impl ToCssWithGuard for PropertyRegistration {
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#serialize-a-csspropertyrule>
+ fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@property ")?;
+ self.name.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str(" { ")?;
+ self.decl_to_css(dest)?;
+ dest.write_char('}')
+ }
+}
+
+impl ToShmem for PropertyRegistration {
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ Err(String::from(
+ "ToShmem failed for PropertyRule: cannot handle @property rules",
+ ))
+ }
+}
+
+/// A custom property name wrapper that includes the `--` prefix in its serialization
+#[derive(Clone, Debug, PartialEq, MallocSizeOf)]
+pub struct PropertyRuleName(pub CustomPropertyName);
+
+impl ToCss for PropertyRuleName {
+ fn to_css<W: Write>(&self, dest: &mut CssWriter<W>) -> fmt::Result {
+ dest.write_str("--")?;
+ serialize_atom_name(&self.0, dest)
+ }
+}
+
+/// <https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor>
+#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, ToCss)]
+pub enum Inherits {
+ /// `true` value for the `inherits` descriptor
+ True,
+ /// `false` value for the `inherits` descriptor
+ False,
+}
+
+/// Specifies the initial value of the custom property registration represented by the @property
+/// rule, controlling the property’s initial value.
+///
+/// The SpecifiedValue is wrapped in an Arc to avoid copying when using it.
+pub type InitialValue = Arc<SpecifiedValue>;
+
+impl Parse for InitialValue {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.skip_whitespace();
+ Ok(Arc::new(SpecifiedValue::parse(input, &context.url_data)?))
+ }
+}
diff --git a/servo/components/style/properties_and_values/syntax/ascii.rs b/servo/components/style/properties_and_values/syntax/ascii.rs
new file mode 100644
index 0000000000..e1a1b08535
--- /dev/null
+++ b/servo/components/style/properties_and_values/syntax/ascii.rs
@@ -0,0 +1,60 @@
+/* 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/. */
+
+/// Trims ASCII whitespace characters from a slice, and returns the trimmed input.
+pub fn trim_ascii_whitespace(input: &str) -> &str {
+ if input.is_empty() {
+ return input;
+ }
+
+ let mut start = 0;
+ {
+ let mut iter = input.as_bytes().iter();
+ loop {
+ let byte = match iter.next() {
+ Some(b) => b,
+ None => return "",
+ };
+
+ if !byte.is_ascii_whitespace() {
+ break;
+ }
+ start += 1;
+ }
+ }
+
+ let mut end = input.len();
+ assert!(start < end);
+ {
+ let mut iter = input.as_bytes()[start..].iter().rev();
+ loop {
+ let byte = match iter.next() {
+ Some(b) => b,
+ None => {
+ debug_assert!(false, "We should have caught this in the loop above!");
+ return "";
+ },
+ };
+
+ if !byte.is_ascii_whitespace() {
+ break;
+ }
+ end -= 1;
+ }
+ }
+
+ &input[start..end]
+}
+
+#[test]
+fn trim_ascii_whitespace_test() {
+ fn test(i: &str, o: &str) {
+ assert_eq!(trim_ascii_whitespace(i), o)
+ }
+
+ test("", "");
+ test(" ", "");
+ test(" a b c ", "a b c");
+ test(" \t \t \ta b c \t \t \t \t", "a b c");
+}
diff --git a/servo/components/style/properties_and_values/syntax/data_type.rs b/servo/components/style/properties_and_values/syntax/data_type.rs
new file mode 100644
index 0000000000..be331e2222
--- /dev/null
+++ b/servo/components/style/properties_and_values/syntax/data_type.rs
@@ -0,0 +1,134 @@
+/* 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/. */
+
+//! Used for parsing and serializing component names from the syntax string.
+
+use super::{Component, ComponentName, Multiplier};
+use std::fmt::{self, Debug, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// <https://drafts.css-houdini.org/css-properties-values-api-1/#supported-names>
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)]
+pub enum DataType {
+ /// Any valid `<length>` value
+ Length,
+ /// `<number>` values
+ Number,
+ /// Any valid <percentage> value
+ Percentage,
+ /// Any valid `<length>` or `<percentage>` value, any valid `<calc()>` expression combining
+ /// `<length>` and `<percentage>` components.
+ LengthPercentage,
+ /// Any valid `<color>` value
+ Color,
+ /// Any valid `<image>` value
+ Image,
+ /// Any valid `<url>` value
+ Url,
+ /// Any valid `<integer>` value
+ Integer,
+ /// Any valid `<angle>` value
+ Angle,
+ /// Any valid `<time>` value
+ Time,
+ /// Any valid `<resolution>` value
+ Resolution,
+ /// Any valid `<transform-function>` value
+ TransformFunction,
+ /// Any valid `<custom-ident>` value
+ CustomIdent,
+ /// A list of valid `<transform-function>` values. Note that "<transform-list>" is a pre-multiplied
+ /// data type name equivalent to "<transform-function>+"
+ TransformList,
+ /// Any valid `<string>` value
+ ///
+ /// <https://github.com/w3c/css-houdini-drafts/issues/1103>
+ String,
+}
+
+impl DataType {
+ /// Converts a component name from a pre-multiplied data type to its un-pre-multiplied equivalent.
+ ///
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#pre-multiplied-data-type-name>
+ pub fn unpremultiply(&self) -> Option<Component> {
+ match *self {
+ DataType::TransformList => Some(Component {
+ name: ComponentName::DataType(DataType::TransformFunction),
+ multiplier: Some(Multiplier::Space),
+ }),
+ _ => None,
+ }
+ }
+
+ /// Parses a syntax component name.
+ pub fn from_str(ty: &str) -> Option<Self> {
+ Some(match ty.as_bytes() {
+ b"length" => DataType::Length,
+ b"number" => DataType::Number,
+ b"percentage" => DataType::Percentage,
+ b"length-percentage" => DataType::LengthPercentage,
+ b"color" => DataType::Color,
+ b"image" => DataType::Image,
+ b"url" => DataType::Url,
+ b"integer" => DataType::Integer,
+ b"angle" => DataType::Angle,
+ b"time" => DataType::Time,
+ b"resolution" => DataType::Resolution,
+ b"transform-function" => DataType::TransformFunction,
+ b"custom-ident" => DataType::CustomIdent,
+ b"transform-list" => DataType::TransformList,
+ b"string" => DataType::String,
+ _ => return None,
+ })
+ }
+
+ /// Returns true if this data type requires deferring computation to properly
+ /// resolve font-dependent lengths.
+ pub fn may_reference_font_relative_length(&self) -> bool {
+ match self {
+ DataType::Length |
+ DataType::LengthPercentage |
+ DataType::TransformFunction |
+ DataType::TransformList => true,
+ DataType::Number |
+ DataType::Percentage |
+ DataType::Color |
+ DataType::Image |
+ DataType::Url |
+ DataType::Integer |
+ DataType::Angle |
+ DataType::Time |
+ DataType::Resolution |
+ DataType::CustomIdent |
+ DataType::String => false,
+ }
+ }
+}
+
+impl ToCss for DataType {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_char('<')?;
+ dest.write_str(match *self {
+ DataType::Length => "length",
+ DataType::Number => "number",
+ DataType::Percentage => "percentage",
+ DataType::LengthPercentage => "length-percentage",
+ DataType::Color => "color",
+ DataType::Image => "image",
+ DataType::Url => "url",
+ DataType::Integer => "integer",
+ DataType::Angle => "angle",
+ DataType::Time => "time",
+ DataType::Resolution => "resolution",
+ DataType::TransformFunction => "transform-function",
+ DataType::CustomIdent => "custom-ident",
+ DataType::TransformList => "transform-list",
+ DataType::String => "string",
+ })?;
+ dest.write_char('>')
+ }
+}
diff --git a/servo/components/style/properties_and_values/syntax/mod.rs b/servo/components/style/properties_and_values/syntax/mod.rs
new file mode 100644
index 0000000000..404c8caa7b
--- /dev/null
+++ b/servo/components/style/properties_and_values/syntax/mod.rs
@@ -0,0 +1,392 @@
+/* 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/. */
+
+//! Used for parsing and serializing the [`@property`] syntax string.
+//!
+//! <https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-syntax>
+
+use std::fmt::{self, Debug};
+use std::{borrow::Cow, fmt::Write};
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::CustomIdent;
+use cssparser::{Parser as CSSParser, ParserInput as CSSParserInput};
+use style_traits::{
+ CssWriter, ParseError as StyleParseError, PropertySyntaxParseError as ParseError,
+ StyleParseErrorKind, ToCss,
+};
+
+use self::data_type::DataType;
+
+mod ascii;
+pub mod data_type;
+
+/// <https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-syntax>
+#[derive(Debug, Clone, Default, MallocSizeOf, PartialEq)]
+pub struct Descriptor {
+ /// The parsed components, if any.
+ /// TODO: Could be a Box<[]> if that supported const construction.
+ pub components: Vec<Component>,
+ /// The specified css syntax, if any.
+ specified: Option<Box<str>>,
+}
+
+impl Descriptor {
+ /// Returns the universal descriptor.
+ pub const fn universal() -> Self {
+ Self {
+ components: Vec::new(),
+ specified: None,
+ }
+ }
+
+ /// Returns whether this is the universal syntax descriptor.
+ #[inline]
+ pub fn is_universal(&self) -> bool {
+ self.components.is_empty()
+ }
+
+ /// Returns the specified string, if any.
+ #[inline]
+ pub fn specified_string(&self) -> Option<&str> {
+ self.specified.as_deref()
+ }
+
+ /// Parse a syntax descriptor.
+ /// https://drafts.css-houdini.org/css-properties-values-api-1/#consume-a-syntax-definition
+ pub fn from_str(css: &str, save_specified: bool) -> Result<Self, ParseError> {
+ // 1. Strip leading and trailing ASCII whitespace from string.
+ let input = ascii::trim_ascii_whitespace(css);
+
+ // 2. If string's length is 0, return failure.
+ if input.is_empty() {
+ return Err(ParseError::EmptyInput);
+ }
+
+ let specified = if save_specified {
+ Some(Box::from(css))
+ } else {
+ None
+ };
+
+ // 3. If string's length is 1, and the only code point in string is U+002A
+ // ASTERISK (*), return the universal syntax descriptor.
+ if input.len() == 1 && input.as_bytes()[0] == b'*' {
+ return Ok(Self {
+ components: Default::default(),
+ specified,
+ });
+ }
+
+ // 4. Let stream be an input stream created from the code points of string,
+ // preprocessed as specified in [css-syntax-3]. Let descriptor be an
+ // initially empty list of syntax components.
+ //
+ // NOTE(emilio): Instead of preprocessing we cheat and treat new-lines and
+ // nulls in the parser specially.
+ let mut components = vec![];
+ {
+ let mut parser = Parser::new(input, &mut components);
+ // 5. Repeatedly consume the next input code point from stream.
+ parser.parse()?;
+ }
+ Ok(Self { components, specified })
+ }
+
+ /// Returns true if the syntax permits the value to be computed as a length.
+ pub fn may_compute_length(&self) -> bool {
+ for component in self.components.iter() {
+ match &component.name {
+ ComponentName::DataType(ref t) => {
+ if matches!(t, DataType::Length | DataType::LengthPercentage) {
+ return true;
+ }
+ },
+ ComponentName::Ident(_) => (),
+ };
+ }
+ false
+ }
+
+ /// Returns true if the syntax requires deferring computation to properly
+ /// resolve font-dependent lengths.
+ pub fn may_reference_font_relative_length(&self) -> bool {
+ for component in self.components.iter() {
+ match &component.name {
+ ComponentName::DataType(ref t) => {
+ if t.may_reference_font_relative_length() {
+ return true;
+ }
+ },
+ ComponentName::Ident(_) => (),
+ };
+ }
+ false
+ }
+}
+
+impl ToCss for Descriptor {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if let Some(ref specified) = self.specified {
+ return specified.to_css(dest);
+ }
+
+ if self.is_universal() {
+ return dest.write_char('*');
+ }
+
+ let mut first = true;
+ for component in &*self.components {
+ if !first {
+ dest.write_str(" | ")?;
+ }
+ component.to_css(dest)?;
+ first = false;
+ }
+
+ Ok(())
+ }
+}
+
+impl Parse for Descriptor {
+ /// Parse a syntax descriptor.
+ fn parse<'i>(
+ _: &ParserContext,
+ parser: &mut CSSParser<'i, '_>,
+ ) -> Result<Self, StyleParseError<'i>> {
+ let input = parser.expect_string()?;
+ Descriptor::from_str(input.as_ref(), /* save_specified = */ true)
+ .map_err(|err| parser.new_custom_error(StyleParseErrorKind::PropertySyntaxField(err)))
+ }
+}
+
+/// <https://drafts.css-houdini.org/css-properties-values-api-1/#multipliers>
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue)]
+pub enum Multiplier {
+ /// Indicates a space-separated list.
+ Space,
+ /// Indicates a comma-separated list.
+ Comma,
+}
+
+impl ToCss for Multiplier {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_char(match *self {
+ Multiplier::Space => '+',
+ Multiplier::Comma => '#',
+ })
+ }
+}
+
+/// <https://drafts.css-houdini.org/css-properties-values-api-1/#syntax-component>
+#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
+pub struct Component {
+ name: ComponentName,
+ multiplier: Option<Multiplier>,
+}
+
+impl Component {
+ /// Returns the component's name.
+ #[inline]
+ pub fn name(&self) -> &ComponentName {
+ &self.name
+ }
+
+ /// Returns the component's multiplier, if one exists.
+ #[inline]
+ pub fn multiplier(&self) -> Option<Multiplier> {
+ self.multiplier
+ }
+
+ /// If the component is premultiplied, return the un-premultiplied component.
+ #[inline]
+ pub fn unpremultiplied(&self) -> Cow<Self> {
+ match self.name.unpremultiply() {
+ Some(component) => {
+ debug_assert!(
+ self.multiplier.is_none(),
+ "Shouldn't have parsed a multiplier for a pre-multiplied data type name",
+ );
+ Cow::Owned(component)
+ },
+ None => Cow::Borrowed(self),
+ }
+ }
+}
+
+impl ToCss for Component {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.name().to_css(dest)?;
+ self.multiplier().to_css(dest)
+ }
+}
+
+/// <https://drafts.css-houdini.org/css-properties-values-api-1/#syntax-component-name>
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss)]
+pub enum ComponentName {
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#data-type-name>
+ DataType(DataType),
+ /// <https://drafts.csswg.org/css-values-4/#custom-idents>
+ Ident(CustomIdent),
+}
+
+impl ComponentName {
+ fn unpremultiply(&self) -> Option<Component> {
+ match *self {
+ ComponentName::DataType(ref t) => t.unpremultiply(),
+ ComponentName::Ident(..) => None,
+ }
+ }
+
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#pre-multiplied-data-type-name>
+ fn is_pre_multiplied(&self) -> bool {
+ self.unpremultiply().is_some()
+ }
+}
+
+struct Parser<'a> {
+ input: &'a str,
+ position: usize,
+ output: &'a mut Vec<Component>,
+}
+
+/// <https://drafts.csswg.org/css-syntax-3/#letter>
+fn is_letter(byte: u8) -> bool {
+ match byte {
+ b'A'..=b'Z' | b'a'..=b'z' => true,
+ _ => false,
+ }
+}
+
+/// <https://drafts.csswg.org/css-syntax-3/#non-ascii-code-point>
+fn is_non_ascii(byte: u8) -> bool {
+ byte >= 0x80
+}
+
+/// <https://drafts.csswg.org/css-syntax-3/#name-start-code-point>
+fn is_name_start(byte: u8) -> bool {
+ is_letter(byte) || is_non_ascii(byte) || byte == b'_'
+}
+
+impl<'a> Parser<'a> {
+ fn new(input: &'a str, output: &'a mut Vec<Component>) -> Self {
+ Self {
+ input,
+ position: 0,
+ output,
+ }
+ }
+
+ fn peek(&self) -> Option<u8> {
+ self.input.as_bytes().get(self.position).cloned()
+ }
+
+ fn parse(&mut self) -> Result<(), ParseError> {
+ // 5. Repeatedly consume the next input code point from stream:
+ loop {
+ let component = self.parse_component()?;
+ self.output.push(component);
+ self.skip_whitespace();
+
+ let byte = match self.peek() {
+ None => return Ok(()),
+ Some(b) => b,
+ };
+
+ if byte != b'|' {
+ return Err(ParseError::ExpectedPipeBetweenComponents);
+ }
+
+ self.position += 1;
+ }
+ }
+
+ fn skip_whitespace(&mut self) {
+ loop {
+ match self.peek() {
+ Some(c) if c.is_ascii_whitespace() => self.position += 1,
+ _ => return,
+ }
+ }
+ }
+
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-data-type-name>
+ fn parse_data_type_name(&mut self) -> Result<DataType, ParseError> {
+ let start = self.position;
+ loop {
+ let byte = match self.peek() {
+ Some(b) => b,
+ None => return Err(ParseError::UnclosedDataTypeName),
+ };
+ if byte != b'>' {
+ self.position += 1;
+ continue;
+ }
+ let ty = match DataType::from_str(&self.input[start..self.position]) {
+ Some(ty) => ty,
+ None => return Err(ParseError::UnknownDataTypeName),
+ };
+ self.position += 1;
+ return Ok(ty);
+ }
+ }
+
+ fn parse_name(&mut self) -> Result<ComponentName, ParseError> {
+ let b = match self.peek() {
+ Some(b) => b,
+ None => return Err(ParseError::UnexpectedEOF),
+ };
+
+ if b == b'<' {
+ self.position += 1;
+ return Ok(ComponentName::DataType(self.parse_data_type_name()?));
+ }
+
+ if b != b'\\' && !is_name_start(b) {
+ return Err(ParseError::InvalidNameStart);
+ }
+
+ let input = &self.input[self.position..];
+ let mut input = CSSParserInput::new(input);
+ let mut input = CSSParser::new(&mut input);
+ let name = match CustomIdent::parse(&mut input, &[]) {
+ Ok(name) => name,
+ Err(_) => return Err(ParseError::InvalidName),
+ };
+ self.position += input.position().byte_index();
+ return Ok(ComponentName::Ident(name));
+ }
+
+ fn parse_multiplier(&mut self) -> Option<Multiplier> {
+ let multiplier = match self.peek()? {
+ b'+' => Multiplier::Space,
+ b'#' => Multiplier::Comma,
+ _ => return None,
+ };
+ self.position += 1;
+ Some(multiplier)
+ }
+
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-a-syntax-component>
+ fn parse_component(&mut self) -> Result<Component, ParseError> {
+ // Consume as much whitespace as possible from stream.
+ self.skip_whitespace();
+ let name = self.parse_name()?;
+ let multiplier = if name.is_pre_multiplied() {
+ None
+ } else {
+ self.parse_multiplier()
+ };
+ Ok(Component { name, multiplier })
+ }
+}
diff --git a/servo/components/style/properties_and_values/value.rs b/servo/components/style/properties_and_values/value.rs
new file mode 100644
index 0000000000..8e9d78b8cc
--- /dev/null
+++ b/servo/components/style/properties_and_values/value.rs
@@ -0,0 +1,626 @@
+/* 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/. */
+
+//! Parsing for registered custom properties.
+
+use std::fmt::{self, Write};
+
+use super::{
+ registry::PropertyRegistrationData,
+ syntax::{
+ data_type::DataType, Component as SyntaxComponent, ComponentName, Descriptor, Multiplier,
+ },
+};
+use crate::custom_properties::ComputedValue as ComputedPropertyValue;
+use crate::parser::{Parse, ParserContext};
+use crate::properties;
+use crate::stylesheets::{CssRuleType, Origin, UrlExtraData};
+use crate::values::{
+ animated::{self, Animate, Procedure},
+ computed::{self, ToComputedValue},
+ specified, CustomIdent,
+};
+use cssparser::{BasicParseErrorKind, ParseErrorKind, Parser as CSSParser, TokenSerializationType};
+use selectors::matching::QuirksMode;
+use servo_arc::Arc;
+use smallvec::SmallVec;
+use style_traits::{
+ owned_str::OwnedStr, CssWriter, ParseError as StyleParseError, ParsingMode,
+ PropertySyntaxParseError, StyleParseErrorKind, ToCss,
+};
+
+/// A single component of the computed value.
+pub type ComputedValueComponent = GenericValueComponent<
+ computed::Length,
+ computed::Number,
+ computed::Percentage,
+ computed::LengthPercentage,
+ computed::Color,
+ computed::Image,
+ computed::url::ComputedUrl,
+ computed::Integer,
+ computed::Angle,
+ computed::Time,
+ computed::Resolution,
+ computed::Transform,
+>;
+
+/// A single component of the specified value.
+pub type SpecifiedValueComponent = GenericValueComponent<
+ specified::Length,
+ specified::Number,
+ specified::Percentage,
+ specified::LengthPercentage,
+ specified::Color,
+ specified::Image,
+ specified::url::SpecifiedUrl,
+ specified::Integer,
+ specified::Angle,
+ specified::Time,
+ specified::Resolution,
+ specified::Transform,
+>;
+
+impl<L, N, P, LP, C, Image, U, Integer, A, T, R, Transform>
+ GenericValueComponent<L, N, P, LP, C, Image, U, Integer, A, T, R, Transform>
+{
+ fn serialization_types(&self) -> (TokenSerializationType, TokenSerializationType) {
+ let first_token_type = match self {
+ Self::Length(_) | Self::Angle(_) | Self::Time(_) | Self::Resolution(_) => {
+ TokenSerializationType::Dimension
+ },
+ Self::Number(_) | Self::Integer(_) => TokenSerializationType::Number,
+ Self::Percentage(_) | Self::LengthPercentage(_) => TokenSerializationType::Percentage,
+ Self::Color(_) |
+ Self::Image(_) |
+ Self::Url(_) |
+ Self::TransformFunction(_) |
+ Self::TransformList(_) => TokenSerializationType::Function,
+ Self::CustomIdent(_) => TokenSerializationType::Ident,
+ Self::String(_) => TokenSerializationType::Other,
+ };
+ let last_token_type = if first_token_type == TokenSerializationType::Function {
+ TokenSerializationType::Other
+ } else {
+ first_token_type
+ };
+ (first_token_type, last_token_type)
+ }
+}
+
+/// A generic enum used for both specified value components and computed value components.
+#[derive(Animate, Clone, ToCss, ToComputedValue, Debug, MallocSizeOf, PartialEq)]
+#[animation(no_bound(Image, Url))]
+pub enum GenericValueComponent<
+ Length,
+ Number,
+ Percentage,
+ LengthPercentage,
+ Color,
+ Image,
+ Url,
+ Integer,
+ Angle,
+ Time,
+ Resolution,
+ TransformFunction,
+> {
+ /// A <length> value
+ Length(Length),
+ /// A <number> value
+ Number(Number),
+ /// A <percentage> value
+ Percentage(Percentage),
+ /// A <length-percentage> value
+ LengthPercentage(LengthPercentage),
+ /// A <color> value
+ Color(Color),
+ /// An <image> value
+ #[animation(error)]
+ Image(Image),
+ /// A <url> value
+ #[animation(error)]
+ Url(Url),
+ /// An <integer> value
+ Integer(Integer),
+ /// An <angle> value
+ Angle(Angle),
+ /// A <time> value
+ Time(Time),
+ /// A <resolution> value
+ Resolution(Resolution),
+ /// A <transform-function> value
+ TransformFunction(TransformFunction),
+ /// A <custom-ident> value
+ #[animation(error)]
+ CustomIdent(CustomIdent),
+ /// A <transform-list> value, equivalent to <transform-function>+
+ TransformList(ComponentList<Self>),
+ /// A <string> value
+ #[animation(error)]
+ String(OwnedStr),
+}
+
+/// A list of component values, including the list's multiplier.
+#[derive(Clone, ToComputedValue, Debug, MallocSizeOf, PartialEq)]
+pub struct ComponentList<Component> {
+ /// Multiplier
+ pub multiplier: Multiplier,
+ /// The list of components contained.
+ pub components: crate::OwnedSlice<Component>,
+}
+
+impl<Component: Animate> Animate for ComponentList<Component> {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.multiplier != other.multiplier {
+ return Err(());
+ }
+ let components = animated::lists::by_computed_value::animate(&self.components, &other.components, procedure)?;
+ Ok(Self {
+ multiplier: self.multiplier,
+ components,
+ })
+ }
+}
+
+impl<Component: ToCss> ToCss for ComponentList<Component> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let mut iter = self.components.iter();
+ let Some(first) = iter.next() else {
+ return Ok(());
+ };
+ first.to_css(dest)?;
+
+ // The separator implied by the multiplier for this list.
+ let separator = match self.multiplier {
+ // <https://drafts.csswg.org/cssom-1/#serialize-a-whitespace-separated-list>
+ Multiplier::Space => " ",
+ // <https://drafts.csswg.org/cssom-1/#serialize-a-comma-separated-list>
+ Multiplier::Comma => ", ",
+ };
+ for component in iter {
+ dest.write_str(separator)?;
+ component.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// A specified registered custom property value.
+#[derive(Animate, ToComputedValue, ToCss, Clone, Debug, MallocSizeOf, PartialEq)]
+pub enum Value<Component> {
+ /// A single specified component value whose syntax descriptor component did not have a
+ /// multiplier.
+ Component(Component),
+ /// A specified value whose syntax descriptor was the universal syntax definition.
+ #[animation(error)]
+ Universal(#[ignore_malloc_size_of = "Arc"] Arc<ComputedPropertyValue>),
+ /// A list of specified component values whose syntax descriptor component had a multiplier.
+ List(#[animation(field_bound)] ComponentList<Component>),
+}
+
+/// Specified custom property value.
+pub type SpecifiedValue = Value<SpecifiedValueComponent>;
+
+/// Computed custom property value.
+pub type ComputedValue = Value<ComputedValueComponent>;
+
+impl SpecifiedValue {
+ /// Convert a Computed custom property value to a VariableValue.
+ pub fn compute<'i, 't>(
+ input: &mut CSSParser<'i, 't>,
+ registration: &PropertyRegistrationData,
+ url_data: &UrlExtraData,
+ context: &computed::Context,
+ allow_computationally_dependent: AllowComputationallyDependent,
+ ) -> Result<ComputedPropertyValue, ()> {
+ let value = Self::get_computed_value(
+ input,
+ registration,
+ url_data,
+ context,
+ allow_computationally_dependent,
+ )?;
+ Ok(value.to_variable_value(url_data))
+ }
+
+ /// Convert a registered custom property to a Computed custom property value, given input and a
+ /// property registration.
+ fn get_computed_value<'i, 't>(
+ input: &mut CSSParser<'i, 't>,
+ registration: &PropertyRegistrationData,
+ url_data: &UrlExtraData,
+ context: &computed::Context,
+ allow_computationally_dependent: AllowComputationallyDependent,
+ ) -> Result<ComputedValue, ()> {
+ debug_assert!(!registration.syntax.is_universal(), "Shouldn't be needed");
+ let Ok(value) = Self::parse(
+ input,
+ &registration.syntax,
+ url_data,
+ allow_computationally_dependent,
+ ) else {
+ return Err(());
+ };
+
+ Ok(value.to_computed_value(context))
+ }
+
+ /// Parse and validate a registered custom property value according to its syntax descriptor,
+ /// and check for computational independence.
+ pub fn parse<'i, 't>(
+ mut input: &mut CSSParser<'i, 't>,
+ syntax: &Descriptor,
+ url_data: &UrlExtraData,
+ allow_computationally_dependent: AllowComputationallyDependent,
+ ) -> Result<Self, StyleParseError<'i>> {
+ if syntax.is_universal() {
+ return Ok(Self::Universal(Arc::new(ComputedPropertyValue::parse(
+ &mut input, url_data,
+ )?)));
+ }
+
+ let mut values = SmallComponentVec::new();
+ let mut multiplier = None;
+ {
+ let mut parser = Parser::new(syntax, &mut values, &mut multiplier);
+ parser.parse(&mut input, url_data, allow_computationally_dependent)?;
+ }
+ let computed_value = if let Some(multiplier) = multiplier {
+ Self::List(ComponentList {
+ multiplier,
+ components: values.to_vec().into(),
+ })
+ } else {
+ Self::Component(values[0].clone())
+ };
+ Ok(computed_value)
+ }
+}
+
+impl ComputedValue {
+ fn serialization_types(&self) -> (TokenSerializationType, TokenSerializationType) {
+ match self {
+ Self::Component(component) => component.serialization_types(),
+ Self::Universal(_) => unreachable!(),
+ Self::List(list) => list
+ .components
+ .first()
+ .map_or(Default::default(), |f| f.serialization_types()),
+ }
+ }
+
+ fn to_declared_value(&self, url_data: &UrlExtraData) -> Arc<ComputedPropertyValue> {
+ if let Self::Universal(var) = self {
+ return Arc::clone(var);
+ }
+ Arc::new(self.to_variable_value(url_data))
+ }
+
+ fn to_variable_value(&self, url_data: &UrlExtraData) -> ComputedPropertyValue {
+ debug_assert!(!matches!(self, Self::Universal(..)), "Shouldn't be needed");
+ // TODO(zrhoffman, 1864736): Preserve the computed type instead of converting back to a
+ // string.
+ let serialization_types = self.serialization_types();
+ ComputedPropertyValue::new(
+ self.to_css_string(),
+ url_data,
+ serialization_types.0,
+ serialization_types.1,
+ )
+ }
+}
+
+/// Whether the computed value parsing should allow computationaly dependent values like 3em or
+/// var(-foo).
+///
+/// https://drafts.css-houdini.org/css-properties-values-api-1/#computationally-independent
+pub enum AllowComputationallyDependent {
+ /// Only computationally independent values are allowed.
+ No,
+ /// Computationally independent and dependent values are allowed.
+ Yes,
+}
+
+type SmallComponentVec = SmallVec<[SpecifiedValueComponent; 1]>;
+
+struct Parser<'a> {
+ syntax: &'a Descriptor,
+ output: &'a mut SmallComponentVec,
+ output_multiplier: &'a mut Option<Multiplier>,
+}
+
+impl<'a> Parser<'a> {
+ fn new(
+ syntax: &'a Descriptor,
+ output: &'a mut SmallComponentVec,
+ output_multiplier: &'a mut Option<Multiplier>,
+ ) -> Self {
+ Self {
+ syntax,
+ output,
+ output_multiplier,
+ }
+ }
+
+ fn parse<'i, 't>(
+ &mut self,
+ input: &mut CSSParser<'i, 't>,
+ url_data: &UrlExtraData,
+ allow_computationally_dependent: AllowComputationallyDependent,
+ ) -> Result<(), StyleParseError<'i>> {
+ use self::AllowComputationallyDependent::*;
+ let parsing_mode = match allow_computationally_dependent {
+ No => ParsingMode::DISALLOW_FONT_RELATIVE,
+ Yes => ParsingMode::DEFAULT,
+ };
+ let ref context = ParserContext::new(
+ Origin::Author,
+ url_data,
+ Some(CssRuleType::Style),
+ parsing_mode,
+ QuirksMode::NoQuirks,
+ /* namespaces = */ Default::default(),
+ None,
+ None,
+ );
+ for component in self.syntax.components.iter() {
+ let result = input.try_parse(|input| {
+ input.parse_entirely(|input| {
+ Self::parse_value(context, input, &component.unpremultiplied())
+ })
+ });
+ let Ok(values) = result else { continue };
+ self.output.extend(values);
+ *self.output_multiplier = component.multiplier();
+ break;
+ }
+ if self.output.is_empty() {
+ return Err(input.new_error(BasicParseErrorKind::EndOfInput));
+ }
+ Ok(())
+ }
+
+ fn parse_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut CSSParser<'i, 't>,
+ component: &SyntaxComponent,
+ ) -> Result<SmallComponentVec, StyleParseError<'i>> {
+ let mut values = SmallComponentVec::new();
+ values.push(Self::parse_component_without_multiplier(
+ context, input, component,
+ )?);
+
+ if let Some(multiplier) = component.multiplier() {
+ loop {
+ let result = Self::expect_multiplier(input, &multiplier);
+ if Self::expect_multiplier_yielded_eof_error(&result) {
+ break;
+ }
+ result?;
+ values.push(Self::parse_component_without_multiplier(
+ context, input, component,
+ )?);
+ }
+ }
+ Ok(values)
+ }
+
+ fn parse_component_without_multiplier<'i, 't>(
+ context: &ParserContext,
+ input: &mut CSSParser<'i, 't>,
+ component: &SyntaxComponent,
+ ) -> Result<SpecifiedValueComponent, StyleParseError<'i>> {
+ let data_type = match component.name() {
+ ComponentName::DataType(ty) => ty,
+ ComponentName::Ident(ref name) => {
+ let ident = CustomIdent::parse(input, &[])?;
+ if ident != *name {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ return Ok(SpecifiedValueComponent::CustomIdent(ident));
+ },
+ };
+
+ let value = match data_type {
+ DataType::Length => {
+ SpecifiedValueComponent::Length(specified::Length::parse(context, input)?)
+ },
+ DataType::Number => {
+ SpecifiedValueComponent::Number(specified::Number::parse(context, input)?)
+ },
+ DataType::Percentage => {
+ SpecifiedValueComponent::Percentage(specified::Percentage::parse(context, input)?)
+ },
+ DataType::LengthPercentage => SpecifiedValueComponent::LengthPercentage(
+ specified::LengthPercentage::parse(context, input)?,
+ ),
+ DataType::Color => {
+ SpecifiedValueComponent::Color(specified::Color::parse(context, input)?)
+ },
+ DataType::Image => {
+ SpecifiedValueComponent::Image(specified::Image::parse(context, input)?)
+ },
+ DataType::Url => {
+ SpecifiedValueComponent::Url(specified::url::SpecifiedUrl::parse(context, input)?)
+ },
+ DataType::Integer => {
+ SpecifiedValueComponent::Integer(specified::Integer::parse(context, input)?)
+ },
+ DataType::Angle => {
+ SpecifiedValueComponent::Angle(specified::Angle::parse(context, input)?)
+ },
+ DataType::Time => {
+ SpecifiedValueComponent::Time(specified::Time::parse(context, input)?)
+ },
+ DataType::Resolution => {
+ SpecifiedValueComponent::Resolution(specified::Resolution::parse(context, input)?)
+ },
+ DataType::TransformFunction => SpecifiedValueComponent::TransformFunction(
+ specified::Transform::parse(context, input)?,
+ ),
+ DataType::CustomIdent => {
+ let name = CustomIdent::parse(input, &[])?;
+ SpecifiedValueComponent::CustomIdent(name)
+ },
+ DataType::TransformList => {
+ let mut values = vec![];
+ let Some(multiplier) = component.unpremultiplied().multiplier() else {
+ debug_assert!(false, "Unpremultiplied <transform-list> had no multiplier?");
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::PropertySyntaxField(
+ PropertySyntaxParseError::UnexpectedEOF,
+ )),
+ );
+ };
+ debug_assert_matches!(multiplier, Multiplier::Space);
+ loop {
+ values.push(SpecifiedValueComponent::TransformFunction(
+ specified::Transform::parse(context, input)?,
+ ));
+ let result = Self::expect_multiplier(input, &multiplier);
+ if Self::expect_multiplier_yielded_eof_error(&result) {
+ break;
+ }
+ result?;
+ }
+ let list = ComponentList {
+ multiplier,
+ components: values.into(),
+ };
+ SpecifiedValueComponent::TransformList(list)
+ },
+ DataType::String => {
+ let string = input.expect_string()?;
+ SpecifiedValueComponent::String(string.as_ref().to_owned().into())
+ },
+ };
+ Ok(value)
+ }
+
+ fn expect_multiplier_yielded_eof_error<'i>(result: &Result<(), StyleParseError<'i>>) -> bool {
+ matches!(
+ result,
+ Err(StyleParseError {
+ kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput),
+ ..
+ })
+ )
+ }
+
+ fn expect_multiplier<'i, 't>(
+ input: &mut CSSParser<'i, 't>,
+ multiplier: &Multiplier,
+ ) -> Result<(), StyleParseError<'i>> {
+ match multiplier {
+ Multiplier::Space => {
+ input.expect_whitespace()?;
+ if input.is_exhausted() {
+ // If there was trailing whitespace, do not interpret it as a multiplier
+ return Err(input.new_error(BasicParseErrorKind::EndOfInput));
+ }
+ Ok(())
+ },
+ Multiplier::Comma => Ok(input.expect_comma()?),
+ }
+ }
+}
+
+
+/// An animated value for custom property.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
+pub struct CustomAnimatedValue {
+ /// The name of the custom property.
+ pub(crate) name: crate::custom_properties::Name,
+ /// The computed value of the custom property.
+ value: ComputedValue,
+ /// The url data where the value came from.
+ /// FIXME: This seems like it should not be needed: registered properties don't need it, and
+ /// unregistered properties animate discretely. But we need it so far because the computed
+ /// value representation isn't typed.
+ url_data: UrlExtraData,
+}
+
+impl Animate for CustomAnimatedValue {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.name != other.name {
+ return Err(())
+ }
+ let value = self.value.animate(&other.value, procedure)?;
+ Ok(Self {
+ name: self.name.clone(),
+ value,
+ // NOTE: This is sketchy AF, but it's ~fine, since values that can animate (non-universal)
+ // don't need it.
+ url_data: self.url_data.clone(),
+ })
+ }
+}
+
+impl CustomAnimatedValue {
+ pub(crate) fn from_computed(
+ name: &crate::custom_properties::Name,
+ value: &Arc<ComputedPropertyValue>,
+ ) -> Self {
+ Self {
+ name: name.clone(),
+ // FIXME: Should probably preserve type-ness in ComputedPropertyValue.
+ value: ComputedValue::Universal(value.clone()),
+ url_data: value.url_data.clone(),
+ }
+ }
+
+ pub(crate) fn from_declaration(
+ declaration: &properties::CustomDeclaration,
+ context: &mut computed::Context,
+ _initial: &properties::ComputedValues,
+ ) -> Option<Self> {
+ let value = match declaration.value {
+ properties::CustomDeclarationValue::Value(ref v) => v,
+ // FIXME: This should be made to work to the extent possible like for non-custom
+ // properties (using `initial` at least to handle unset / inherit).
+ properties::CustomDeclarationValue::CSSWideKeyword(..) => return None,
+ };
+
+ debug_assert!(
+ context.builder.stylist.is_some(),
+ "Need a Stylist to get property registration!"
+ );
+ let registration =
+ context.builder.stylist.unwrap().get_custom_property_registration(&declaration.name);
+
+ // FIXME: Do we need to perform substitution here somehow?
+ let computed_value = if registration.syntax.is_universal() {
+ None
+ } else {
+ let mut input = cssparser::ParserInput::new(&value.css);
+ let mut input = CSSParser::new(&mut input);
+ SpecifiedValue::get_computed_value(
+ &mut input,
+ registration,
+ &value.url_data,
+ context,
+ AllowComputationallyDependent::Yes,
+ ).ok()
+ };
+
+ let url_data = value.url_data.clone();
+ let value = computed_value.unwrap_or_else(|| ComputedValue::Universal(Arc::clone(value)));
+ Some(Self {
+ name: declaration.name.clone(),
+ url_data,
+ value,
+ })
+ }
+
+ pub(crate) fn to_declaration(&self) -> properties::PropertyDeclaration {
+ properties::PropertyDeclaration::Custom(properties::CustomDeclaration {
+ name: self.name.clone(),
+ value: properties::CustomDeclarationValue::Value(self.value.to_declared_value(&self.url_data)),
+ })
+ }
+}
diff --git a/servo/components/style/queries/condition.rs b/servo/components/style/queries/condition.rs
new file mode 100644
index 0000000000..e17e6abd2e
--- /dev/null
+++ b/servo/components/style/queries/condition.rs
@@ -0,0 +1,366 @@
+/* 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/. */
+
+//! A query condition:
+//!
+//! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition
+//! https://drafts.csswg.org/css-contain-3/#typedef-container-condition
+
+use super::{FeatureFlags, FeatureType, QueryFeatureExpression};
+use crate::values::computed;
+use crate::{error_reporting::ContextualParseError, parser::ParserContext};
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// A binary `and` or `or` operator.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
+#[allow(missing_docs)]
+pub enum Operator {
+ And,
+ Or,
+}
+
+/// Whether to allow an `or` condition or not during parsing.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)]
+enum AllowOr {
+ Yes,
+ No,
+}
+
+/// https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)]
+pub enum KleeneValue {
+ /// False
+ False = 0,
+ /// True
+ True = 1,
+ /// Either true or false, but we’re not sure which yet.
+ Unknown,
+}
+
+impl From<bool> for KleeneValue {
+ fn from(b: bool) -> Self {
+ if b {
+ Self::True
+ } else {
+ Self::False
+ }
+ }
+}
+
+impl KleeneValue {
+ /// Turns this Kleene value to a bool, taking the unknown value as an
+ /// argument.
+ pub fn to_bool(self, unknown: bool) -> bool {
+ match self {
+ Self::True => true,
+ Self::False => false,
+ Self::Unknown => unknown,
+ }
+ }
+}
+
+impl std::ops::Not for KleeneValue {
+ type Output = Self;
+
+ fn not(self) -> Self {
+ match self {
+ Self::True => Self::False,
+ Self::False => Self::True,
+ Self::Unknown => Self::Unknown,
+ }
+ }
+}
+
+// Implements the logical and operation.
+impl std::ops::BitAnd for KleeneValue {
+ type Output = Self;
+
+ fn bitand(self, other: Self) -> Self {
+ if self == Self::False || other == Self::False {
+ return Self::False;
+ }
+ if self == Self::Unknown || other == Self::Unknown {
+ return Self::Unknown;
+ }
+ Self::True
+ }
+}
+
+// Implements the logical or operation.
+impl std::ops::BitOr for KleeneValue {
+ type Output = Self;
+
+ fn bitor(self, other: Self) -> Self {
+ if self == Self::True || other == Self::True {
+ return Self::True;
+ }
+ if self == Self::Unknown || other == Self::Unknown {
+ return Self::Unknown;
+ }
+ Self::False
+ }
+}
+
+impl std::ops::BitOrAssign for KleeneValue {
+ fn bitor_assign(&mut self, other: Self) {
+ *self = *self | other;
+ }
+}
+
+impl std::ops::BitAndAssign for KleeneValue {
+ fn bitand_assign(&mut self, other: Self) {
+ *self = *self & other;
+ }
+}
+
+/// Represents a condition.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum QueryCondition {
+ /// A simple feature expression, implicitly parenthesized.
+ Feature(QueryFeatureExpression),
+ /// A negation of a condition.
+ Not(Box<QueryCondition>),
+ /// A set of joint operations.
+ Operation(Box<[QueryCondition]>, Operator),
+ /// A condition wrapped in parenthesis.
+ InParens(Box<QueryCondition>),
+ /// [ <function-token> <any-value>? ) ] | [ ( <any-value>? ) ]
+ GeneralEnclosed(String),
+}
+
+impl ToCss for QueryCondition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ // NOTE(emilio): QueryFeatureExpression already includes the
+ // parenthesis.
+ QueryCondition::Feature(ref f) => f.to_css(dest),
+ QueryCondition::Not(ref c) => {
+ dest.write_str("not ")?;
+ c.to_css(dest)
+ },
+ QueryCondition::InParens(ref c) => {
+ dest.write_char('(')?;
+ c.to_css(dest)?;
+ dest.write_char(')')
+ },
+ QueryCondition::Operation(ref list, op) => {
+ let mut iter = list.iter();
+ iter.next().unwrap().to_css(dest)?;
+ for item in iter {
+ dest.write_char(' ')?;
+ op.to_css(dest)?;
+ dest.write_char(' ')?;
+ item.to_css(dest)?;
+ }
+ Ok(())
+ },
+ QueryCondition::GeneralEnclosed(ref s) => dest.write_str(&s),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value>
+fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> {
+ input.expect_no_error_token().map_err(Into::into)
+}
+
+impl QueryCondition {
+ /// Parse a single condition.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, feature_type, AllowOr::Yes)
+ }
+
+ fn visit<F>(&self, visitor: &mut F)
+ where
+ F: FnMut(&Self),
+ {
+ visitor(self);
+ match *self {
+ Self::Feature(..) => {},
+ Self::GeneralEnclosed(..) => {},
+ Self::Not(ref cond) => cond.visit(visitor),
+ Self::Operation(ref conds, _op) => {
+ for cond in conds.iter() {
+ cond.visit(visitor);
+ }
+ },
+ Self::InParens(ref cond) => cond.visit(visitor),
+ }
+ }
+
+ /// Returns the union of all flags in the expression. This is useful for
+ /// container queries.
+ pub fn cumulative_flags(&self) -> FeatureFlags {
+ let mut result = FeatureFlags::empty();
+ self.visit(&mut |condition| {
+ if let Self::Feature(ref f) = condition {
+ result.insert(f.feature_flags())
+ }
+ });
+ result
+ }
+
+ /// Parse a single condition, disallowing `or` expressions.
+ ///
+ /// To be used from the legacy query syntax.
+ pub fn parse_disallow_or<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, feature_type, AllowOr::No)
+ }
+
+ /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition or
+ /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition-without-or
+ /// (depending on `allow_or`).
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ allow_or: AllowOr,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() {
+ let inner_condition = Self::parse_in_parens(context, input, feature_type)?;
+ return Ok(QueryCondition::Not(Box::new(inner_condition)));
+ }
+
+ let first_condition = Self::parse_in_parens(context, input, feature_type)?;
+ let operator = match input.try_parse(Operator::parse) {
+ Ok(op) => op,
+ Err(..) => return Ok(first_condition),
+ };
+
+ if allow_or == AllowOr::No && operator == Operator::Or {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let mut conditions = vec![];
+ conditions.push(first_condition);
+ conditions.push(Self::parse_in_parens(context, input, feature_type)?);
+
+ let delim = match operator {
+ Operator::And => "and",
+ Operator::Or => "or",
+ };
+
+ loop {
+ if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() {
+ return Ok(QueryCondition::Operation(
+ conditions.into_boxed_slice(),
+ operator,
+ ));
+ }
+
+ conditions.push(Self::parse_in_parens(context, input, feature_type)?);
+ }
+ }
+
+ fn parse_in_parenthesis_block<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ // Base case. Make sure to preserve this error as it's more generally
+ // relevant.
+ let feature_error = match input.try_parse(|input| {
+ QueryFeatureExpression::parse_in_parenthesis_block(context, input, feature_type)
+ }) {
+ Ok(expr) => return Ok(Self::Feature(expr)),
+ Err(e) => e,
+ };
+ if let Ok(inner) = Self::parse(context, input, feature_type) {
+ return Ok(Self::InParens(Box::new(inner)));
+ }
+ Err(feature_error)
+ }
+
+ /// Parse a condition in parentheses, or `<general-enclosed>`.
+ ///
+ /// https://drafts.csswg.org/mediaqueries/#typedef-media-in-parens
+ pub fn parse_in_parens<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ input.skip_whitespace();
+ let start = input.position();
+ let start_location = input.current_source_location();
+ match *input.next()? {
+ Token::ParenthesisBlock => {
+ let nested = input.try_parse(|input| {
+ input.parse_nested_block(|input| {
+ Self::parse_in_parenthesis_block(context, input, feature_type)
+ })
+ });
+ match nested {
+ Ok(nested) => return Ok(nested),
+ Err(e) => {
+ // We're about to swallow the error in a `<general-enclosed>`
+ // condition, so report it while we can.
+ let loc = e.location;
+ let error =
+ ContextualParseError::InvalidMediaRule(input.slice_from(start), e);
+ context.log_css_error(loc, error);
+ },
+ }
+ },
+ Token::Function(..) => {
+ // TODO: handle `style()` queries, etc.
+ },
+ ref t => return Err(start_location.new_unexpected_token_error(t.clone())),
+ }
+ input.parse_nested_block(consume_any_value)?;
+ Ok(Self::GeneralEnclosed(input.slice_from(start).to_owned()))
+ }
+
+ /// Whether this condition matches the device and quirks mode.
+ /// https://drafts.csswg.org/mediaqueries/#evaluating
+ /// https://drafts.csswg.org/mediaqueries/#typedef-general-enclosed
+ /// Kleene 3-valued logic is adopted here due to the introduction of
+ /// <general-enclosed>.
+ pub fn matches(&self, context: &computed::Context) -> KleeneValue {
+ match *self {
+ QueryCondition::Feature(ref f) => f.matches(context),
+ QueryCondition::GeneralEnclosed(_) => KleeneValue::Unknown,
+ QueryCondition::InParens(ref c) => c.matches(context),
+ QueryCondition::Not(ref c) => !c.matches(context),
+ QueryCondition::Operation(ref conditions, op) => {
+ debug_assert!(!conditions.is_empty(), "We never create an empty op");
+ match op {
+ Operator::And => {
+ let mut result = KleeneValue::True;
+ for c in conditions.iter() {
+ result &= c.matches(context);
+ if result == KleeneValue::False {
+ break;
+ }
+ }
+ result
+ },
+ Operator::Or => {
+ let mut result = KleeneValue::False;
+ for c in conditions.iter() {
+ result |= c.matches(context);
+ if result == KleeneValue::True {
+ break;
+ }
+ }
+ result
+ },
+ }
+ },
+ }
+ }
+}
diff --git a/servo/components/style/queries/feature.rs b/servo/components/style/queries/feature.rs
new file mode 100644
index 0000000000..83ff7e7522
--- /dev/null
+++ b/servo/components/style/queries/feature.rs
@@ -0,0 +1,198 @@
+/* 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/. */
+
+//! Query features.
+
+use super::condition::KleeneValue;
+use crate::parser::ParserContext;
+use crate::values::computed::{self, CSSPixelLength, Ratio, Resolution};
+use crate::values::AtomString;
+use crate::Atom;
+use cssparser::Parser;
+use std::fmt;
+use style_traits::ParseError;
+
+/// A generic discriminant for an enum value.
+pub type KeywordDiscriminant = u8;
+
+type QueryFeatureGetter<T> = fn(device: &computed::Context) -> T;
+
+/// Serializes a given discriminant.
+///
+/// FIXME(emilio): we could prevent this allocation if the ToCss code would
+/// generate a method for keywords to get the static string or something.
+pub type KeywordSerializer = fn(KeywordDiscriminant) -> String;
+
+/// Parses a given identifier.
+pub type KeywordParser = for<'a, 'i, 't> fn(
+ context: &'a ParserContext,
+ input: &'a mut Parser<'i, 't>,
+) -> Result<KeywordDiscriminant, ParseError<'i>>;
+
+/// An evaluator for a given feature.
+///
+/// This determines the kind of values that get parsed, too.
+#[allow(missing_docs)]
+pub enum Evaluator {
+ Length(QueryFeatureGetter<CSSPixelLength>),
+ OptionalLength(QueryFeatureGetter<Option<CSSPixelLength>>),
+ Integer(QueryFeatureGetter<i32>),
+ Float(QueryFeatureGetter<f32>),
+ BoolInteger(QueryFeatureGetter<bool>),
+ /// A non-negative number ratio, such as the one from device-pixel-ratio.
+ NumberRatio(QueryFeatureGetter<Ratio>),
+ OptionalNumberRatio(QueryFeatureGetter<Option<Ratio>>),
+ /// A resolution.
+ Resolution(QueryFeatureGetter<Resolution>),
+ String(fn(&computed::Context, Option<&AtomString>) -> KleeneValue),
+ /// A keyword value.
+ Enumerated {
+ /// The parser to get a discriminant given a string.
+ parser: KeywordParser,
+ /// The serializer to get a string from a discriminant.
+ ///
+ /// This is guaranteed to be called with a keyword that `parser` has
+ /// produced.
+ serializer: KeywordSerializer,
+ /// The evaluator itself. This is guaranteed to be called with a
+ /// keyword that `parser` has produced.
+ evaluator: fn(&computed::Context, Option<KeywordDiscriminant>) -> KleeneValue,
+ },
+}
+
+/// A simple helper macro to create a keyword evaluator.
+///
+/// This assumes that keyword feature expressions don't accept ranges, and
+/// asserts if that's not true. As of today there's nothing like that (does that
+/// even make sense?).
+macro_rules! keyword_evaluator {
+ ($actual_evaluator:ident, $keyword_type:ty) => {{
+ fn __parse<'i, 't>(
+ context: &$crate::parser::ParserContext,
+ input: &mut $crate::cssparser::Parser<'i, 't>,
+ ) -> Result<$crate::queries::feature::KeywordDiscriminant, ::style_traits::ParseError<'i>>
+ {
+ let kw = <$keyword_type as $crate::parser::Parse>::parse(context, input)?;
+ Ok(kw as $crate::queries::feature::KeywordDiscriminant)
+ }
+
+ fn __serialize(kw: $crate::queries::feature::KeywordDiscriminant) -> String {
+ // This unwrap is ok because the only discriminants that get
+ // back to us is the ones that `parse` produces.
+ let value: $keyword_type = ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap();
+ <$keyword_type as ::style_traits::ToCss>::to_css_string(&value)
+ }
+
+ fn __evaluate(
+ context: &$crate::values::computed::Context,
+ value: Option<$crate::queries::feature::KeywordDiscriminant>,
+ ) -> $crate::queries::condition::KleeneValue {
+ // This unwrap is ok because the only discriminants that get
+ // back to us is the ones that `parse` produces.
+ let value: Option<$keyword_type> =
+ value.map(|kw| ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap());
+ $crate::queries::condition::KleeneValue::from($actual_evaluator(context, value))
+ }
+
+ $crate::queries::feature::Evaluator::Enumerated {
+ parser: __parse,
+ serializer: __serialize,
+ evaluator: __evaluate,
+ }
+ }};
+}
+
+/// Different flags or toggles that change how a expression is parsed or
+/// evaluated.
+#[derive(Clone, Copy, Debug, ToShmem)]
+pub struct FeatureFlags(u8);
+bitflags! {
+ impl FeatureFlags : u8 {
+ /// The feature should only be parsed in chrome and ua sheets.
+ const CHROME_AND_UA_ONLY = 1 << 0;
+ /// The feature requires a -webkit- prefix.
+ const WEBKIT_PREFIX = 1 << 1;
+ /// The feature requires the inline-axis containment.
+ const CONTAINER_REQUIRES_INLINE_AXIS = 1 << 2;
+ /// The feature requires the block-axis containment.
+ const CONTAINER_REQUIRES_BLOCK_AXIS = 1 << 3;
+ /// The feature requires containment in the physical width axis.
+ const CONTAINER_REQUIRES_WIDTH_AXIS = 1 << 4;
+ /// The feature requires containment in the physical height axis.
+ const CONTAINER_REQUIRES_HEIGHT_AXIS = 1 << 5;
+ /// The feature evaluation depends on the viewport size.
+ const VIEWPORT_DEPENDENT = 1 << 6;
+ }
+}
+
+impl FeatureFlags {
+ /// Returns parsing requirement flags.
+ pub fn parsing_requirements(self) -> Self {
+ self.intersection(Self::CHROME_AND_UA_ONLY | Self::WEBKIT_PREFIX)
+ }
+
+ /// Returns all the container axis flags.
+ pub fn all_container_axes() -> Self {
+ Self::CONTAINER_REQUIRES_INLINE_AXIS |
+ Self::CONTAINER_REQUIRES_BLOCK_AXIS |
+ Self::CONTAINER_REQUIRES_WIDTH_AXIS |
+ Self::CONTAINER_REQUIRES_HEIGHT_AXIS
+ }
+
+ /// Returns our subset of container axis flags.
+ pub fn container_axes(self) -> Self {
+ self.intersection(Self::all_container_axes())
+ }
+}
+
+/// Whether a feature allows ranges or not.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[allow(missing_docs)]
+pub enum AllowsRanges {
+ Yes,
+ No,
+}
+
+/// A description of a feature.
+pub struct QueryFeatureDescription {
+ /// The feature name, in ascii lowercase.
+ pub name: Atom,
+ /// Whether min- / max- prefixes are allowed or not.
+ pub allows_ranges: AllowsRanges,
+ /// The evaluator, which we also use to determine which kind of value to
+ /// parse.
+ pub evaluator: Evaluator,
+ /// Different feature-specific flags.
+ pub flags: FeatureFlags,
+}
+
+impl QueryFeatureDescription {
+ /// Whether this feature allows ranges.
+ #[inline]
+ pub fn allows_ranges(&self) -> bool {
+ self.allows_ranges == AllowsRanges::Yes
+ }
+}
+
+/// A simple helper to construct a `QueryFeatureDescription`.
+macro_rules! feature {
+ ($name:expr, $allows_ranges:expr, $evaluator:expr, $flags:expr,) => {
+ $crate::queries::feature::QueryFeatureDescription {
+ name: $name,
+ allows_ranges: $allows_ranges,
+ evaluator: $evaluator,
+ flags: $flags,
+ }
+ };
+}
+
+impl fmt::Debug for QueryFeatureDescription {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("QueryFeatureDescription")
+ .field("name", &self.name)
+ .field("allows_ranges", &self.allows_ranges)
+ .field("flags", &self.flags)
+ .finish()
+ }
+}
diff --git a/servo/components/style/queries/feature_expression.rs b/servo/components/style/queries/feature_expression.rs
new file mode 100644
index 0000000000..c0171c2058
--- /dev/null
+++ b/servo/components/style/queries/feature_expression.rs
@@ -0,0 +1,764 @@
+/* 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/. */
+
+//! Parsing for query feature expressions, like `(foo: bar)` or
+//! `(width >= 400px)`.
+
+use super::feature::{Evaluator, QueryFeatureDescription};
+use super::feature::{FeatureFlags, KeywordDiscriminant};
+use crate::parser::{Parse, ParserContext};
+use crate::queries::condition::KleeneValue;
+use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
+use crate::values::computed::{self, Ratio, ToComputedValue};
+use crate::values::specified::{Integer, Length, Number, Resolution};
+use crate::values::{AtomString, CSSFloat};
+use crate::{Atom, Zero};
+use cssparser::{Parser, Token};
+use std::cmp::{Ordering, PartialOrd};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// Whether we're parsing a media or container query feature.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub enum FeatureType {
+ /// We're parsing a media feature.
+ Media,
+ /// We're parsing a container feature.
+ Container,
+}
+
+impl FeatureType {
+ fn features(&self) -> &'static [QueryFeatureDescription] {
+ #[cfg(feature = "gecko")]
+ use crate::gecko::media_features::MEDIA_FEATURES;
+ #[cfg(feature = "servo")]
+ use crate::servo::media_queries::MEDIA_FEATURES;
+
+ use crate::stylesheets::container_rule::CONTAINER_FEATURES;
+
+ match *self {
+ FeatureType::Media => &MEDIA_FEATURES,
+ FeatureType::Container => &CONTAINER_FEATURES,
+ }
+ }
+
+ fn find_feature(&self, name: &Atom) -> Option<(usize, &'static QueryFeatureDescription)> {
+ self.features()
+ .iter()
+ .enumerate()
+ .find(|(_, f)| f.name == *name)
+ }
+}
+
+/// The kind of matching that should be performed on a feature value.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+enum LegacyRange {
+ /// At least the specified value.
+ Min,
+ /// At most the specified value.
+ Max,
+}
+
+/// The operator that was specified in this feature.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+enum Operator {
+ /// =
+ Equal,
+ /// >
+ GreaterThan,
+ /// >=
+ GreaterThanEqual,
+ /// <
+ LessThan,
+ /// <=
+ LessThanEqual,
+}
+
+impl ToCss for Operator {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str(match *self {
+ Self::Equal => "=",
+ Self::LessThan => "<",
+ Self::LessThanEqual => "<=",
+ Self::GreaterThan => ">",
+ Self::GreaterThanEqual => ">=",
+ })
+ }
+}
+
+impl Operator {
+ fn is_compatible_with(self, right_op: Self) -> bool {
+ // Some operators are not compatible with each other in multi-range
+ // context.
+ match self {
+ Self::Equal => false,
+ Self::GreaterThan | Self::GreaterThanEqual => {
+ matches!(right_op, Self::GreaterThan | Self::GreaterThanEqual)
+ },
+ Self::LessThan | Self::LessThanEqual => {
+ matches!(right_op, Self::LessThan | Self::LessThanEqual)
+ },
+ }
+ }
+
+ fn evaluate(&self, cmp: Ordering) -> bool {
+ match *self {
+ Self::Equal => cmp == Ordering::Equal,
+ Self::GreaterThan => cmp == Ordering::Greater,
+ Self::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater,
+ Self::LessThan => cmp == Ordering::Less,
+ Self::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less,
+ }
+ }
+
+ fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let operator = match *input.next()? {
+ Token::Delim('=') => return Ok(Operator::Equal),
+ Token::Delim('>') => Operator::GreaterThan,
+ Token::Delim('<') => Operator::LessThan,
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ };
+
+ // https://drafts.csswg.org/mediaqueries-4/#mq-syntax:
+ //
+ // No whitespace is allowed between the “<” or “>”
+ // <delim-token>s and the following “=” <delim-token>, if it’s
+ // present.
+ //
+ // TODO(emilio): Maybe we should ignore comments as well?
+ // https://github.com/w3c/csswg-drafts/issues/6248
+ let parsed_equal = input
+ .try_parse(|i| {
+ let t = i.next_including_whitespace().map_err(|_| ())?;
+ if !matches!(t, Token::Delim('=')) {
+ return Err(());
+ }
+ Ok(())
+ })
+ .is_ok();
+
+ if !parsed_equal {
+ return Ok(operator);
+ }
+
+ Ok(match operator {
+ Operator::GreaterThan => Operator::GreaterThanEqual,
+ Operator::LessThan => Operator::LessThanEqual,
+ _ => unreachable!(),
+ })
+ }
+}
+
+#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
+enum QueryFeatureExpressionKind {
+ /// Just the media feature name.
+ Empty,
+
+ /// A single value.
+ Single(QueryExpressionValue),
+
+ /// Legacy range syntax (min-*: value) or so.
+ LegacyRange(LegacyRange, QueryExpressionValue),
+
+ /// Modern range context syntax:
+ /// https://drafts.csswg.org/mediaqueries-5/#mq-range-context
+ Range {
+ left: Option<(Operator, QueryExpressionValue)>,
+ right: Option<(Operator, QueryExpressionValue)>,
+ },
+}
+
+impl QueryFeatureExpressionKind {
+ /// Evaluate a given range given an optional query value and a value from
+ /// the browser.
+ fn evaluate<T>(
+ &self,
+ context_value: T,
+ mut compute: impl FnMut(&QueryExpressionValue) -> T,
+ ) -> bool
+ where
+ T: PartialOrd + Zero,
+ {
+ match *self {
+ Self::Empty => return !context_value.is_zero(),
+ Self::Single(ref value) => {
+ let value = compute(value);
+ let cmp = match context_value.partial_cmp(&value) {
+ Some(c) => c,
+ None => return false,
+ };
+ cmp == Ordering::Equal
+ },
+ Self::LegacyRange(ref range, ref value) => {
+ let value = compute(value);
+ let cmp = match context_value.partial_cmp(&value) {
+ Some(c) => c,
+ None => return false,
+ };
+ cmp == Ordering::Equal ||
+ match range {
+ LegacyRange::Min => cmp == Ordering::Greater,
+ LegacyRange::Max => cmp == Ordering::Less,
+ }
+ },
+ Self::Range {
+ ref left,
+ ref right,
+ } => {
+ debug_assert!(left.is_some() || right.is_some());
+ if let Some((ref op, ref value)) = left {
+ let value = compute(value);
+ let cmp = match value.partial_cmp(&context_value) {
+ Some(c) => c,
+ None => return false,
+ };
+ if !op.evaluate(cmp) {
+ return false;
+ }
+ }
+ if let Some((ref op, ref value)) = right {
+ let value = compute(value);
+ let cmp = match context_value.partial_cmp(&value) {
+ Some(c) => c,
+ None => return false,
+ };
+ if !op.evaluate(cmp) {
+ return false;
+ }
+ }
+ true
+ },
+ }
+ }
+
+ /// Non-ranged features only need to compare to one value at most.
+ fn non_ranged_value(&self) -> Option<&QueryExpressionValue> {
+ match *self {
+ Self::Empty => None,
+ Self::Single(ref v) => Some(v),
+ Self::LegacyRange(..) | Self::Range { .. } => {
+ debug_assert!(false, "Unexpected ranged value in non-ranged feature!");
+ None
+ },
+ }
+ }
+}
+
+/// A feature expression contains a reference to the feature, the value the
+/// query contained, and the range to evaluate.
+#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
+pub struct QueryFeatureExpression {
+ feature_type: FeatureType,
+ feature_index: usize,
+ kind: QueryFeatureExpressionKind,
+}
+
+impl ToCss for QueryFeatureExpression {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_char('(')?;
+
+ match self.kind {
+ QueryFeatureExpressionKind::Empty => self.write_name(dest)?,
+ QueryFeatureExpressionKind::Single(ref v) |
+ QueryFeatureExpressionKind::LegacyRange(_, ref v) => {
+ self.write_name(dest)?;
+ dest.write_str(": ")?;
+ v.to_css(dest, self)?;
+ },
+ QueryFeatureExpressionKind::Range {
+ ref left,
+ ref right,
+ } => {
+ if let Some((ref op, ref val)) = left {
+ val.to_css(dest, self)?;
+ dest.write_char(' ')?;
+ op.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+ self.write_name(dest)?;
+ if let Some((ref op, ref val)) = right {
+ dest.write_char(' ')?;
+ op.to_css(dest)?;
+ dest.write_char(' ')?;
+ val.to_css(dest, self)?;
+ }
+ },
+ }
+ dest.write_char(')')
+ }
+}
+
+fn consume_operation_or_colon<'i>(
+ input: &mut Parser<'i, '_>,
+) -> Result<Option<Operator>, ParseError<'i>> {
+ if input.try_parse(|input| input.expect_colon()).is_ok() {
+ return Ok(None);
+ }
+ Operator::parse(input).map(|op| Some(op))
+}
+
+#[allow(unused_variables)]
+fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool {
+ #[cfg(feature = "gecko")]
+ {
+ if *feature == atom!("forced-colors") {
+ // forced-colors is always enabled in the ua and chrome. On
+ // the web it is hidden behind a preference, which is defaulted
+ // to 'true' as of bug 1659511.
+ return !context.chrome_rules_enabled() &&
+ !static_prefs::pref!("layout.css.forced-colors.enabled");
+ }
+ // prefers-contrast is always enabled in the ua and chrome. On
+ // the web it is hidden behind a preference.
+ if *feature == atom!("prefers-contrast") {
+ return !context.chrome_rules_enabled() &&
+ !static_prefs::pref!("layout.css.prefers-contrast.enabled");
+ }
+
+ // prefers-reduced-transparency is always enabled in the ua and chrome. On
+ // the web it is hidden behind a preference (see Bug 1822176).
+ if *feature == atom!("prefers-reduced-transparency") {
+ return !context.chrome_rules_enabled() &&
+ !static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled");
+ }
+
+ // inverted-colors is always enabled in the ua and chrome. On
+ // the web it is hidden behind a preferenc.
+ if *feature == atom!("inverted-colors") {
+ return !context.chrome_rules_enabled() &&
+ !static_prefs::pref!("layout.css.inverted-colors.enabled");
+ }
+ }
+ false
+}
+
+impl QueryFeatureExpression {
+ fn new(
+ feature_type: FeatureType,
+ feature_index: usize,
+ kind: QueryFeatureExpressionKind,
+ ) -> Self {
+ debug_assert!(feature_index < feature_type.features().len());
+ Self {
+ feature_type,
+ feature_index,
+ kind,
+ }
+ }
+
+ fn write_name<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let feature = self.feature();
+ if feature.flags.contains(FeatureFlags::WEBKIT_PREFIX) {
+ dest.write_str("-webkit-")?;
+ }
+
+ if let QueryFeatureExpressionKind::LegacyRange(range, _) = self.kind {
+ match range {
+ LegacyRange::Min => dest.write_str("min-")?,
+ LegacyRange::Max => dest.write_str("max-")?,
+ }
+ }
+
+ // NB: CssStringWriter not needed, feature names are under control.
+ write!(dest, "{}", feature.name)?;
+
+ Ok(())
+ }
+
+ fn feature(&self) -> &'static QueryFeatureDescription {
+ &self.feature_type.features()[self.feature_index]
+ }
+
+ /// Returns the feature flags for our feature.
+ pub fn feature_flags(&self) -> FeatureFlags {
+ self.feature().flags
+ }
+
+ /// Parse a feature expression of the form:
+ ///
+ /// ```
+ /// (media-feature: media-value)
+ /// ```
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_parenthesis_block()?;
+ input.parse_nested_block(|input| {
+ Self::parse_in_parenthesis_block(context, input, feature_type)
+ })
+ }
+
+ fn parse_feature_name<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<(usize, Option<LegacyRange>), ParseError<'i>> {
+ let mut flags = FeatureFlags::empty();
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+
+ if context.chrome_rules_enabled() {
+ flags.insert(FeatureFlags::CHROME_AND_UA_ONLY);
+ }
+
+ let mut feature_name = &**ident;
+ if starts_with_ignore_ascii_case(feature_name, "-webkit-") {
+ feature_name = &feature_name[8..];
+ flags.insert(FeatureFlags::WEBKIT_PREFIX);
+ }
+
+ let range = if starts_with_ignore_ascii_case(feature_name, "min-") {
+ feature_name = &feature_name[4..];
+ Some(LegacyRange::Min)
+ } else if starts_with_ignore_ascii_case(feature_name, "max-") {
+ feature_name = &feature_name[4..];
+ Some(LegacyRange::Max)
+ } else {
+ None
+ };
+
+ let atom = Atom::from(string_as_ascii_lowercase(feature_name));
+ let (feature_index, feature) = match feature_type.find_feature(&atom) {
+ Some((i, f)) => (i, f),
+ None => {
+ return Err(location.new_custom_error(
+ StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
+ ))
+ },
+ };
+
+ if disabled_by_pref(&feature.name, context) ||
+ !flags.contains(feature.flags.parsing_requirements()) ||
+ (range.is_some() && !feature.allows_ranges())
+ {
+ return Err(location.new_custom_error(
+ StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
+ ));
+ }
+
+ Ok((feature_index, range))
+ }
+
+ /// Parses the following range syntax:
+ ///
+ /// (feature-value <operator> feature-name)
+ /// (feature-value <operator> feature-name <operator> feature-value)
+ fn parse_multi_range_syntax<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ let start = input.state();
+
+ // To parse the values, we first need to find the feature name. We rely
+ // on feature values for ranged features not being able to be top-level
+ // <ident>s, which holds.
+ let feature_index = loop {
+ // NOTE: parse_feature_name advances the input.
+ if let Ok((index, range)) = Self::parse_feature_name(context, input, feature_type) {
+ if range.is_some() {
+ // Ranged names are not allowed here.
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ break index;
+ }
+ if input.is_exhausted() {
+ return Err(start
+ .source_location()
+ .new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ };
+
+ input.reset(&start);
+
+ let feature = &feature_type.features()[feature_index];
+ let left_val = QueryExpressionValue::parse(feature, context, input)?;
+ let left_op = Operator::parse(input)?;
+
+ {
+ let (parsed_index, _) = Self::parse_feature_name(context, input, feature_type)?;
+ debug_assert_eq!(
+ parsed_index, feature_index,
+ "How did we find a different feature?"
+ );
+ }
+
+ let right_op = input.try_parse(Operator::parse).ok();
+ let right = match right_op {
+ Some(op) => {
+ if !left_op.is_compatible_with(op) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Some((op, QueryExpressionValue::parse(feature, context, input)?))
+ },
+ None => None,
+ };
+ Ok(Self::new(
+ feature_type,
+ feature_index,
+ QueryFeatureExpressionKind::Range {
+ left: Some((left_op, left_val)),
+ right,
+ },
+ ))
+ }
+
+ /// Parse a feature expression where we've already consumed the parenthesis.
+ pub fn parse_in_parenthesis_block<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ let (feature_index, range) =
+ match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) {
+ Ok(v) => v,
+ Err(e) => {
+ if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) {
+ return Ok(expr);
+ }
+ return Err(e);
+ },
+ };
+ let operator = input.try_parse(consume_operation_or_colon);
+ let operator = match operator {
+ Err(..) => {
+ // If there's no colon, this is a query of the form
+ // '(<feature>)', that is, there's no value specified.
+ //
+ // Gecko doesn't allow ranged expressions without a
+ // value, so just reject them here too.
+ if range.is_some() {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue)
+ );
+ }
+
+ return Ok(Self::new(
+ feature_type,
+ feature_index,
+ QueryFeatureExpressionKind::Empty,
+ ));
+ },
+ Ok(operator) => operator,
+ };
+
+ let feature = &feature_type.features()[feature_index];
+
+ let value = QueryExpressionValue::parse(feature, context, input).map_err(|err| {
+ err.location
+ .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue)
+ })?;
+
+ let kind = match range {
+ Some(range) => {
+ if operator.is_some() {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)
+ );
+ }
+ QueryFeatureExpressionKind::LegacyRange(range, value)
+ },
+ None => match operator {
+ Some(operator) => {
+ if !feature.allows_ranges() {
+ return Err(input
+ .new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator));
+ }
+ QueryFeatureExpressionKind::Range {
+ left: None,
+ right: Some((operator, value)),
+ }
+ },
+ None => QueryFeatureExpressionKind::Single(value),
+ },
+ };
+
+ Ok(Self::new(feature_type, feature_index, kind))
+ }
+
+ /// Returns whether this query evaluates to true for the given device.
+ pub fn matches(&self, context: &computed::Context) -> KleeneValue {
+ macro_rules! expect {
+ ($variant:ident, $v:expr) => {
+ match *$v {
+ QueryExpressionValue::$variant(ref v) => v,
+ _ => unreachable!("Unexpected QueryExpressionValue"),
+ }
+ };
+ }
+
+ KleeneValue::from(match self.feature().evaluator {
+ Evaluator::Length(eval) => {
+ let v = eval(context);
+ self.kind
+ .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
+ },
+ Evaluator::OptionalLength(eval) => {
+ let v = match eval(context) {
+ Some(v) => v,
+ None => return KleeneValue::Unknown,
+ };
+ self.kind
+ .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
+ },
+ Evaluator::Integer(eval) => {
+ let v = eval(context);
+ self.kind.evaluate(v, |v| *expect!(Integer, v))
+ },
+ Evaluator::Float(eval) => {
+ let v = eval(context);
+ self.kind.evaluate(v, |v| *expect!(Float, v))
+ },
+ Evaluator::NumberRatio(eval) => {
+ let ratio = eval(context);
+ // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value()
+ // to convert it if necessary.
+ // FIXME: we may need to update here once
+ // https://github.com/w3c/csswg-drafts/issues/4954 got resolved.
+ self.kind
+ .evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
+ },
+ Evaluator::OptionalNumberRatio(eval) => {
+ let ratio = match eval(context) {
+ Some(v) => v,
+ None => return KleeneValue::Unknown,
+ };
+ // See above for subtleties here.
+ self.kind
+ .evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
+ },
+ Evaluator::Resolution(eval) => {
+ let v = eval(context).dppx();
+ self.kind.evaluate(v, |v| {
+ expect!(Resolution, v).to_computed_value(context).dppx()
+ })
+ },
+ Evaluator::Enumerated { evaluator, .. } => {
+ let computed = self
+ .kind
+ .non_ranged_value()
+ .map(|v| *expect!(Enumerated, v));
+ return evaluator(context, computed);
+ },
+ Evaluator::String(evaluator) => {
+ let string = self.kind.non_ranged_value().map(|v| expect!(String, v));
+ return evaluator(context, string);
+ },
+ Evaluator::BoolInteger(eval) => {
+ let computed = self
+ .kind
+ .non_ranged_value()
+ .map(|v| *expect!(BoolInteger, v));
+ let boolean = eval(context);
+ computed.map_or(boolean, |v| v == boolean)
+ },
+ })
+ }
+}
+
+/// A value found or expected in a expression.
+///
+/// FIXME(emilio): How should calc() serialize in the Number / Integer /
+/// BoolInteger / NumberRatio case, as computed or as specified value?
+///
+/// If the first, this would need to store the relevant values.
+///
+/// See: https://github.com/w3c/csswg-drafts/issues/1968
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum QueryExpressionValue {
+ /// A length.
+ Length(Length),
+ /// An integer.
+ Integer(i32),
+ /// A floating point value.
+ Float(CSSFloat),
+ /// A boolean value, specified as an integer (i.e., either 0 or 1).
+ BoolInteger(bool),
+ /// A single non-negative number or two non-negative numbers separated by '/',
+ /// with optional whitespace on either side of the '/'.
+ NumberRatio(Ratio),
+ /// A resolution.
+ Resolution(Resolution),
+ /// An enumerated value, defined by the variant keyword table in the
+ /// feature's `mData` member.
+ Enumerated(KeywordDiscriminant),
+ /// An arbitrary ident value.
+ String(AtomString),
+}
+
+impl QueryExpressionValue {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, for_expr: &QueryFeatureExpression) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ QueryExpressionValue::Length(ref l) => l.to_css(dest),
+ QueryExpressionValue::Integer(v) => v.to_css(dest),
+ QueryExpressionValue::Float(v) => v.to_css(dest),
+ QueryExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }),
+ QueryExpressionValue::NumberRatio(ratio) => ratio.to_css(dest),
+ QueryExpressionValue::Resolution(ref r) => r.to_css(dest),
+ QueryExpressionValue::Enumerated(value) => match for_expr.feature().evaluator {
+ Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)),
+ _ => unreachable!(),
+ },
+ QueryExpressionValue::String(ref s) => s.to_css(dest),
+ }
+ }
+
+ fn parse<'i, 't>(
+ for_feature: &QueryFeatureDescription,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<QueryExpressionValue, ParseError<'i>> {
+ Ok(match for_feature.evaluator {
+ Evaluator::OptionalLength(..) | Evaluator::Length(..) => {
+ let length = Length::parse(context, input)?;
+ QueryExpressionValue::Length(length)
+ },
+ Evaluator::Integer(..) => {
+ let integer = Integer::parse(context, input)?;
+ QueryExpressionValue::Integer(integer.value())
+ },
+ Evaluator::BoolInteger(..) => {
+ let integer = Integer::parse_non_negative(context, input)?;
+ let value = integer.value();
+ if value > 1 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ QueryExpressionValue::BoolInteger(value == 1)
+ },
+ Evaluator::Float(..) => {
+ let number = Number::parse(context, input)?;
+ QueryExpressionValue::Float(number.get())
+ },
+ Evaluator::OptionalNumberRatio(..) | Evaluator::NumberRatio(..) => {
+ use crate::values::specified::Ratio as SpecifiedRatio;
+ let ratio = SpecifiedRatio::parse(context, input)?;
+ QueryExpressionValue::NumberRatio(Ratio::new(ratio.0.get(), ratio.1.get()))
+ },
+ Evaluator::Resolution(..) => {
+ QueryExpressionValue::Resolution(Resolution::parse(context, input)?)
+ },
+ Evaluator::String(..) => {
+ QueryExpressionValue::String(input.expect_string()?.as_ref().into())
+ },
+ Evaluator::Enumerated { parser, .. } => {
+ QueryExpressionValue::Enumerated(parser(context, input)?)
+ },
+ })
+ }
+}
diff --git a/servo/components/style/queries/mod.rs b/servo/components/style/queries/mod.rs
new file mode 100644
index 0000000000..ec11ab3721
--- /dev/null
+++ b/servo/components/style/queries/mod.rs
@@ -0,0 +1,19 @@
+/* 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/. */
+
+//! Code shared between [media queries][mq] and [container queries][cq].
+//!
+//! [mq]: https://drafts.csswg.org/mediaqueries/
+//! [cq]: https://drafts.csswg.org/css-contain-3/#container-rule
+
+pub mod condition;
+
+#[macro_use]
+pub mod feature;
+pub mod feature_expression;
+pub mod values;
+
+pub use self::condition::QueryCondition;
+pub use self::feature::FeatureFlags;
+pub use self::feature_expression::{FeatureType, QueryFeatureExpression};
diff --git a/servo/components/style/queries/values.rs b/servo/components/style/queries/values.rs
new file mode 100644
index 0000000000..f4934408c4
--- /dev/null
+++ b/servo/components/style/queries/values.rs
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Common feature values between media and container features.
+
+use app_units::Au;
+use euclid::default::Size2D;
+
+/// The orientation media / container feature.
+/// https://drafts.csswg.org/mediaqueries-5/#orientation
+/// https://drafts.csswg.org/css-contain-3/#orientation
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum Orientation {
+ Portrait,
+ Landscape,
+}
+
+impl Orientation {
+ /// A helper to evaluate a orientation query given a generic size getter.
+ pub fn eval(size: Size2D<Au>, value: Option<Self>) -> bool {
+ let query_orientation = match value {
+ Some(v) => v,
+ None => return true,
+ };
+
+ // Per spec, square viewports should be 'portrait'
+ let is_landscape = size.width > size.height;
+ match query_orientation {
+ Self::Landscape => is_landscape,
+ Self::Portrait => !is_landscape,
+ }
+ }
+}
diff --git a/servo/components/style/rule_cache.rs b/servo/components/style/rule_cache.rs
new file mode 100644
index 0000000000..70c5b79731
--- /dev/null
+++ b/servo/components/style/rule_cache.rs
@@ -0,0 +1,219 @@
+/* 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/. */
+
+//! A cache from rule node to computed values, in order to cache reset
+//! properties.
+
+use crate::logical_geometry::WritingMode;
+use crate::properties::{ComputedValues, StyleBuilder};
+use crate::rule_tree::StrongRuleNode;
+use crate::selector_parser::PseudoElement;
+use crate::shared_lock::StylesheetGuards;
+use crate::values::computed::{NonNegativeLength, Zoom};
+use fxhash::FxHashMap;
+use servo_arc::Arc;
+use smallvec::SmallVec;
+
+/// The conditions for caching and matching a style in the rule cache.
+#[derive(Clone, Debug, Default)]
+pub struct RuleCacheConditions {
+ uncacheable: bool,
+ font_size: Option<NonNegativeLength>,
+ line_height: Option<NonNegativeLength>,
+ writing_mode: Option<WritingMode>,
+}
+
+impl RuleCacheConditions {
+ /// Sets the style as depending in the font-size value.
+ pub fn set_font_size_dependency(&mut self, font_size: NonNegativeLength) {
+ debug_assert!(self.font_size.map_or(true, |f| f == font_size));
+ self.font_size = Some(font_size);
+ }
+
+ /// Sets the style as depending in the line-height value.
+ pub fn set_line_height_dependency(&mut self, line_height: NonNegativeLength) {
+ debug_assert!(self.line_height.map_or(true, |l| l == line_height));
+ self.line_height = Some(line_height);
+ }
+
+ /// Sets the style as uncacheable.
+ pub fn set_uncacheable(&mut self) {
+ self.uncacheable = true;
+ }
+
+ /// Sets the style as depending in the writing-mode value `writing_mode`.
+ pub fn set_writing_mode_dependency(&mut self, writing_mode: WritingMode) {
+ debug_assert!(self.writing_mode.map_or(true, |wm| wm == writing_mode));
+ self.writing_mode = Some(writing_mode);
+ }
+
+ /// Returns whether the current style's reset properties are cacheable.
+ fn cacheable(&self) -> bool {
+ !self.uncacheable
+ }
+}
+
+#[derive(Debug)]
+struct CachedConditions {
+ font_size: Option<NonNegativeLength>,
+ line_height: Option<NonNegativeLength>,
+ writing_mode: Option<WritingMode>,
+ zoom: Zoom,
+}
+
+impl CachedConditions {
+ /// Returns whether `style` matches the conditions.
+ fn matches(&self, style: &StyleBuilder) -> bool {
+ if style.effective_zoom != self.zoom {
+ return false;
+ }
+
+ if let Some(fs) = self.font_size {
+ if style.get_font().clone_font_size().computed_size != fs {
+ return false;
+ }
+ }
+
+ if let Some(lh) = self.line_height {
+ let new_line_height =
+ style
+ .device
+ .calc_line_height(&style.get_font(), style.writing_mode, None);
+ if new_line_height != lh {
+ return false;
+ }
+ }
+
+ if let Some(wm) = self.writing_mode {
+ if style.writing_mode != wm {
+ return false;
+ }
+ }
+
+ true
+ }
+}
+
+/// A TLS cache from rules matched to computed values.
+pub struct RuleCache {
+ // FIXME(emilio): Consider using LRUCache or something like that?
+ map: FxHashMap<StrongRuleNode, SmallVec<[(CachedConditions, Arc<ComputedValues>); 1]>>,
+}
+
+impl RuleCache {
+ /// Creates an empty `RuleCache`.
+ pub fn new() -> Self {
+ Self {
+ map: FxHashMap::default(),
+ }
+ }
+
+ /// Walk the rule tree and return a rule node for using as the key
+ /// for rule cache.
+ ///
+ /// It currently skips a rule node when it is neither from a style
+ /// rule, nor containing any declaration of reset property. We don't
+ /// skip style rule so that we don't need to walk a long way in the
+ /// worst case. Skipping declarations rule nodes should be enough
+ /// to address common cases that rule cache would fail to share
+ /// when using the rule node directly, like preshint, style attrs,
+ /// and animations.
+ fn get_rule_node_for_cache<'r>(
+ guards: &StylesheetGuards,
+ mut rule_node: Option<&'r StrongRuleNode>,
+ ) -> Option<&'r StrongRuleNode> {
+ while let Some(node) = rule_node {
+ match node.style_source() {
+ Some(s) => match s.as_declarations() {
+ Some(decls) => {
+ let cascade_level = node.cascade_level();
+ let decls = decls.read_with(cascade_level.guard(guards));
+ if decls.contains_any_reset() {
+ break;
+ }
+ },
+ None => break,
+ },
+ None => {},
+ }
+ rule_node = node.parent();
+ }
+ rule_node
+ }
+
+ /// Finds a node in the properties matched cache.
+ ///
+ /// This needs to receive a `StyleBuilder` with the `early` properties
+ /// already applied.
+ pub fn find(
+ &self,
+ guards: &StylesheetGuards,
+ builder_with_early_props: &StyleBuilder,
+ ) -> Option<&ComputedValues> {
+ // A pseudo-element with property restrictions can result in different
+ // computed values if it's also used for a non-pseudo.
+ if builder_with_early_props
+ .pseudo
+ .and_then(|p| p.property_restriction())
+ .is_some()
+ {
+ return None;
+ }
+
+ let rules = builder_with_early_props.rules.as_ref();
+ let rules = Self::get_rule_node_for_cache(guards, rules)?;
+ let cached_values = self.map.get(rules)?;
+
+ for &(ref conditions, ref values) in cached_values.iter() {
+ if conditions.matches(builder_with_early_props) {
+ debug!("Using cached reset style with conditions {:?}", conditions);
+ return Some(&**values);
+ }
+ }
+ None
+ }
+
+ /// Inserts a node into the rules cache if possible.
+ ///
+ /// Returns whether the style was inserted into the cache.
+ pub fn insert_if_possible(
+ &mut self,
+ guards: &StylesheetGuards,
+ style: &Arc<ComputedValues>,
+ pseudo: Option<&PseudoElement>,
+ conditions: &RuleCacheConditions,
+ ) -> bool {
+ if !conditions.cacheable() {
+ return false;
+ }
+
+ // A pseudo-element with property restrictions can result in different
+ // computed values if it's also used for a non-pseudo.
+ if pseudo.and_then(|p| p.property_restriction()).is_some() {
+ return false;
+ }
+
+ let rules = style.rules.as_ref();
+ let rules = match Self::get_rule_node_for_cache(guards, rules) {
+ Some(r) => r.clone(),
+ None => return false,
+ };
+
+ debug!(
+ "Inserting cached reset style with conditions {:?}",
+ conditions
+ );
+ let cached_conditions = CachedConditions {
+ writing_mode: conditions.writing_mode,
+ font_size: conditions.font_size,
+ line_height: conditions.line_height,
+ zoom: style.effective_zoom,
+ };
+ self.map
+ .entry(rules)
+ .or_default()
+ .push((cached_conditions, style.clone()));
+ true
+ }
+}
diff --git a/servo/components/style/rule_collector.rs b/servo/components/style/rule_collector.rs
new file mode 100644
index 0000000000..058d682317
--- /dev/null
+++ b/servo/components/style/rule_collector.rs
@@ -0,0 +1,505 @@
+/* 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/. */
+
+//! Collects a series of applicable rules for a given element.
+
+use crate::applicable_declarations::{ApplicableDeclarationBlock, ApplicableDeclarationList};
+use crate::dom::{TElement, TNode, TShadowRoot};
+use crate::properties::{AnimationDeclarations, PropertyDeclarationBlock};
+use crate::rule_tree::{CascadeLevel, ShadowCascadeOrder};
+use crate::selector_map::SelectorMap;
+use crate::selector_parser::PseudoElement;
+use crate::shared_lock::Locked;
+use crate::stylesheets::{layer_rule::LayerOrder, Origin};
+use crate::stylist::{AuthorStylesEnabled, CascadeData, Rule, RuleInclusion, Stylist};
+use selectors::matching::{MatchingContext, MatchingMode};
+use servo_arc::ArcBorrow;
+use smallvec::SmallVec;
+
+/// This is a bit of a hack so <svg:use> matches the rules of the enclosing
+/// tree.
+///
+/// This function returns the containing shadow host ignoring <svg:use> shadow
+/// trees, since those match the enclosing tree's rules.
+///
+/// Only a handful of places need to really care about this. This is not a
+/// problem for invalidation and that kind of stuff because they still don't
+/// match rules based on elements outside of the shadow tree, and because the
+/// <svg:use> subtrees are immutable and recreated each time the source tree
+/// changes.
+///
+/// We historically allow cross-document <svg:use> to have these rules applied,
+/// but I think that's not great. Gecko is the only engine supporting that.
+///
+/// See https://github.com/w3c/svgwg/issues/504 for the relevant spec
+/// discussion.
+#[inline]
+pub fn containing_shadow_ignoring_svg_use<E: TElement>(
+ element: E,
+) -> Option<<E::ConcreteNode as TNode>::ConcreteShadowRoot> {
+ let mut shadow = element.containing_shadow()?;
+ loop {
+ let host = shadow.host();
+ let host_is_svg_use_element =
+ host.is_svg_element() && host.local_name() == &**local_name!("use");
+ if !host_is_svg_use_element {
+ return Some(shadow);
+ }
+ debug_assert!(
+ shadow.style_data().is_none(),
+ "We allow no stylesheets in <svg:use> subtrees"
+ );
+ shadow = host.containing_shadow()?;
+ }
+}
+
+/// An object that we use with all the intermediate state needed for the
+/// cascade.
+///
+/// This is done basically to be able to organize the cascade in smaller
+/// functions, and be able to reason about it easily.
+pub struct RuleCollector<'a, 'b: 'a, E>
+where
+ E: TElement,
+{
+ element: E,
+ rule_hash_target: E,
+ stylist: &'a Stylist,
+ pseudo_element: Option<&'a PseudoElement>,
+ style_attribute: Option<ArcBorrow<'a, Locked<PropertyDeclarationBlock>>>,
+ smil_override: Option<ArcBorrow<'a, Locked<PropertyDeclarationBlock>>>,
+ animation_declarations: AnimationDeclarations,
+ rule_inclusion: RuleInclusion,
+ rules: &'a mut ApplicableDeclarationList,
+ context: &'a mut MatchingContext<'b, E::Impl>,
+ matches_user_and_content_rules: bool,
+ matches_document_author_rules: bool,
+ in_sort_scope: bool,
+}
+
+impl<'a, 'b: 'a, E> RuleCollector<'a, 'b, E>
+where
+ E: TElement,
+{
+ /// Trivially construct a new collector.
+ pub fn new(
+ stylist: &'a Stylist,
+ element: E,
+ pseudo_element: Option<&'a PseudoElement>,
+ style_attribute: Option<ArcBorrow<'a, Locked<PropertyDeclarationBlock>>>,
+ smil_override: Option<ArcBorrow<'a, Locked<PropertyDeclarationBlock>>>,
+ animation_declarations: AnimationDeclarations,
+ rule_inclusion: RuleInclusion,
+ rules: &'a mut ApplicableDeclarationList,
+ context: &'a mut MatchingContext<'b, E::Impl>,
+ ) -> Self {
+ // When we're matching with matching_mode =
+ // `ForStatelessPseudoeElement`, the "target" for the rule hash is the
+ // element itself, since it's what's generating the pseudo-element.
+ let rule_hash_target = match context.matching_mode() {
+ MatchingMode::ForStatelessPseudoElement => element,
+ MatchingMode::Normal => element.rule_hash_target(),
+ };
+
+ let matches_user_and_content_rules = rule_hash_target.matches_user_and_content_rules();
+
+ // Gecko definitely has pseudo-elements with style attributes, like
+ // ::-moz-color-swatch.
+ debug_assert!(
+ cfg!(feature = "gecko") || style_attribute.is_none() || pseudo_element.is_none(),
+ "Style attributes do not apply to pseudo-elements"
+ );
+ debug_assert!(pseudo_element.map_or(true, |p| !p.is_precomputed()));
+
+ Self {
+ element,
+ rule_hash_target,
+ stylist,
+ pseudo_element,
+ style_attribute,
+ smil_override,
+ animation_declarations,
+ rule_inclusion,
+ context,
+ rules,
+ matches_user_and_content_rules,
+ matches_document_author_rules: matches_user_and_content_rules,
+ in_sort_scope: false,
+ }
+ }
+
+ /// Sets up the state necessary to collect rules from a given DOM tree
+ /// (either the document tree, or a shadow tree).
+ ///
+ /// All rules in the same tree need to be matched together, and this
+ /// function takes care of sorting them by specificity and source order.
+ #[inline]
+ fn in_tree(&mut self, host: Option<E>, f: impl FnOnce(&mut Self)) {
+ debug_assert!(!self.in_sort_scope, "Nested sorting makes no sense");
+ let start = self.rules.len();
+ self.in_sort_scope = true;
+ let old_host = self.context.current_host.take();
+ self.context.current_host = host.map(|e| e.opaque());
+ f(self);
+ if start != self.rules.len() {
+ self.rules[start..].sort_unstable_by_key(|block| {
+ (block.layer_order(), block.specificity, block.source_order())
+ });
+ }
+ self.context.current_host = old_host;
+ self.in_sort_scope = false;
+ }
+
+ #[inline]
+ fn in_shadow_tree(&mut self, host: E, f: impl FnOnce(&mut Self)) {
+ self.in_tree(Some(host), f);
+ }
+
+ fn collect_stylist_rules(&mut self, origin: Origin) {
+ let cascade_level = match origin {
+ Origin::UserAgent => CascadeLevel::UANormal,
+ Origin::User => CascadeLevel::UserNormal,
+ Origin::Author => CascadeLevel::same_tree_author_normal(),
+ };
+
+ let cascade_data = self.stylist.cascade_data().borrow_for_origin(origin);
+ let map = match cascade_data.normal_rules(self.pseudo_element) {
+ Some(m) => m,
+ None => return,
+ };
+
+ self.in_tree(None, |collector| {
+ collector.collect_rules_in_map(map, cascade_level, cascade_data);
+ });
+ }
+
+ fn collect_user_agent_rules(&mut self) {
+ self.collect_stylist_rules(Origin::UserAgent);
+ }
+
+ fn collect_user_rules(&mut self) {
+ if !self.matches_user_and_content_rules {
+ return;
+ }
+
+ self.collect_stylist_rules(Origin::User);
+ }
+
+ /// Presentational hints.
+ ///
+ /// These go before author rules, but after user rules, see:
+ /// https://drafts.csswg.org/css-cascade/#preshint
+ fn collect_presentational_hints(&mut self) {
+ if self.pseudo_element.is_some() {
+ return;
+ }
+
+ let length_before_preshints = self.rules.len();
+ self.element
+ .synthesize_presentational_hints_for_legacy_attributes(
+ self.context.visited_handling(),
+ self.rules,
+ );
+ if cfg!(debug_assertions) {
+ if self.rules.len() != length_before_preshints {
+ for declaration in &self.rules[length_before_preshints..] {
+ assert_eq!(declaration.level(), CascadeLevel::PresHints);
+ }
+ }
+ }
+ }
+
+ #[inline]
+ fn collect_rules_in_list(
+ &mut self,
+ part_rules: &[Rule],
+ cascade_level: CascadeLevel,
+ cascade_data: &CascadeData,
+ ) {
+ debug_assert!(self.in_sort_scope, "Rules gotta be sorted");
+ SelectorMap::get_matching_rules(
+ self.element,
+ part_rules,
+ &mut self.rules,
+ &mut self.context,
+ cascade_level,
+ cascade_data,
+ &self.stylist,
+ );
+ }
+
+ #[inline]
+ fn collect_rules_in_map(
+ &mut self,
+ map: &SelectorMap<Rule>,
+ cascade_level: CascadeLevel,
+ cascade_data: &CascadeData,
+ ) {
+ debug_assert!(self.in_sort_scope, "Rules gotta be sorted");
+ map.get_all_matching_rules(
+ self.element,
+ self.rule_hash_target,
+ &mut self.rules,
+ &mut self.context,
+ cascade_level,
+ cascade_data,
+ &self.stylist,
+ );
+ }
+
+ /// Collects the rules for the ::slotted pseudo-element and the :host
+ /// pseudo-class.
+ fn collect_host_and_slotted_rules(&mut self) {
+ let mut slots = SmallVec::<[_; 3]>::new();
+ let mut current = self.rule_hash_target.assigned_slot();
+ let mut shadow_cascade_order = ShadowCascadeOrder::for_outermost_shadow_tree();
+
+ while let Some(slot) = current {
+ debug_assert!(
+ self.matches_user_and_content_rules,
+ "We should not slot NAC anywhere"
+ );
+ slots.push(slot);
+ current = slot.assigned_slot();
+ shadow_cascade_order.dec();
+ }
+
+ self.collect_host_rules(shadow_cascade_order);
+
+ // Match slotted rules in reverse order, so that the outer slotted rules
+ // come before the inner rules (and thus have less priority).
+ for slot in slots.iter().rev() {
+ shadow_cascade_order.inc();
+
+ let shadow = slot.containing_shadow().unwrap();
+ let data = match shadow.style_data() {
+ Some(d) => d,
+ None => continue,
+ };
+ let slotted_rules = match data.slotted_rules(self.pseudo_element) {
+ Some(r) => r,
+ None => continue,
+ };
+
+ self.in_shadow_tree(shadow.host(), |collector| {
+ let cascade_level = CascadeLevel::AuthorNormal {
+ shadow_cascade_order,
+ };
+ collector.collect_rules_in_map(slotted_rules, cascade_level, data);
+ });
+ }
+ }
+
+ fn collect_rules_from_containing_shadow_tree(&mut self) {
+ if !self.matches_user_and_content_rules {
+ return;
+ }
+
+ let containing_shadow = containing_shadow_ignoring_svg_use(self.rule_hash_target);
+ let containing_shadow = match containing_shadow {
+ Some(s) => s,
+ None => return,
+ };
+
+ self.matches_document_author_rules = false;
+
+ let cascade_data = match containing_shadow.style_data() {
+ Some(c) => c,
+ None => return,
+ };
+
+ let cascade_level = CascadeLevel::same_tree_author_normal();
+ self.in_shadow_tree(containing_shadow.host(), |collector| {
+ if let Some(map) = cascade_data.normal_rules(collector.pseudo_element) {
+ collector.collect_rules_in_map(map, cascade_level, cascade_data);
+ }
+
+ // Collect rules from :host::part() and such
+ let hash_target = collector.rule_hash_target;
+ if !hash_target.has_part_attr() {
+ return;
+ }
+
+ let part_rules = match cascade_data.part_rules(collector.pseudo_element) {
+ Some(p) => p,
+ None => return,
+ };
+
+ hash_target.each_part(|part| {
+ if let Some(part_rules) = part_rules.get(&part.0) {
+ collector.collect_rules_in_list(part_rules, cascade_level, cascade_data);
+ }
+ });
+ });
+ }
+
+ /// Collects the rules for the :host pseudo-class.
+ fn collect_host_rules(&mut self, shadow_cascade_order: ShadowCascadeOrder) {
+ let shadow = match self.rule_hash_target.shadow_root() {
+ Some(s) => s,
+ None => return,
+ };
+
+ let style_data = match shadow.style_data() {
+ Some(d) => d,
+ None => return,
+ };
+
+ let host_rules = match style_data.host_rules(self.pseudo_element) {
+ Some(rules) => rules,
+ None => return,
+ };
+
+ let rule_hash_target = self.rule_hash_target;
+ self.in_shadow_tree(rule_hash_target, |collector| {
+ let cascade_level = CascadeLevel::AuthorNormal {
+ shadow_cascade_order,
+ };
+ collector.collect_rules_in_map(host_rules, cascade_level, style_data);
+ });
+ }
+
+ fn collect_document_author_rules(&mut self) {
+ if !self.matches_document_author_rules {
+ return;
+ }
+
+ self.collect_stylist_rules(Origin::Author);
+ }
+
+ fn collect_part_rules_from_outer_trees(&mut self) {
+ if !self.rule_hash_target.has_part_attr() {
+ return;
+ }
+
+ let mut inner_shadow = match self.rule_hash_target.containing_shadow() {
+ Some(s) => s,
+ None => return,
+ };
+
+ let mut shadow_cascade_order = ShadowCascadeOrder::for_innermost_containing_tree();
+
+ let mut parts = SmallVec::<[_; 3]>::new();
+ self.rule_hash_target.each_part(|p| parts.push(p.clone()));
+
+ loop {
+ if parts.is_empty() {
+ return;
+ }
+
+ let inner_shadow_host = inner_shadow.host();
+ let outer_shadow = inner_shadow_host.containing_shadow();
+ let cascade_data = match outer_shadow {
+ Some(shadow) => shadow.style_data(),
+ None => Some(
+ self.stylist
+ .cascade_data()
+ .borrow_for_origin(Origin::Author),
+ ),
+ };
+
+ if let Some(cascade_data) = cascade_data {
+ if let Some(part_rules) = cascade_data.part_rules(self.pseudo_element) {
+ let containing_host = outer_shadow.map(|s| s.host());
+ let cascade_level = CascadeLevel::AuthorNormal {
+ shadow_cascade_order,
+ };
+ self.in_tree(containing_host, |collector| {
+ for p in &parts {
+ if let Some(part_rules) = part_rules.get(&p.0) {
+ collector.collect_rules_in_list(
+ part_rules,
+ cascade_level,
+ cascade_data,
+ );
+ }
+ }
+ });
+ shadow_cascade_order.inc();
+ }
+ }
+
+ inner_shadow = match outer_shadow {
+ Some(s) => s,
+ None => break, // Nowhere to export to.
+ };
+
+ let mut new_parts = SmallVec::new();
+ for part in &parts {
+ inner_shadow_host.each_exported_part(part, |exported_part| {
+ new_parts.push(exported_part.clone());
+ });
+ }
+ parts = new_parts;
+ }
+ }
+
+ fn collect_style_attribute(&mut self) {
+ if let Some(sa) = self.style_attribute {
+ self.rules
+ .push(ApplicableDeclarationBlock::from_declarations(
+ sa.clone_arc(),
+ CascadeLevel::same_tree_author_normal(),
+ LayerOrder::style_attribute(),
+ ));
+ }
+ }
+
+ fn collect_animation_rules(&mut self) {
+ if let Some(so) = self.smil_override {
+ self.rules
+ .push(ApplicableDeclarationBlock::from_declarations(
+ so.clone_arc(),
+ CascadeLevel::SMILOverride,
+ LayerOrder::root(),
+ ));
+ }
+
+ // The animations sheet (CSS animations, script-generated
+ // animations, and CSS transitions that are no longer tied to CSS
+ // markup).
+ if let Some(anim) = self.animation_declarations.animations.take() {
+ self.rules
+ .push(ApplicableDeclarationBlock::from_declarations(
+ anim,
+ CascadeLevel::Animations,
+ LayerOrder::root(),
+ ));
+ }
+
+ // The transitions sheet (CSS transitions that are tied to CSS
+ // markup).
+ if let Some(anim) = self.animation_declarations.transitions.take() {
+ self.rules
+ .push(ApplicableDeclarationBlock::from_declarations(
+ anim,
+ CascadeLevel::Transitions,
+ LayerOrder::root(),
+ ));
+ }
+ }
+
+ /// Collects all the rules, leaving the result in `self.rules`.
+ ///
+ /// Note that `!important` rules are handled during rule tree insertion.
+ pub fn collect_all(mut self) {
+ self.collect_user_agent_rules();
+ self.collect_user_rules();
+ if self.rule_inclusion == RuleInclusion::DefaultOnly {
+ return;
+ }
+ self.collect_presentational_hints();
+ // FIXME(emilio): Should the author styles enabled stuff avoid the
+ // presentational hints from getting pushed? See bug 1505770.
+ if self.stylist.author_styles_enabled() == AuthorStylesEnabled::No {
+ return;
+ }
+ self.collect_host_and_slotted_rules();
+ self.collect_rules_from_containing_shadow_tree();
+ self.collect_document_author_rules();
+ self.collect_style_attribute();
+ self.collect_part_rules_from_outer_trees();
+ self.collect_animation_rules();
+ }
+}
diff --git a/servo/components/style/rule_tree/core.rs b/servo/components/style/rule_tree/core.rs
new file mode 100644
index 0000000000..85584a0e22
--- /dev/null
+++ b/servo/components/style/rule_tree/core.rs
@@ -0,0 +1,772 @@
+/* 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/. */
+
+#![allow(unsafe_code)]
+
+use crate::applicable_declarations::CascadePriority;
+use crate::shared_lock::StylesheetGuards;
+use crate::stylesheets::layer_rule::LayerOrder;
+use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps};
+use parking_lot::RwLock;
+use smallvec::SmallVec;
+use std::fmt;
+use std::hash;
+use std::io::Write;
+use std::mem;
+use std::ptr;
+use std::sync::atomic::{self, AtomicPtr, AtomicUsize, Ordering};
+
+use super::map::{Entry, Map};
+use super::unsafe_box::UnsafeBox;
+use super::{CascadeLevel, StyleSource};
+
+/// The rule tree, the structure servo uses to preserve the results of selector
+/// matching.
+///
+/// This is organized as a tree of rules. When a node matches a set of rules,
+/// they're inserted in order in the tree, starting with the less specific one.
+///
+/// When a rule is inserted in the tree, other elements may share the path up to
+/// a given rule. If that's the case, we don't duplicate child nodes, but share
+/// them.
+///
+/// When the rule node refcount drops to zero, it doesn't get freed. It gets
+/// instead put into a free list, and it is potentially GC'd after a while.
+///
+/// That way, a rule node that represents a likely-to-match-again rule (like a
+/// :hover rule) can be reused if we haven't GC'd it yet.
+#[derive(Debug)]
+pub struct RuleTree {
+ root: StrongRuleNode,
+}
+
+impl Drop for RuleTree {
+ fn drop(&mut self) {
+ unsafe { self.swap_free_list_and_gc(ptr::null_mut()) }
+ }
+}
+
+impl MallocSizeOf for RuleTree {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = 0;
+ let mut stack = SmallVec::<[_; 32]>::new();
+ stack.push(self.root.clone());
+
+ while let Some(node) = stack.pop() {
+ n += unsafe { ops.malloc_size_of(&*node.p) };
+ let children = node.p.children.read();
+ children.shallow_size_of(ops);
+ for c in &*children {
+ stack.push(unsafe { c.upgrade() });
+ }
+ }
+
+ n
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+struct ChildKey(CascadePriority, ptr::NonNull<()>);
+unsafe impl Send for ChildKey {}
+unsafe impl Sync for ChildKey {}
+
+impl RuleTree {
+ /// Construct a new rule tree.
+ pub fn new() -> Self {
+ RuleTree {
+ root: StrongRuleNode::new(Box::new(RuleNode::root())),
+ }
+ }
+
+ /// Get the root rule node.
+ pub fn root(&self) -> &StrongRuleNode {
+ &self.root
+ }
+
+ /// This can only be called when no other threads is accessing this tree.
+ pub fn gc(&self) {
+ unsafe { self.swap_free_list_and_gc(RuleNode::DANGLING_PTR) }
+ }
+
+ /// This can only be called when no other threads is accessing this tree.
+ pub fn maybe_gc(&self) {
+ #[cfg(debug_assertions)]
+ self.maybe_dump_stats();
+
+ if self.root.p.approximate_free_count.load(Ordering::Relaxed) > RULE_TREE_GC_INTERVAL {
+ self.gc();
+ }
+ }
+
+ #[cfg(debug_assertions)]
+ fn maybe_dump_stats(&self) {
+ use itertools::Itertools;
+ use std::cell::Cell;
+ use std::time::{Duration, Instant};
+
+ if !log_enabled!(log::Level::Trace) {
+ return;
+ }
+
+ const RULE_TREE_STATS_INTERVAL: Duration = Duration::from_secs(2);
+
+ thread_local! {
+ pub static LAST_STATS: Cell<Instant> = Cell::new(Instant::now());
+ };
+
+ let should_dump = LAST_STATS.with(|s| {
+ let now = Instant::now();
+ if now.duration_since(s.get()) < RULE_TREE_STATS_INTERVAL {
+ return false;
+ }
+ s.set(now);
+ true
+ });
+
+ if !should_dump {
+ return;
+ }
+
+ let mut children_count = fxhash::FxHashMap::default();
+
+ let mut stack = SmallVec::<[_; 32]>::new();
+ stack.push(self.root.clone());
+ while let Some(node) = stack.pop() {
+ let children = node.p.children.read();
+ *children_count.entry(children.len()).or_insert(0) += 1;
+ for c in &*children {
+ stack.push(unsafe { c.upgrade() });
+ }
+ }
+
+ trace!("Rule tree stats:");
+ let counts = children_count.keys().sorted();
+ for count in counts {
+ trace!(" {} - {}", count, children_count[count]);
+ }
+ }
+
+ /// Steals the free list and drops its contents.
+ unsafe fn swap_free_list_and_gc(&self, ptr: *mut RuleNode) {
+ let root = &self.root.p;
+
+ debug_assert!(!root.next_free.load(Ordering::Relaxed).is_null());
+
+ // Reset the approximate free count to zero, as we are going to steal
+ // the free list.
+ root.approximate_free_count.store(0, Ordering::Relaxed);
+
+ // Steal the free list head. Memory loads on nodes while iterating it
+ // must observe any prior changes that occured so this requires
+ // acquire ordering, but there are no writes that need to be kept
+ // before this swap so there is no need for release.
+ let mut head = root.next_free.swap(ptr, Ordering::Acquire);
+
+ while head != RuleNode::DANGLING_PTR {
+ debug_assert!(!head.is_null());
+
+ let mut node = UnsafeBox::from_raw(head);
+
+ // The root node cannot go on the free list.
+ debug_assert!(node.root.is_some());
+
+ // The refcount of nodes on the free list never goes below 1.
+ debug_assert!(node.refcount.load(Ordering::Relaxed) > 0);
+
+ // No one else is currently writing to that field. Get the address
+ // of the next node in the free list and replace it with null,
+ // other threads will now consider that this node is not on the
+ // free list.
+ head = node.next_free.swap(ptr::null_mut(), Ordering::Relaxed);
+
+ // This release write synchronises with the acquire fence in
+ // `WeakRuleNode::upgrade`, making sure that if `upgrade` observes
+ // decrements the refcount to 0, it will also observe the
+ // `node.next_free` swap to null above.
+ if node.refcount.fetch_sub(1, Ordering::Release) == 1 {
+ // And given it observed the null swap above, it will need
+ // `pretend_to_be_on_free_list` to finish its job, writing
+ // `RuleNode::DANGLING_PTR` in `node.next_free`.
+ RuleNode::pretend_to_be_on_free_list(&node);
+
+ // Drop this node now that we just observed its refcount going
+ // down to zero.
+ RuleNode::drop_without_free_list(&mut node);
+ }
+ }
+ }
+}
+
+/// The number of RuleNodes added to the free list before we will consider
+/// doing a GC when calling maybe_gc(). (The value is copied from Gecko,
+/// where it likely did not result from a rigorous performance analysis.)
+const RULE_TREE_GC_INTERVAL: usize = 300;
+
+/// A node in the rule tree.
+struct RuleNode {
+ /// The root node. Only the root has no root pointer, for obvious reasons.
+ root: Option<WeakRuleNode>,
+
+ /// The parent rule node. Only the root has no parent.
+ parent: Option<StrongRuleNode>,
+
+ /// The actual style source, either coming from a selector in a StyleRule,
+ /// or a raw property declaration block (like the style attribute).
+ ///
+ /// None for the root node.
+ source: Option<StyleSource>,
+
+ /// The cascade level + layer order this rule is positioned at.
+ cascade_priority: CascadePriority,
+
+ /// The refcount of this node.
+ ///
+ /// Starts at one. Incremented in `StrongRuleNode::clone` and
+ /// `WeakRuleNode::upgrade`. Decremented in `StrongRuleNode::drop`
+ /// and `RuleTree::swap_free_list_and_gc`.
+ ///
+ /// If a non-root node's refcount reaches zero, it is incremented back to at
+ /// least one in `RuleNode::pretend_to_be_on_free_list` until the caller who
+ /// observed it dropping to zero had a chance to try to remove it from its
+ /// parent's children list.
+ ///
+ /// The refcount should never be decremented to zero if the value in
+ /// `next_free` is not null.
+ refcount: AtomicUsize,
+
+ /// Only used for the root, stores the number of free rule nodes that are
+ /// around.
+ approximate_free_count: AtomicUsize,
+
+ /// The children of a given rule node. Children remove themselves from here
+ /// when they go away.
+ children: RwLock<Map<ChildKey, WeakRuleNode>>,
+
+ /// This field has two different meanings depending on whether this is the
+ /// root node or not.
+ ///
+ /// If it is the root, it represents the head of the free list. It may be
+ /// null, which means the free list is gone because the tree was dropped,
+ /// and it may be `RuleNode::DANGLING_PTR`, which means the free list is
+ /// empty.
+ ///
+ /// If it is not the root node, this field is either null if the node is
+ /// not on the free list, `RuleNode::DANGLING_PTR` if it is the last item
+ /// on the free list or the node is pretending to be on the free list, or
+ /// any valid non-null pointer representing the next item on the free list
+ /// after this one.
+ ///
+ /// See `RuleNode::push_on_free_list`, `swap_free_list_and_gc`, and
+ /// `WeakRuleNode::upgrade`.
+ ///
+ /// Two threads should never attempt to put the same node on the free list
+ /// both at the same time.
+ next_free: AtomicPtr<RuleNode>,
+}
+
+// On Gecko builds, hook into the leak checking machinery.
+#[cfg(feature = "gecko_refcount_logging")]
+mod gecko_leak_checking {
+ use super::RuleNode;
+ use std::mem::size_of;
+ use std::os::raw::{c_char, c_void};
+
+ extern "C" {
+ fn NS_LogCtor(aPtr: *mut c_void, aTypeName: *const c_char, aSize: u32);
+ fn NS_LogDtor(aPtr: *mut c_void, aTypeName: *const c_char, aSize: u32);
+ }
+ static NAME: &'static [u8] = b"RuleNode\0";
+
+ /// Logs the creation of a heap-allocated object to Gecko's leak-checking machinery.
+ pub(super) fn log_ctor(ptr: *const RuleNode) {
+ let s = NAME as *const [u8] as *const u8 as *const c_char;
+ unsafe {
+ NS_LogCtor(ptr as *mut c_void, s, size_of::<RuleNode>() as u32);
+ }
+ }
+
+ /// Logs the destruction of a heap-allocated object to Gecko's leak-checking machinery.
+ pub(super) fn log_dtor(ptr: *const RuleNode) {
+ let s = NAME as *const [u8] as *const u8 as *const c_char;
+ unsafe {
+ NS_LogDtor(ptr as *mut c_void, s, size_of::<RuleNode>() as u32);
+ }
+ }
+}
+
+#[inline(always)]
+fn log_new(_ptr: *const RuleNode) {
+ #[cfg(feature = "gecko_refcount_logging")]
+ gecko_leak_checking::log_ctor(_ptr);
+}
+
+#[inline(always)]
+fn log_drop(_ptr: *const RuleNode) {
+ #[cfg(feature = "gecko_refcount_logging")]
+ gecko_leak_checking::log_dtor(_ptr);
+}
+
+impl RuleNode {
+ const DANGLING_PTR: *mut Self = ptr::NonNull::dangling().as_ptr();
+
+ unsafe fn new(
+ root: WeakRuleNode,
+ parent: StrongRuleNode,
+ source: StyleSource,
+ cascade_priority: CascadePriority,
+ ) -> Self {
+ debug_assert!(root.p.parent.is_none());
+ RuleNode {
+ root: Some(root),
+ parent: Some(parent),
+ source: Some(source),
+ cascade_priority,
+ refcount: AtomicUsize::new(1),
+ children: Default::default(),
+ approximate_free_count: AtomicUsize::new(0),
+ next_free: AtomicPtr::new(ptr::null_mut()),
+ }
+ }
+
+ fn root() -> Self {
+ RuleNode {
+ root: None,
+ parent: None,
+ source: None,
+ cascade_priority: CascadePriority::new(CascadeLevel::UANormal, LayerOrder::root()),
+ refcount: AtomicUsize::new(1),
+ approximate_free_count: AtomicUsize::new(0),
+ children: Default::default(),
+ next_free: AtomicPtr::new(RuleNode::DANGLING_PTR),
+ }
+ }
+
+ fn key(&self) -> ChildKey {
+ ChildKey(
+ self.cascade_priority,
+ self.source
+ .as_ref()
+ .expect("Called key() on the root node")
+ .key(),
+ )
+ }
+
+ /// Drops a node without ever putting it on the free list.
+ ///
+ /// Note that the node may not be dropped if we observe that its refcount
+ /// isn't zero anymore when we write-lock its parent's children map to
+ /// remove it.
+ ///
+ /// This loops over parents of dropped nodes if their own refcount reaches
+ /// zero to avoid recursion when dropping deep hierarchies of nodes.
+ ///
+ /// For non-root nodes, this should always be preceded by a call of
+ /// `RuleNode::pretend_to_be_on_free_list`.
+ unsafe fn drop_without_free_list(this: &mut UnsafeBox<Self>) {
+ // We clone the box and shadow the original one to be able to loop
+ // over its ancestors if they also need to be dropped.
+ let mut this = UnsafeBox::clone(this);
+ loop {
+ // If the node has a parent, we need to remove it from its parent's
+ // children list.
+ if let Some(parent) = this.parent.as_ref() {
+ debug_assert!(!this.next_free.load(Ordering::Relaxed).is_null());
+
+ // We lock the parent's children list, which means no other
+ // thread will have any more opportunity to resurrect the node
+ // anymore.
+ let mut children = parent.p.children.write();
+
+ this.next_free.store(ptr::null_mut(), Ordering::Relaxed);
+
+ // We decrement the counter to remove the "pretend to be
+ // on the free list" reference.
+ let old_refcount = this.refcount.fetch_sub(1, Ordering::Release);
+ debug_assert!(old_refcount != 0);
+ if old_refcount != 1 {
+ // Other threads resurrected this node and those references
+ // are still alive, we have nothing to do anymore.
+ return;
+ }
+
+ // We finally remove the node from its parent's children list,
+ // there are now no other references to it and it cannot
+ // be resurrected anymore even after we unlock the list.
+ debug!(
+ "Remove from child list: {:?}, parent: {:?}",
+ this.as_mut_ptr(),
+ this.parent.as_ref().map(|p| p.p.as_mut_ptr())
+ );
+ let weak = children.remove(&this.key(), |node| node.p.key()).unwrap();
+ assert_eq!(weak.p.as_mut_ptr(), this.as_mut_ptr());
+ } else {
+ debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut());
+ debug_assert_eq!(this.refcount.load(Ordering::Relaxed), 0);
+ }
+
+ // We are going to drop this node for good this time, as per the
+ // usual refcounting protocol we need an acquire fence here before
+ // we run the destructor.
+ //
+ // See https://github.com/rust-lang/rust/pull/41714#issuecomment-298996916
+ // for why it doesn't matter whether this is a load or a fence.
+ atomic::fence(Ordering::Acquire);
+
+ // Remove the parent reference from the child to avoid
+ // recursively dropping it and putting it on the free list.
+ let parent = UnsafeBox::deref_mut(&mut this).parent.take();
+
+ // We now drop the actual box and its contents, no one should
+ // access the current value in `this` anymore.
+ log_drop(&*this);
+ UnsafeBox::drop(&mut this);
+
+ if let Some(parent) = parent {
+ // We will attempt to drop the node's parent without the free
+ // list, so we clone the inner unsafe box and forget the
+ // original parent to avoid running its `StrongRuleNode`
+ // destructor which would attempt to use the free list if it
+ // still exists.
+ this = UnsafeBox::clone(&parent.p);
+ mem::forget(parent);
+ if this.refcount.fetch_sub(1, Ordering::Release) == 1 {
+ debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut());
+ if this.root.is_some() {
+ RuleNode::pretend_to_be_on_free_list(&this);
+ }
+ // Parent also reached refcount zero, we loop to drop it.
+ continue;
+ }
+ }
+
+ return;
+ }
+ }
+
+ /// Pushes this node on the tree's free list. Returns false if the free list
+ /// is gone. Should only be called after we decremented a node's refcount
+ /// to zero and pretended to be on the free list.
+ unsafe fn push_on_free_list(this: &UnsafeBox<Self>) -> bool {
+ let root = &this.root.as_ref().unwrap().p;
+
+ debug_assert!(this.refcount.load(Ordering::Relaxed) > 0);
+ debug_assert_eq!(this.next_free.load(Ordering::Relaxed), Self::DANGLING_PTR);
+
+ // Increment the approximate free count by one.
+ root.approximate_free_count.fetch_add(1, Ordering::Relaxed);
+
+ // If the compare-exchange operation fails in the loop, we will retry
+ // with the new head value, so this can be a relaxed load.
+ let mut head = root.next_free.load(Ordering::Relaxed);
+
+ while !head.is_null() {
+ // Two threads can never attempt to push the same node on the free
+ // list both at the same time, so whoever else pushed a node on the
+ // free list cannot have done so with this node.
+ debug_assert_ne!(head, this.as_mut_ptr());
+
+ // Store the current head of the free list in this node.
+ this.next_free.store(head, Ordering::Relaxed);
+
+ // Any thread acquiring the free list must observe the previous
+ // next_free changes that occured, hence the release ordering
+ // on success.
+ match root.next_free.compare_exchange_weak(
+ head,
+ this.as_mut_ptr(),
+ Ordering::Release,
+ Ordering::Relaxed,
+ ) {
+ Ok(_) => {
+ // This node is now on the free list, caller should not use
+ // the node anymore.
+ return true;
+ },
+ Err(new_head) => head = new_head,
+ }
+ }
+
+ // Tree was dropped and free list has been destroyed. We did not push
+ // this node on the free list but we still pretend to be on the free
+ // list to be ready to call `drop_without_free_list`.
+ false
+ }
+
+ /// Makes the node pretend to be on the free list. This will increment the
+ /// refcount by 1 and store `Self::DANGLING_PTR` in `next_free`. This
+ /// method should only be called after caller decremented the refcount to
+ /// zero, with the null pointer stored in `next_free`.
+ unsafe fn pretend_to_be_on_free_list(this: &UnsafeBox<Self>) {
+ debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut());
+ this.refcount.fetch_add(1, Ordering::Relaxed);
+ this.next_free.store(Self::DANGLING_PTR, Ordering::Release);
+ }
+
+ fn as_mut_ptr(&self) -> *mut RuleNode {
+ self as *const RuleNode as *mut RuleNode
+ }
+}
+
+pub(crate) struct WeakRuleNode {
+ p: UnsafeBox<RuleNode>,
+}
+
+/// A strong reference to a rule node.
+pub struct StrongRuleNode {
+ p: UnsafeBox<RuleNode>,
+}
+
+#[cfg(feature = "servo")]
+malloc_size_of_is_0!(StrongRuleNode);
+
+impl StrongRuleNode {
+ fn new(n: Box<RuleNode>) -> Self {
+ debug_assert_eq!(n.parent.is_none(), !n.source.is_some());
+
+ log_new(&*n);
+
+ debug!("Creating rule node: {:p}", &*n);
+
+ Self {
+ p: UnsafeBox::from_box(n),
+ }
+ }
+
+ unsafe fn from_unsafe_box(p: UnsafeBox<RuleNode>) -> Self {
+ Self { p }
+ }
+
+ unsafe fn downgrade(&self) -> WeakRuleNode {
+ WeakRuleNode {
+ p: UnsafeBox::clone(&self.p),
+ }
+ }
+
+ /// Get the parent rule node of this rule node.
+ pub fn parent(&self) -> Option<&StrongRuleNode> {
+ self.p.parent.as_ref()
+ }
+
+ pub(super) fn ensure_child(
+ &self,
+ root: &StrongRuleNode,
+ source: StyleSource,
+ cascade_priority: CascadePriority,
+ ) -> StrongRuleNode {
+ use parking_lot::RwLockUpgradableReadGuard;
+
+ debug_assert!(
+ self.p.cascade_priority <= cascade_priority,
+ "Should be ordered (instead {:?} > {:?}), from {:?} and {:?}",
+ self.p.cascade_priority,
+ cascade_priority,
+ self.p.source,
+ source,
+ );
+
+ let key = ChildKey(cascade_priority, source.key());
+ let children = self.p.children.upgradable_read();
+ if let Some(child) = children.get(&key, |node| node.p.key()) {
+ // Sound to call because we read-locked the parent's children.
+ return unsafe { child.upgrade() };
+ }
+ let mut children = RwLockUpgradableReadGuard::upgrade(children);
+ match children.entry(key, |node| node.p.key()) {
+ Entry::Occupied(child) => {
+ // Sound to call because we write-locked the parent's children.
+ unsafe { child.upgrade() }
+ },
+ Entry::Vacant(entry) => unsafe {
+ let node = StrongRuleNode::new(Box::new(RuleNode::new(
+ root.downgrade(),
+ self.clone(),
+ source,
+ cascade_priority,
+ )));
+ // Sound to call because we still own a strong reference to
+ // this node, through the `node` variable itself that we are
+ // going to return to the caller.
+ entry.insert(node.downgrade());
+ node
+ },
+ }
+ }
+
+ /// Get the style source corresponding to this rule node. May return `None`
+ /// if it's the root node, which means that the node hasn't matched any
+ /// rules.
+ pub fn style_source(&self) -> Option<&StyleSource> {
+ self.p.source.as_ref()
+ }
+
+ /// The cascade priority.
+ #[inline]
+ pub fn cascade_priority(&self) -> CascadePriority {
+ self.p.cascade_priority
+ }
+
+ /// The cascade level.
+ #[inline]
+ pub fn cascade_level(&self) -> CascadeLevel {
+ self.cascade_priority().cascade_level()
+ }
+
+ /// The importance.
+ #[inline]
+ pub fn importance(&self) -> crate::properties::Importance {
+ self.cascade_level().importance()
+ }
+
+ /// Returns whether this node has any child, only intended for testing
+ /// purposes.
+ pub unsafe fn has_children_for_testing(&self) -> bool {
+ !self.p.children.read().is_empty()
+ }
+
+ pub(super) fn dump<W: Write>(&self, guards: &StylesheetGuards, writer: &mut W, indent: usize) {
+ const INDENT_INCREMENT: usize = 4;
+
+ for _ in 0..indent {
+ let _ = write!(writer, " ");
+ }
+
+ let _ = writeln!(
+ writer,
+ " - {:p} (ref: {:?}, parent: {:?})",
+ &*self.p,
+ self.p.refcount.load(Ordering::Relaxed),
+ self.parent().map(|p| &*p.p as *const RuleNode)
+ );
+
+ for _ in 0..indent {
+ let _ = write!(writer, " ");
+ }
+
+ if let Some(source) = self.style_source() {
+ source.dump(self.cascade_level().guard(guards), writer);
+ } else {
+ if indent != 0 {
+ warn!("How has this happened?");
+ }
+ let _ = write!(writer, "(root)");
+ }
+
+ let _ = write!(writer, "\n");
+ for child in &*self.p.children.read() {
+ unsafe {
+ child
+ .upgrade()
+ .dump(guards, writer, indent + INDENT_INCREMENT);
+ }
+ }
+ }
+}
+
+impl Clone for StrongRuleNode {
+ fn clone(&self) -> Self {
+ debug!(
+ "{:p}: {:?}+",
+ &*self.p,
+ self.p.refcount.load(Ordering::Relaxed)
+ );
+ debug_assert!(self.p.refcount.load(Ordering::Relaxed) > 0);
+ self.p.refcount.fetch_add(1, Ordering::Relaxed);
+ unsafe { StrongRuleNode::from_unsafe_box(UnsafeBox::clone(&self.p)) }
+ }
+}
+
+impl Drop for StrongRuleNode {
+ #[cfg_attr(feature = "servo", allow(unused_mut))]
+ fn drop(&mut self) {
+ let node = &*self.p;
+ debug!("{:p}: {:?}-", node, node.refcount.load(Ordering::Relaxed));
+ debug!(
+ "Dropping node: {:p}, root: {:?}, parent: {:?}",
+ node,
+ node.root.as_ref().map(|r| &*r.p as *const RuleNode),
+ node.parent.as_ref().map(|p| &*p.p as *const RuleNode)
+ );
+
+ let should_drop = {
+ debug_assert!(node.refcount.load(Ordering::Relaxed) > 0);
+ node.refcount.fetch_sub(1, Ordering::Release) == 1
+ };
+
+ if !should_drop {
+ // The refcount didn't even drop zero yet, there is nothing for us
+ // to do anymore.
+ return;
+ }
+
+ unsafe {
+ if node.root.is_some() {
+ // This is a non-root node and we just observed the refcount
+ // dropping to zero, we need to pretend to be on the free list
+ // to unstuck any thread who tried to resurrect this node first
+ // through `WeakRuleNode::upgrade`.
+ RuleNode::pretend_to_be_on_free_list(&self.p);
+
+ // Attempt to push the node on the free list. This may fail
+ // if the free list is gone.
+ if RuleNode::push_on_free_list(&self.p) {
+ return;
+ }
+ }
+
+ // Either this was the last reference of the root node, or the
+ // tree rule is gone and there is no free list anymore. Drop the
+ // node.
+ RuleNode::drop_without_free_list(&mut self.p);
+ }
+ }
+}
+
+impl WeakRuleNode {
+ /// Upgrades this weak node reference, returning a strong one.
+ ///
+ /// Must be called with items stored in a node's children list. The children
+ /// list must at least be read-locked when this is called.
+ unsafe fn upgrade(&self) -> StrongRuleNode {
+ debug!("Upgrading weak node: {:p}", &*self.p);
+
+ if self.p.refcount.fetch_add(1, Ordering::Relaxed) == 0 {
+ // We observed a refcount of 0, we need to wait for this node to
+ // be put on the free list. Resetting the `next_free` pointer to
+ // null is only done in `RuleNode::drop_without_free_list`, just
+ // before a release refcount decrement, so this acquire fence here
+ // makes sure that we observed the write to null before we loop
+ // until there is a non-null value.
+ atomic::fence(Ordering::Acquire);
+ while self.p.next_free.load(Ordering::Relaxed).is_null() {}
+ }
+ StrongRuleNode::from_unsafe_box(UnsafeBox::clone(&self.p))
+ }
+}
+
+impl fmt::Debug for StrongRuleNode {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ (&*self.p as *const RuleNode).fmt(f)
+ }
+}
+
+impl Eq for StrongRuleNode {}
+impl PartialEq for StrongRuleNode {
+ fn eq(&self, other: &Self) -> bool {
+ &*self.p as *const RuleNode == &*other.p
+ }
+}
+
+impl hash::Hash for StrongRuleNode {
+ fn hash<H>(&self, state: &mut H)
+ where
+ H: hash::Hasher,
+ {
+ (&*self.p as *const RuleNode).hash(state)
+ }
+}
+
+// Large pages generate thousands of RuleNode objects.
+size_of_test!(RuleNode, 80);
+// StrongRuleNode should be pointer-sized even inside an option.
+size_of_test!(Option<StrongRuleNode>, 8);
diff --git a/servo/components/style/rule_tree/level.rs b/servo/components/style/rule_tree/level.rs
new file mode 100644
index 0000000000..b8cbe55ed9
--- /dev/null
+++ b/servo/components/style/rule_tree/level.rs
@@ -0,0 +1,249 @@
+/* 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/. */
+
+#![forbid(unsafe_code)]
+
+use crate::properties::Importance;
+use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards};
+use crate::stylesheets::Origin;
+
+/// The cascade level these rules are relevant at, as per[1][2][3].
+///
+/// Presentational hints for SVG and HTML are in the "author-level
+/// zero-specificity" level, that is, right after user rules, and before author
+/// rules.
+///
+/// The order of variants declared here is significant, and must be in
+/// _ascending_ order of precedence.
+///
+/// See also [4] for the Shadow DOM bits. We rely on the invariant that rules
+/// from outside the tree the element is in can't affect the element.
+///
+/// The opposite is not true (i.e., :host and ::slotted) from an "inner" shadow
+/// tree may affect an element connected to the document or an "outer" shadow
+/// tree.
+///
+/// [1]: https://drafts.csswg.org/css-cascade/#cascade-origin
+/// [2]: https://drafts.csswg.org/css-cascade/#preshint
+/// [3]: https://html.spec.whatwg.org/multipage/#presentational-hints
+/// [4]: https://drafts.csswg.org/css-scoping/#shadow-cascading
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Ord, PartialEq, PartialOrd)]
+pub enum CascadeLevel {
+ /// Normal User-Agent rules.
+ UANormal,
+ /// User normal rules.
+ UserNormal,
+ /// Presentational hints.
+ PresHints,
+ /// Shadow DOM styles from author styles.
+ AuthorNormal {
+ /// The order in the shadow tree hierarchy. This number is relative to
+ /// the tree of the element, and thus the only invariants that need to
+ /// be preserved is:
+ ///
+ /// * Zero is the same tree as the element that matched the rule. This
+ /// is important so that we can optimize style attribute insertions.
+ ///
+ /// * The levels are ordered in accordance with
+ /// https://drafts.csswg.org/css-scoping/#shadow-cascading
+ shadow_cascade_order: ShadowCascadeOrder,
+ },
+ /// SVG SMIL animations.
+ SMILOverride,
+ /// CSS animations and script-generated animations.
+ Animations,
+ /// Author-supplied important rules.
+ AuthorImportant {
+ /// The order in the shadow tree hierarchy, inverted, so that PartialOrd
+ /// does the right thing.
+ shadow_cascade_order: ShadowCascadeOrder,
+ },
+ /// User important rules.
+ UserImportant,
+ /// User-agent important rules.
+ UAImportant,
+ /// Transitions
+ Transitions,
+}
+
+impl CascadeLevel {
+ /// Convert this level from "unimportant" to "important".
+ pub fn important(&self) -> Self {
+ match *self {
+ Self::UANormal => Self::UAImportant,
+ Self::UserNormal => Self::UserImportant,
+ Self::AuthorNormal {
+ shadow_cascade_order,
+ } => Self::AuthorImportant {
+ shadow_cascade_order: -shadow_cascade_order,
+ },
+ Self::PresHints |
+ Self::SMILOverride |
+ Self::Animations |
+ Self::AuthorImportant { .. } |
+ Self::UserImportant |
+ Self::UAImportant |
+ Self::Transitions => *self,
+ }
+ }
+
+ /// Convert this level from "important" to "non-important".
+ pub fn unimportant(&self) -> Self {
+ match *self {
+ Self::UAImportant => Self::UANormal,
+ Self::UserImportant => Self::UserNormal,
+ Self::AuthorImportant {
+ shadow_cascade_order,
+ } => Self::AuthorNormal {
+ shadow_cascade_order: -shadow_cascade_order,
+ },
+ Self::PresHints |
+ Self::SMILOverride |
+ Self::Animations |
+ Self::AuthorNormal { .. } |
+ Self::UserNormal |
+ Self::UANormal |
+ Self::Transitions => *self,
+ }
+ }
+
+ /// Select a lock guard for this level
+ pub fn guard<'a>(&self, guards: &'a StylesheetGuards<'a>) -> &'a SharedRwLockReadGuard<'a> {
+ match *self {
+ Self::UANormal | Self::UserNormal | Self::UserImportant | Self::UAImportant => {
+ guards.ua_or_user
+ },
+ _ => guards.author,
+ }
+ }
+
+ /// Returns the cascade level for author important declarations from the
+ /// same tree as the element.
+ #[inline]
+ pub fn same_tree_author_important() -> Self {
+ Self::AuthorImportant {
+ shadow_cascade_order: ShadowCascadeOrder::for_same_tree(),
+ }
+ }
+
+ /// Returns the cascade level for author normal declarations from the same
+ /// tree as the element.
+ #[inline]
+ pub fn same_tree_author_normal() -> Self {
+ Self::AuthorNormal {
+ shadow_cascade_order: ShadowCascadeOrder::for_same_tree(),
+ }
+ }
+
+ /// Returns whether this cascade level represents important rules of some
+ /// sort.
+ #[inline]
+ pub fn is_important(&self) -> bool {
+ match *self {
+ Self::AuthorImportant { .. } | Self::UserImportant | Self::UAImportant => true,
+ _ => false,
+ }
+ }
+
+ /// Returns the importance relevant for this rule. Pretty similar to
+ /// `is_important`.
+ #[inline]
+ pub fn importance(&self) -> Importance {
+ if self.is_important() {
+ Importance::Important
+ } else {
+ Importance::Normal
+ }
+ }
+
+ /// Returns the cascade origin of the rule.
+ #[inline]
+ pub fn origin(&self) -> Origin {
+ match *self {
+ Self::UAImportant | Self::UANormal => Origin::UserAgent,
+ Self::UserImportant | Self::UserNormal => Origin::User,
+ Self::PresHints |
+ Self::AuthorNormal { .. } |
+ Self::AuthorImportant { .. } |
+ Self::SMILOverride |
+ Self::Animations |
+ Self::Transitions => Origin::Author,
+ }
+ }
+
+ /// Returns whether this cascade level represents an animation rules.
+ #[inline]
+ pub fn is_animation(&self) -> bool {
+ match *self {
+ Self::SMILOverride | Self::Animations | Self::Transitions => true,
+ _ => false,
+ }
+ }
+}
+
+/// A counter to track how many shadow root rules deep we are. This is used to
+/// handle:
+///
+/// https://drafts.csswg.org/css-scoping/#shadow-cascading
+///
+/// See the static functions for the meaning of different values.
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Ord, PartialEq, PartialOrd)]
+pub struct ShadowCascadeOrder(i8);
+
+impl ShadowCascadeOrder {
+ /// We keep a maximum of 3 bits of order as a limit so that we can pack
+ /// CascadeLevel in one byte by using half of it for the order, if that ends
+ /// up being necessary.
+ const MAX: i8 = 0b111;
+ const MIN: i8 = -Self::MAX;
+
+ /// A level for the outermost shadow tree (the shadow tree we own, and the
+ /// ones from the slots we're slotted in).
+ #[inline]
+ pub fn for_outermost_shadow_tree() -> Self {
+ Self(-1)
+ }
+
+ /// A level for the element's tree.
+ #[inline]
+ fn for_same_tree() -> Self {
+ Self(0)
+ }
+
+ /// A level for the innermost containing tree (the one closest to the
+ /// element).
+ #[inline]
+ pub fn for_innermost_containing_tree() -> Self {
+ Self(1)
+ }
+
+ /// Decrement the level, moving inwards. We should only move inwards if
+ /// we're traversing slots.
+ #[inline]
+ pub fn dec(&mut self) {
+ debug_assert!(self.0 < 0);
+ if self.0 != Self::MIN {
+ self.0 -= 1;
+ }
+ }
+
+ /// The level, moving inwards. We should only move inwards if we're
+ /// traversing slots.
+ #[inline]
+ pub fn inc(&mut self) {
+ debug_assert_ne!(self.0, -1);
+ if self.0 != Self::MAX {
+ self.0 += 1;
+ }
+ }
+}
+
+impl std::ops::Neg for ShadowCascadeOrder {
+ type Output = Self;
+ #[inline]
+ fn neg(self) -> Self {
+ Self(self.0.neg())
+ }
+}
diff --git a/servo/components/style/rule_tree/map.rs b/servo/components/style/rule_tree/map.rs
new file mode 100644
index 0000000000..33c470e9c1
--- /dev/null
+++ b/servo/components/style/rule_tree/map.rs
@@ -0,0 +1,201 @@
+/* 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/. */
+
+#![forbid(unsafe_code)]
+
+use fxhash::FxHashMap;
+use malloc_size_of::{MallocShallowSizeOf, MallocSizeOfOps};
+use std::collections::hash_map;
+use std::hash::Hash;
+use std::mem;
+
+pub(super) struct Map<K, V> {
+ inner: MapInner<K, V>,
+}
+
+enum MapInner<K, V> {
+ Empty,
+ One(V),
+ Map(Box<FxHashMap<K, V>>),
+}
+
+pub(super) struct MapIter<'a, K, V> {
+ inner: MapIterInner<'a, K, V>,
+}
+
+enum MapIterInner<'a, K, V> {
+ One(std::option::IntoIter<&'a V>),
+ Map(std::collections::hash_map::Values<'a, K, V>),
+}
+
+pub(super) enum Entry<'a, K, V> {
+ Occupied(&'a mut V),
+ Vacant(VacantEntry<'a, K, V>),
+}
+
+pub(super) struct VacantEntry<'a, K, V> {
+ inner: VacantEntryInner<'a, K, V>,
+}
+
+enum VacantEntryInner<'a, K, V> {
+ One(&'a mut MapInner<K, V>),
+ Map(hash_map::VacantEntry<'a, K, V>),
+}
+
+impl<K, V> Default for Map<K, V> {
+ fn default() -> Self {
+ Map {
+ inner: MapInner::Empty,
+ }
+ }
+}
+
+impl<'a, K, V> IntoIterator for &'a Map<K, V> {
+ type Item = &'a V;
+ type IntoIter = MapIter<'a, K, V>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ MapIter {
+ inner: match &self.inner {
+ MapInner::Empty => MapIterInner::One(None.into_iter()),
+ MapInner::One(one) => MapIterInner::One(Some(one).into_iter()),
+ MapInner::Map(map) => MapIterInner::Map(map.values()),
+ },
+ }
+ }
+}
+
+impl<'a, K, V> Iterator for MapIter<'a, K, V> {
+ type Item = &'a V;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ match &mut self.inner {
+ MapIterInner::One(one_iter) => one_iter.next(),
+ MapIterInner::Map(map_iter) => map_iter.next(),
+ }
+ }
+}
+
+impl<K, V> Map<K, V>
+where
+ K: Eq + Hash,
+{
+ pub(super) fn is_empty(&self) -> bool {
+ match &self.inner {
+ MapInner::Empty => true,
+ MapInner::One(_) => false,
+ MapInner::Map(map) => map.is_empty(),
+ }
+ }
+
+ #[cfg(debug_assertions)]
+ pub(super) fn len(&self) -> usize {
+ match &self.inner {
+ MapInner::Empty => 0,
+ MapInner::One(_) => 1,
+ MapInner::Map(map) => map.len(),
+ }
+ }
+
+ pub(super) fn get(&self, key: &K, key_from_value: impl FnOnce(&V) -> K) -> Option<&V> {
+ match &self.inner {
+ MapInner::One(one) if *key == key_from_value(one) => Some(one),
+ MapInner::Map(map) => map.get(key),
+ MapInner::Empty | MapInner::One(_) => None,
+ }
+ }
+
+ pub(super) fn entry(
+ &mut self,
+ key: K,
+ key_from_value: impl FnOnce(&V) -> K,
+ ) -> Entry<'_, K, V> {
+ match self.inner {
+ ref mut inner @ MapInner::Empty => Entry::Vacant(VacantEntry {
+ inner: VacantEntryInner::One(inner),
+ }),
+ MapInner::One(_) => {
+ let one = match mem::replace(&mut self.inner, MapInner::Empty) {
+ MapInner::One(one) => one,
+ _ => unreachable!(),
+ };
+ // If this panics, the child `one` will be lost.
+ let one_key = key_from_value(&one);
+ // Same for the equality test.
+ if key == one_key {
+ self.inner = MapInner::One(one);
+ let one = match &mut self.inner {
+ MapInner::One(one) => one,
+ _ => unreachable!(),
+ };
+ return Entry::Occupied(one);
+ }
+ self.inner = MapInner::Map(Box::new(FxHashMap::with_capacity_and_hasher(
+ 2,
+ Default::default(),
+ )));
+ let map = match &mut self.inner {
+ MapInner::Map(map) => map,
+ _ => unreachable!(),
+ };
+ map.insert(one_key, one);
+ match map.entry(key) {
+ hash_map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry {
+ inner: VacantEntryInner::Map(entry),
+ }),
+ _ => unreachable!(),
+ }
+ },
+ MapInner::Map(ref mut map) => match map.entry(key) {
+ hash_map::Entry::Occupied(entry) => Entry::Occupied(entry.into_mut()),
+ hash_map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry {
+ inner: VacantEntryInner::Map(entry),
+ }),
+ },
+ }
+ }
+
+ pub(super) fn remove(&mut self, key: &K, key_from_value: impl FnOnce(&V) -> K) -> Option<V> {
+ match &mut self.inner {
+ MapInner::One(one) if *key == key_from_value(one) => {
+ match mem::replace(&mut self.inner, MapInner::Empty) {
+ MapInner::One(one) => Some(one),
+ _ => unreachable!(),
+ }
+ },
+ MapInner::Map(map) => map.remove(key),
+ MapInner::Empty | MapInner::One(_) => None,
+ }
+ }
+}
+
+impl<'a, K, V> VacantEntry<'a, K, V> {
+ pub(super) fn insert(self, value: V) -> &'a mut V {
+ match self.inner {
+ VacantEntryInner::One(map) => {
+ *map = MapInner::One(value);
+ match map {
+ MapInner::One(one) => one,
+ _ => unreachable!(),
+ }
+ },
+ VacantEntryInner::Map(entry) => entry.insert(value),
+ }
+ }
+}
+
+impl<K, V> MallocShallowSizeOf for Map<K, V>
+where
+ K: Eq + Hash,
+{
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match &self.inner {
+ MapInner::Map(m) => {
+ // We want to account for both the box and the hashmap.
+ m.shallow_size_of(ops) + (**m).shallow_size_of(ops)
+ },
+ MapInner::One(_) | MapInner::Empty => 0,
+ }
+ }
+}
diff --git a/servo/components/style/rule_tree/mod.rs b/servo/components/style/rule_tree/mod.rs
new file mode 100644
index 0000000000..18ee018d64
--- /dev/null
+++ b/servo/components/style/rule_tree/mod.rs
@@ -0,0 +1,403 @@
+/* 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/. */
+
+#![deny(unsafe_code)]
+
+//! The rule tree.
+
+use crate::applicable_declarations::{ApplicableDeclarationList, CascadePriority};
+use crate::properties::{LonghandIdSet, PropertyDeclarationBlock};
+use crate::shared_lock::{Locked, StylesheetGuards};
+use crate::stylesheets::layer_rule::LayerOrder;
+use servo_arc::ArcBorrow;
+use smallvec::SmallVec;
+use std::io::{self, Write};
+
+mod core;
+mod level;
+mod map;
+mod source;
+mod unsafe_box;
+
+pub use self::core::{RuleTree, StrongRuleNode};
+pub use self::level::{CascadeLevel, ShadowCascadeOrder};
+pub use self::source::StyleSource;
+
+impl RuleTree {
+ fn dump<W: Write>(&self, guards: &StylesheetGuards, writer: &mut W) {
+ let _ = writeln!(writer, " + RuleTree");
+ self.root().dump(guards, writer, 0);
+ }
+
+ /// Dump the rule tree to stdout.
+ pub fn dump_stdout(&self, guards: &StylesheetGuards) {
+ let mut stdout = io::stdout();
+ self.dump(guards, &mut stdout);
+ }
+
+ /// Inserts the given rules, that must be in proper order by specifity, and
+ /// returns the corresponding rule node representing the last inserted one.
+ ///
+ /// !important rules are detected and inserted into the appropriate position
+ /// in the rule tree. This allows selector matching to ignore importance,
+ /// while still maintaining the appropriate cascade order in the rule tree.
+ pub fn insert_ordered_rules_with_important<'a, I>(
+ &self,
+ iter: I,
+ guards: &StylesheetGuards,
+ ) -> StrongRuleNode
+ where
+ I: Iterator<Item = (StyleSource, CascadePriority)>,
+ {
+ use self::CascadeLevel::*;
+ let mut current = self.root().clone();
+
+ let mut found_important = false;
+
+ let mut important_author = SmallVec::<[(StyleSource, CascadePriority); 4]>::new();
+ let mut important_user = SmallVec::<[(StyleSource, CascadePriority); 4]>::new();
+ let mut important_ua = SmallVec::<[(StyleSource, CascadePriority); 4]>::new();
+ let mut transition = None;
+
+ for (source, priority) in iter {
+ let level = priority.cascade_level();
+ debug_assert!(!level.is_important(), "Important levels handled internally");
+
+ let any_important = {
+ let pdb = source.read(level.guard(guards));
+ pdb.any_important()
+ };
+
+ if any_important {
+ found_important = true;
+ match level {
+ AuthorNormal { .. } => {
+ important_author.push((source.clone(), priority.important()))
+ },
+ UANormal => important_ua.push((source.clone(), priority.important())),
+ UserNormal => important_user.push((source.clone(), priority.important())),
+ _ => {},
+ };
+ }
+
+ // We don't optimize out empty rules, even though we could.
+ //
+ // Inspector relies on every rule being inserted in the normal level
+ // at least once, in order to return the rules with the correct
+ // specificity order.
+ //
+ // TODO(emilio): If we want to apply these optimizations without
+ // breaking inspector's expectations, we'd need to run
+ // selector-matching again at the inspector's request. That may or
+ // may not be a better trade-off.
+ if matches!(level, Transitions) && found_important {
+ // There can be at most one transition, and it will come at
+ // the end of the iterator. Stash it and apply it after
+ // !important rules.
+ debug_assert!(transition.is_none());
+ transition = Some(source);
+ } else {
+ current = current.ensure_child(self.root(), source, priority);
+ }
+ }
+
+ // Early-return in the common case of no !important declarations.
+ if !found_important {
+ return current;
+ }
+
+ // Insert important declarations, in order of increasing importance,
+ // followed by any transition rule.
+ //
+ // Important rules are sorted differently from unimportant ones by
+ // shadow order and cascade order.
+ if !important_author.is_empty() &&
+ important_author.first().unwrap().1 != important_author.last().unwrap().1
+ {
+ // We only need to sort if the important rules come from
+ // different trees, but we need this sort to be stable.
+ //
+ // FIXME(emilio): This could maybe be smarter, probably by chunking
+ // the important rules while inserting, and iterating the outer
+ // chunks in reverse order.
+ //
+ // That is, if we have rules with levels like: -1 -1 -1 0 0 0 1 1 1,
+ // we're really only sorting the chunks, while keeping elements
+ // inside the same chunk already sorted. Seems like we could try to
+ // keep a SmallVec-of-SmallVecs with the chunks and just iterate the
+ // outer in reverse.
+ important_author.sort_by_key(|&(_, priority)| priority);
+ }
+
+ for (source, priority) in important_author.drain(..) {
+ current = current.ensure_child(self.root(), source, priority);
+ }
+
+ for (source, priority) in important_user.drain(..) {
+ current = current.ensure_child(self.root(), source, priority);
+ }
+
+ for (source, priority) in important_ua.drain(..) {
+ current = current.ensure_child(self.root(), source, priority);
+ }
+
+ if let Some(source) = transition {
+ current = current.ensure_child(
+ self.root(),
+ source,
+ CascadePriority::new(Transitions, LayerOrder::root()),
+ );
+ }
+
+ current
+ }
+
+ /// Given a list of applicable declarations, insert the rules and return the
+ /// corresponding rule node.
+ pub fn compute_rule_node(
+ &self,
+ applicable_declarations: &mut ApplicableDeclarationList,
+ guards: &StylesheetGuards,
+ ) -> StrongRuleNode {
+ self.insert_ordered_rules_with_important(
+ applicable_declarations.drain(..).map(|d| d.for_rule_tree()),
+ guards,
+ )
+ }
+
+ /// Insert the given rules, that must be in proper order by specifity, and
+ /// return the corresponding rule node representing the last inserted one.
+ pub fn insert_ordered_rules<'a, I>(&self, iter: I) -> StrongRuleNode
+ where
+ I: Iterator<Item = (StyleSource, CascadePriority)>,
+ {
+ self.insert_ordered_rules_from(self.root().clone(), iter)
+ }
+
+ fn insert_ordered_rules_from<'a, I>(&self, from: StrongRuleNode, iter: I) -> StrongRuleNode
+ where
+ I: Iterator<Item = (StyleSource, CascadePriority)>,
+ {
+ let mut current = from;
+ for (source, priority) in iter {
+ current = current.ensure_child(self.root(), source, priority);
+ }
+ current
+ }
+
+ /// Replaces a rule in a given level (if present) for another rule.
+ ///
+ /// Returns the resulting node that represents the new path, or None if
+ /// the old path is still valid.
+ pub fn update_rule_at_level(
+ &self,
+ level: CascadeLevel,
+ layer_order: LayerOrder,
+ pdb: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>,
+ path: &StrongRuleNode,
+ guards: &StylesheetGuards,
+ important_rules_changed: &mut bool,
+ ) -> Option<StrongRuleNode> {
+ // TODO(emilio): Being smarter with lifetimes we could avoid a bit of
+ // the refcount churn.
+ let mut current = path.clone();
+ *important_rules_changed = false;
+
+ // First walk up until the first less-or-equally specific rule.
+ let mut children = SmallVec::<[_; 10]>::new();
+ while current.cascade_priority().cascade_level() > level {
+ children.push((
+ current.style_source().unwrap().clone(),
+ current.cascade_priority(),
+ ));
+ current = current.parent().unwrap().clone();
+ }
+
+ // Then remove the one at the level we want to replace, if any.
+ //
+ // NOTE: Here we assume that only one rule can be at the level we're
+ // replacing.
+ //
+ // This is certainly true for HTML style attribute rules, animations and
+ // transitions, but could not be so for SMIL animations, which we'd need
+ // to special-case (isn't hard, it's just about removing the `if` and
+ // special cases, and replacing them for a `while` loop, avoiding the
+ // optimizations).
+ if current.cascade_priority().cascade_level() == level {
+ *important_rules_changed |= level.is_important();
+
+ let current_decls = current.style_source().unwrap().as_declarations();
+
+ // If the only rule at the level we're replacing is exactly the
+ // same as `pdb`, we're done, and `path` is still valid.
+ if let (Some(ref pdb), Some(ref current_decls)) = (pdb, current_decls) {
+ // If the only rule at the level we're replacing is exactly the
+ // same as `pdb`, we're done, and `path` is still valid.
+ //
+ // TODO(emilio): Another potential optimization is the one where
+ // we can just replace the rule at that level for `pdb`, and
+ // then we don't need to re-create the children, and `path` is
+ // also equally valid. This is less likely, and would require an
+ // in-place mutation of the source, which is, at best, fiddly,
+ // so let's skip it for now.
+ let is_here_already = ArcBorrow::ptr_eq(pdb, current_decls);
+ if is_here_already {
+ debug!("Picking the fast path in rule replacement");
+ return None;
+ }
+ }
+
+ if current_decls.is_some() {
+ current = current.parent().unwrap().clone();
+ }
+ }
+
+ // Insert the rule if it's relevant at this level in the cascade.
+ //
+ // These optimizations are likely to be important, because the levels
+ // where replacements apply (style and animations) tend to trigger
+ // pretty bad styling cases already.
+ if let Some(pdb) = pdb {
+ if level.is_important() {
+ if pdb.read_with(level.guard(guards)).any_important() {
+ current = current.ensure_child(
+ self.root(),
+ StyleSource::from_declarations(pdb.clone_arc()),
+ CascadePriority::new(level, layer_order),
+ );
+ *important_rules_changed = true;
+ }
+ } else {
+ if pdb.read_with(level.guard(guards)).any_normal() {
+ current = current.ensure_child(
+ self.root(),
+ StyleSource::from_declarations(pdb.clone_arc()),
+ CascadePriority::new(level, layer_order),
+ );
+ }
+ }
+ }
+
+ // Now the rule is in the relevant place, push the children as
+ // necessary.
+ let rule = self.insert_ordered_rules_from(current, children.drain(..).rev());
+ Some(rule)
+ }
+
+ /// Returns new rule nodes without Transitions level rule.
+ pub fn remove_transition_rule_if_applicable(&self, path: &StrongRuleNode) -> StrongRuleNode {
+ // Return a clone if there is no transition level.
+ if path.cascade_level() != CascadeLevel::Transitions {
+ return path.clone();
+ }
+
+ path.parent().unwrap().clone()
+ }
+
+ /// Returns new rule node without rules from declarative animations.
+ pub fn remove_animation_rules(&self, path: &StrongRuleNode) -> StrongRuleNode {
+ // Return a clone if there are no animation rules.
+ if !path.has_animation_or_transition_rules() {
+ return path.clone();
+ }
+
+ let iter = path
+ .self_and_ancestors()
+ .take_while(|node| node.cascade_level() >= CascadeLevel::SMILOverride);
+ let mut last = path;
+ let mut children = SmallVec::<[_; 10]>::new();
+ for node in iter {
+ if !node.cascade_level().is_animation() {
+ children.push((
+ node.style_source().unwrap().clone(),
+ node.cascade_priority(),
+ ));
+ }
+ last = node;
+ }
+
+ let rule = self
+ .insert_ordered_rules_from(last.parent().unwrap().clone(), children.drain(..).rev());
+ rule
+ }
+}
+
+impl StrongRuleNode {
+ /// Get an iterator for this rule node and its ancestors.
+ pub fn self_and_ancestors(&self) -> SelfAndAncestors {
+ SelfAndAncestors {
+ current: Some(self),
+ }
+ }
+
+ /// Returns true if there is either animation or transition level rule.
+ pub fn has_animation_or_transition_rules(&self) -> bool {
+ self.self_and_ancestors()
+ .take_while(|node| node.cascade_level() >= CascadeLevel::SMILOverride)
+ .any(|node| node.cascade_level().is_animation())
+ }
+
+ /// Get a set of properties whose CascadeLevel are higher than Animations
+ /// but not equal to Transitions.
+ ///
+ /// If there are any custom properties, we set the boolean value of the
+ /// returned tuple to true.
+ pub fn get_properties_overriding_animations(
+ &self,
+ guards: &StylesheetGuards,
+ ) -> (LonghandIdSet, bool) {
+ use crate::properties::PropertyDeclarationId;
+
+ // We want to iterate over cascade levels that override the animations
+ // level, i.e. !important levels and the transitions level.
+ //
+ // However, we actually want to skip the transitions level because
+ // although it is higher in the cascade than animations, when both
+ // transitions and animations are present for a given element and
+ // property, transitions are suppressed so that they don't actually
+ // override animations.
+ let iter = self
+ .self_and_ancestors()
+ .skip_while(|node| node.cascade_level() == CascadeLevel::Transitions)
+ .take_while(|node| node.cascade_level() > CascadeLevel::Animations);
+ let mut result = (LonghandIdSet::new(), false);
+ for node in iter {
+ let style = node.style_source().unwrap();
+ for (decl, important) in style
+ .read(node.cascade_level().guard(guards))
+ .declaration_importance_iter()
+ {
+ // Although we are only iterating over cascade levels that
+ // override animations, in a given property declaration block we
+ // can have a mixture of !important and non-!important
+ // declarations but only the !important declarations actually
+ // override animations.
+ if important.important() {
+ match decl.id() {
+ PropertyDeclarationId::Longhand(id) => result.0.insert(id),
+ PropertyDeclarationId::Custom(_) => result.1 = true,
+ }
+ }
+ }
+ }
+ result
+ }
+}
+
+/// An iterator over a rule node and its ancestors.
+#[derive(Clone)]
+pub struct SelfAndAncestors<'a> {
+ current: Option<&'a StrongRuleNode>,
+}
+
+impl<'a> Iterator for SelfAndAncestors<'a> {
+ type Item = &'a StrongRuleNode;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.current.map(|node| {
+ self.current = node.parent();
+ node
+ })
+ }
+}
diff --git a/servo/components/style/rule_tree/source.rs b/servo/components/style/rule_tree/source.rs
new file mode 100644
index 0000000000..76443692d7
--- /dev/null
+++ b/servo/components/style/rule_tree/source.rs
@@ -0,0 +1,75 @@
+/* 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/. */
+
+#![forbid(unsafe_code)]
+
+use crate::properties::PropertyDeclarationBlock;
+use crate::shared_lock::{Locked, SharedRwLockReadGuard};
+use crate::stylesheets::StyleRule;
+use servo_arc::{Arc, ArcBorrow, ArcUnion, ArcUnionBorrow};
+use std::io::Write;
+use std::ptr;
+
+/// A style source for the rule node. It can either be a CSS style rule or a
+/// declaration block.
+///
+/// Note that, even though the declaration block from inside the style rule
+/// could be enough to implement the rule tree, keeping the whole rule provides
+/// more debuggability, and also the ability of show those selectors to
+/// devtools.
+#[derive(Clone, Debug)]
+pub struct StyleSource(ArcUnion<Locked<StyleRule>, Locked<PropertyDeclarationBlock>>);
+
+impl PartialEq for StyleSource {
+ fn eq(&self, other: &Self) -> bool {
+ ArcUnion::ptr_eq(&self.0, &other.0)
+ }
+}
+
+impl StyleSource {
+ /// Creates a StyleSource from a StyleRule.
+ pub fn from_rule(rule: Arc<Locked<StyleRule>>) -> Self {
+ StyleSource(ArcUnion::from_first(rule))
+ }
+
+ #[inline]
+ pub(super) fn key(&self) -> ptr::NonNull<()> {
+ self.0.ptr()
+ }
+
+ /// Creates a StyleSource from a PropertyDeclarationBlock.
+ pub fn from_declarations(decls: Arc<Locked<PropertyDeclarationBlock>>) -> Self {
+ StyleSource(ArcUnion::from_second(decls))
+ }
+
+ pub(super) fn dump<W: Write>(&self, guard: &SharedRwLockReadGuard, writer: &mut W) {
+ if let Some(ref rule) = self.0.as_first() {
+ let rule = rule.read_with(guard);
+ let _ = write!(writer, "{:?}", rule.selectors);
+ }
+
+ let _ = write!(writer, " -> {:?}", self.read(guard).declarations());
+ }
+
+ /// Read the style source guard, and obtain thus read access to the
+ /// underlying property declaration block.
+ #[inline]
+ pub fn read<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a PropertyDeclarationBlock {
+ let block: &Locked<PropertyDeclarationBlock> = match self.0.borrow() {
+ ArcUnionBorrow::First(ref rule) => &rule.get().read_with(guard).block,
+ ArcUnionBorrow::Second(ref block) => block.get(),
+ };
+ block.read_with(guard)
+ }
+
+ /// Returns the style rule if applicable, otherwise None.
+ pub fn as_rule(&self) -> Option<ArcBorrow<Locked<StyleRule>>> {
+ self.0.as_first()
+ }
+
+ /// Returns the declaration block if applicable, otherwise None.
+ pub fn as_declarations(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>> {
+ self.0.as_second()
+ }
+}
diff --git a/servo/components/style/rule_tree/unsafe_box.rs b/servo/components/style/rule_tree/unsafe_box.rs
new file mode 100644
index 0000000000..eaa441d7b2
--- /dev/null
+++ b/servo/components/style/rule_tree/unsafe_box.rs
@@ -0,0 +1,74 @@
+/* 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/. */
+
+#![allow(unsafe_code)]
+
+use std::mem::ManuallyDrop;
+use std::ops::Deref;
+use std::ptr;
+
+/// An unsafe box, derefs to `T`.
+pub(super) struct UnsafeBox<T> {
+ inner: ManuallyDrop<Box<T>>,
+}
+
+impl<T> UnsafeBox<T> {
+ /// Creates a new unsafe box.
+ pub(super) fn from_box(value: Box<T>) -> Self {
+ Self {
+ inner: ManuallyDrop::new(value),
+ }
+ }
+
+ /// Creates a new box from a pointer.
+ ///
+ /// # Safety
+ ///
+ /// The input should point to a valid `T`.
+ pub(super) unsafe fn from_raw(ptr: *mut T) -> Self {
+ Self {
+ inner: ManuallyDrop::new(Box::from_raw(ptr)),
+ }
+ }
+
+ /// Creates a new unsafe box from an existing one.
+ ///
+ /// # Safety
+ ///
+ /// There is no refcounting or whatever else in an unsafe box, so this
+ /// operation can lead to double frees.
+ pub(super) unsafe fn clone(this: &Self) -> Self {
+ Self {
+ inner: ptr::read(&this.inner),
+ }
+ }
+
+ /// Returns a mutable reference to the inner value of this unsafe box.
+ ///
+ /// # Safety
+ ///
+ /// Given `Self::clone`, nothing prevents anyone from creating
+ /// multiple mutable references to the inner value, which is completely UB.
+ pub(crate) unsafe fn deref_mut(this: &mut Self) -> &mut T {
+ &mut this.inner
+ }
+
+ /// Drops the inner value of this unsafe box.
+ ///
+ /// # Safety
+ ///
+ /// Given this doesn't consume the unsafe box itself, this has the same
+ /// safety caveats as `ManuallyDrop::drop`.
+ pub(super) unsafe fn drop(this: &mut Self) {
+ ManuallyDrop::drop(&mut this.inner)
+ }
+}
+
+impl<T> Deref for UnsafeBox<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
diff --git a/servo/components/style/scoped_tls.rs b/servo/components/style/scoped_tls.rs
new file mode 100644
index 0000000000..0d3267397a
--- /dev/null
+++ b/servo/components/style/scoped_tls.rs
@@ -0,0 +1,81 @@
+/* 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/. */
+
+//! Stack-scoped thread-local storage for rayon thread pools.
+
+#![allow(unsafe_code)]
+#![deny(missing_docs)]
+
+use crate::global_style_data::STYLO_MAX_THREADS;
+use rayon;
+use std::cell::{Ref, RefCell, RefMut};
+use std::ops::DerefMut;
+
+/// A scoped TLS set, that is alive during the `'scope` lifetime.
+///
+/// We use this on Servo to construct thread-local contexts, but clear them once
+/// we're done with restyling.
+///
+/// Note that the cleanup is done on the thread that owns the scoped TLS, thus
+/// the Send bound.
+pub struct ScopedTLS<'scope, T: Send> {
+ pool: Option<&'scope rayon::ThreadPool>,
+ slots: [RefCell<Option<T>>; STYLO_MAX_THREADS],
+}
+
+/// The scoped TLS is `Sync` because no more than one worker thread can access a
+/// given slot.
+unsafe impl<'scope, T: Send> Sync for ScopedTLS<'scope, T> {}
+
+impl<'scope, T: Send> ScopedTLS<'scope, T> {
+ /// Create a new scoped TLS that will last as long as this rayon threadpool
+ /// reference.
+ pub fn new(pool: Option<&'scope rayon::ThreadPool>) -> Self {
+ debug_assert!(pool.map_or(true, |p| p.current_num_threads() <= STYLO_MAX_THREADS));
+ ScopedTLS {
+ pool,
+ slots: Default::default(),
+ }
+ }
+
+ /// Returns the index corresponding to the calling thread in the thread pool.
+ #[inline]
+ pub fn current_thread_index(&self) -> usize {
+ self.pool.map_or(0, |p| p.current_thread_index().unwrap())
+ }
+
+ /// Return an immutable reference to the `Option<T>` that this thread owns.
+ pub fn borrow(&self) -> Ref<Option<T>> {
+ let idx = self.current_thread_index();
+ self.slots[idx].borrow()
+ }
+
+ /// Return a mutable reference to the `Option<T>` that this thread owns.
+ pub fn borrow_mut(&self) -> RefMut<Option<T>> {
+ let idx = self.current_thread_index();
+ self.slots[idx].borrow_mut()
+ }
+
+ /// Ensure that the current data this thread owns is initialized, or
+ /// initialize it using `f`. We want ensure() to be fast and inline, and we
+ /// want to inline the memmove that initializes the Option<T>. But we don't
+ /// want to inline space for the entire large T struct in our stack frame.
+ /// That's why we hand `f` a mutable borrow to write to instead of just
+ /// having it return a T.
+ #[inline(always)]
+ pub fn ensure<F: FnOnce(&mut Option<T>)>(&self, f: F) -> RefMut<T> {
+ let mut opt = self.borrow_mut();
+ if opt.is_none() {
+ f(opt.deref_mut());
+ }
+
+ RefMut::map(opt, |x| x.as_mut().unwrap())
+ }
+
+ /// Returns the slots. Safe because if we have a mut reference the tls can't be referenced by
+ /// any other thread.
+ pub fn slots(&mut self) -> &mut [RefCell<Option<T>>] {
+ &mut self.slots
+ }
+}
diff --git a/servo/components/style/selector_map.rs b/servo/components/style/selector_map.rs
new file mode 100644
index 0000000000..2b8d6add55
--- /dev/null
+++ b/servo/components/style/selector_map.rs
@@ -0,0 +1,870 @@
+/* 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/. */
+
+//! A data structure to efficiently index structs containing selectors by local
+//! name, ids and hash.
+
+use crate::applicable_declarations::ApplicableDeclarationList;
+use crate::context::QuirksMode;
+use crate::dom::TElement;
+use crate::rule_tree::CascadeLevel;
+use crate::selector_parser::SelectorImpl;
+use crate::stylist::{CascadeData, ContainerConditionId, Rule, Stylist};
+use crate::AllocErr;
+use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded, WeakAtom};
+use dom::ElementState;
+use precomputed_hash::PrecomputedHash;
+use selectors::matching::{matches_selector, MatchingContext};
+use selectors::parser::{Combinator, Component, SelectorIter};
+use smallvec::SmallVec;
+use std::collections::hash_map;
+use std::collections::{HashMap, HashSet};
+use std::hash::{BuildHasherDefault, Hash, Hasher};
+
+/// A hasher implementation that doesn't hash anything, because it expects its
+/// input to be a suitable u32 hash.
+pub struct PrecomputedHasher {
+ hash: Option<u32>,
+}
+
+impl Default for PrecomputedHasher {
+ fn default() -> Self {
+ Self { hash: None }
+ }
+}
+
+/// A vector of relevant attributes, that can be useful for revalidation.
+pub type RelevantAttributes = thin_vec::ThinVec<LocalName>;
+
+/// This is a set of pseudo-classes that are both relatively-rare (they don't
+/// affect most elements by default) and likely or known to have global rules
+/// (in e.g., the UA sheets).
+///
+/// We can avoid selector-matching those global rules for all elements without
+/// these pseudo-class states.
+const RARE_PSEUDO_CLASS_STATES: ElementState = ElementState::from_bits_retain(
+ ElementState::FULLSCREEN.bits() |
+ ElementState::VISITED_OR_UNVISITED.bits() |
+ ElementState::URLTARGET.bits() |
+ ElementState::INERT.bits() |
+ ElementState::FOCUS.bits() |
+ ElementState::FOCUSRING.bits() |
+ ElementState::TOPMOST_MODAL.bits(),
+);
+
+/// A simple alias for a hashmap using PrecomputedHasher.
+pub type PrecomputedHashMap<K, V> = HashMap<K, V, BuildHasherDefault<PrecomputedHasher>>;
+
+/// A simple alias for a hashset using PrecomputedHasher.
+pub type PrecomputedHashSet<K> = HashSet<K, BuildHasherDefault<PrecomputedHasher>>;
+
+impl Hasher for PrecomputedHasher {
+ #[inline]
+ fn write(&mut self, _: &[u8]) {
+ unreachable!(
+ "Called into PrecomputedHasher with something that isn't \
+ a u32"
+ )
+ }
+
+ #[inline]
+ fn write_u32(&mut self, i: u32) {
+ debug_assert!(self.hash.is_none());
+ self.hash = Some(i);
+ }
+
+ #[inline]
+ fn finish(&self) -> u64 {
+ self.hash.expect("PrecomputedHasher wasn't fed?") as u64
+ }
+}
+
+/// A trait to abstract over a given selector map entry.
+pub trait SelectorMapEntry: Sized + Clone {
+ /// Gets the selector we should use to index in the selector map.
+ fn selector(&self) -> SelectorIter<SelectorImpl>;
+}
+
+/// Map element data to selector-providing objects for which the last simple
+/// selector starts with them.
+///
+/// e.g.,
+/// "p > img" would go into the set of selectors corresponding to the
+/// element "img"
+/// "a .foo .bar.baz" would go into the set of selectors corresponding to
+/// the class "bar"
+///
+/// Because we match selectors right-to-left (i.e., moving up the tree
+/// from an element), we need to compare the last simple selector in the
+/// selector with the element.
+///
+/// So, if an element has ID "id1" and classes "foo" and "bar", then all
+/// the rules it matches will have their last simple selector starting
+/// either with "#id1" or with ".foo" or with ".bar".
+///
+/// Hence, the union of the rules keyed on each of element's classes, ID,
+/// element name, etc. will contain the Selectors that actually match that
+/// element.
+///
+/// We use a 1-entry SmallVec to avoid a separate heap allocation in the case
+/// where we only have one entry, which is quite common. See measurements in:
+/// * https://bugzilla.mozilla.org/show_bug.cgi?id=1363789#c5
+/// * https://bugzilla.mozilla.org/show_bug.cgi?id=681755
+///
+/// TODO: Tune the initial capacity of the HashMap
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct SelectorMap<T: 'static> {
+ /// Rules that have `:root` selectors.
+ pub root: SmallVec<[T; 1]>,
+ /// A hash from an ID to rules which contain that ID selector.
+ pub id_hash: MaybeCaseInsensitiveHashMap<Atom, SmallVec<[T; 1]>>,
+ /// A hash from a class name to rules which contain that class selector.
+ pub class_hash: MaybeCaseInsensitiveHashMap<Atom, SmallVec<[T; 1]>>,
+ /// A hash from local name to rules which contain that local name selector.
+ pub local_name_hash: PrecomputedHashMap<LocalName, SmallVec<[T; 1]>>,
+ /// A hash from attributes to rules which contain that attribute selector.
+ pub attribute_hash: PrecomputedHashMap<LocalName, SmallVec<[T; 1]>>,
+ /// A hash from namespace to rules which contain that namespace selector.
+ pub namespace_hash: PrecomputedHashMap<Namespace, SmallVec<[T; 1]>>,
+ /// Rules for pseudo-states that are rare but have global selectors.
+ pub rare_pseudo_classes: SmallVec<[T; 1]>,
+ /// All other rules.
+ pub other: SmallVec<[T; 1]>,
+ /// The number of entries in this map.
+ pub count: usize,
+}
+
+impl<T: 'static> Default for SelectorMap<T> {
+ #[inline]
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<T> SelectorMap<T> {
+ /// Trivially constructs an empty `SelectorMap`.
+ pub fn new() -> Self {
+ SelectorMap {
+ root: SmallVec::new(),
+ id_hash: MaybeCaseInsensitiveHashMap::new(),
+ class_hash: MaybeCaseInsensitiveHashMap::new(),
+ attribute_hash: HashMap::default(),
+ local_name_hash: HashMap::default(),
+ namespace_hash: HashMap::default(),
+ rare_pseudo_classes: SmallVec::new(),
+ other: SmallVec::new(),
+ count: 0,
+ }
+ }
+
+ /// Shrink the capacity of the map if needed.
+ pub fn shrink_if_needed(&mut self) {
+ self.id_hash.shrink_if_needed();
+ self.class_hash.shrink_if_needed();
+ self.attribute_hash.shrink_if_needed();
+ self.local_name_hash.shrink_if_needed();
+ self.namespace_hash.shrink_if_needed();
+ }
+
+ /// Clears the hashmap retaining storage.
+ pub fn clear(&mut self) {
+ self.root.clear();
+ self.id_hash.clear();
+ self.class_hash.clear();
+ self.attribute_hash.clear();
+ self.local_name_hash.clear();
+ self.namespace_hash.clear();
+ self.rare_pseudo_classes.clear();
+ self.other.clear();
+ self.count = 0;
+ }
+
+ /// Returns whether there are any entries in the map.
+ pub fn is_empty(&self) -> bool {
+ self.count == 0
+ }
+
+ /// Returns the number of entries.
+ pub fn len(&self) -> usize {
+ self.count
+ }
+}
+
+impl SelectorMap<Rule> {
+ /// Append to `rule_list` all Rules in `self` that match element.
+ ///
+ /// Extract matching rules as per element's ID, classes, tag name, etc..
+ /// Sort the Rules at the end to maintain cascading order.
+ pub fn get_all_matching_rules<E>(
+ &self,
+ element: E,
+ rule_hash_target: E,
+ matching_rules_list: &mut ApplicableDeclarationList,
+ matching_context: &mut MatchingContext<E::Impl>,
+ cascade_level: CascadeLevel,
+ cascade_data: &CascadeData,
+ stylist: &Stylist,
+ ) where
+ E: TElement,
+ {
+ if self.is_empty() {
+ return;
+ }
+
+ let quirks_mode = matching_context.quirks_mode();
+
+ if rule_hash_target.is_root() {
+ SelectorMap::get_matching_rules(
+ element,
+ &self.root,
+ matching_rules_list,
+ matching_context,
+ cascade_level,
+ cascade_data,
+ stylist,
+ );
+ }
+
+ if let Some(id) = rule_hash_target.id() {
+ if let Some(rules) = self.id_hash.get(id, quirks_mode) {
+ SelectorMap::get_matching_rules(
+ element,
+ rules,
+ matching_rules_list,
+ matching_context,
+ cascade_level,
+ cascade_data,
+ stylist,
+ )
+ }
+ }
+
+ rule_hash_target.each_class(|class| {
+ if let Some(rules) = self.class_hash.get(&class, quirks_mode) {
+ SelectorMap::get_matching_rules(
+ element,
+ rules,
+ matching_rules_list,
+ matching_context,
+ cascade_level,
+ cascade_data,
+ stylist,
+ )
+ }
+ });
+
+ rule_hash_target.each_attr_name(|name| {
+ if let Some(rules) = self.attribute_hash.get(name) {
+ SelectorMap::get_matching_rules(
+ element,
+ rules,
+ matching_rules_list,
+ matching_context,
+ cascade_level,
+ cascade_data,
+ stylist,
+ )
+ }
+ });
+
+ if let Some(rules) = self.local_name_hash.get(rule_hash_target.local_name()) {
+ SelectorMap::get_matching_rules(
+ element,
+ rules,
+ matching_rules_list,
+ matching_context,
+ cascade_level,
+ cascade_data,
+ stylist,
+ )
+ }
+
+ if rule_hash_target
+ .state()
+ .intersects(RARE_PSEUDO_CLASS_STATES)
+ {
+ SelectorMap::get_matching_rules(
+ element,
+ &self.rare_pseudo_classes,
+ matching_rules_list,
+ matching_context,
+ cascade_level,
+ cascade_data,
+ stylist,
+ );
+ }
+
+ if let Some(rules) = self.namespace_hash.get(rule_hash_target.namespace()) {
+ SelectorMap::get_matching_rules(
+ element,
+ rules,
+ matching_rules_list,
+ matching_context,
+ cascade_level,
+ cascade_data,
+ stylist,
+ )
+ }
+
+ SelectorMap::get_matching_rules(
+ element,
+ &self.other,
+ matching_rules_list,
+ matching_context,
+ cascade_level,
+ cascade_data,
+ stylist,
+ );
+ }
+
+ /// Adds rules in `rules` that match `element` to the `matching_rules` list.
+ pub(crate) fn get_matching_rules<E>(
+ element: E,
+ rules: &[Rule],
+ matching_rules: &mut ApplicableDeclarationList,
+ matching_context: &mut MatchingContext<E::Impl>,
+ cascade_level: CascadeLevel,
+ cascade_data: &CascadeData,
+ stylist: &Stylist,
+ ) where
+ E: TElement,
+ {
+ for rule in rules {
+ if !matches_selector(
+ &rule.selector,
+ 0,
+ Some(&rule.hashes),
+ &element,
+ matching_context,
+ ) {
+ continue;
+ }
+
+ if rule.container_condition_id != ContainerConditionId::none() {
+ if !cascade_data.container_condition_matches(
+ rule.container_condition_id,
+ stylist,
+ element,
+ matching_context,
+ ) {
+ continue;
+ }
+ }
+
+ matching_rules.push(rule.to_applicable_declaration_block(cascade_level, cascade_data));
+ }
+ }
+}
+
+impl<T: SelectorMapEntry> SelectorMap<T> {
+ /// Inserts an entry into the correct bucket(s).
+ pub fn insert(&mut self, entry: T, quirks_mode: QuirksMode) -> Result<(), AllocErr> {
+ self.count += 1;
+
+ // NOTE(emilio): It'd be nice for this to be a separate function, but
+ // then the compiler can't reason about the lifetime dependency between
+ // `entry` and `bucket`, and would force us to clone the rule in the
+ // common path.
+ macro_rules! insert_into_bucket {
+ ($entry:ident, $bucket:expr) => {{
+ let vec = match $bucket {
+ Bucket::Root => &mut self.root,
+ Bucket::ID(id) => self
+ .id_hash
+ .try_entry(id.clone(), quirks_mode)?
+ .or_default(),
+ Bucket::Class(class) => self
+ .class_hash
+ .try_entry(class.clone(), quirks_mode)?
+ .or_default(),
+ Bucket::Attribute { name, lower_name } |
+ Bucket::LocalName { name, lower_name } => {
+ // If the local name in the selector isn't lowercase,
+ // insert it into the rule hash twice. This means that,
+ // during lookup, we can always find the rules based on
+ // the local name of the element, regardless of whether
+ // it's an html element in an html document (in which
+ // case we match against lower_name) or not (in which
+ // case we match against name).
+ //
+ // In the case of a non-html-element-in-html-document
+ // with a lowercase localname and a non-lowercase
+ // selector, the rulehash lookup may produce superfluous
+ // selectors, but the subsequent selector matching work
+ // will filter them out.
+ let is_attribute = matches!($bucket, Bucket::Attribute { .. });
+ let hash = if is_attribute {
+ &mut self.attribute_hash
+ } else {
+ &mut self.local_name_hash
+ };
+ if name != lower_name {
+ hash.try_reserve(1)?;
+ let vec = hash.entry(lower_name.clone()).or_default();
+ vec.try_reserve(1)?;
+ vec.push($entry.clone());
+ }
+ hash.try_reserve(1)?;
+ hash.entry(name.clone()).or_default()
+ },
+ Bucket::Namespace(url) => {
+ self.namespace_hash.try_reserve(1)?;
+ self.namespace_hash.entry(url.clone()).or_default()
+ },
+ Bucket::RarePseudoClasses => &mut self.rare_pseudo_classes,
+ Bucket::Universal => &mut self.other,
+ };
+ vec.try_reserve(1)?;
+ vec.push($entry);
+ }};
+ }
+
+ let bucket = {
+ let mut disjoint_buckets = SmallVec::new();
+ let bucket = find_bucket(
+ entry.selector(),
+ &mut disjoint_buckets,
+ );
+
+ // See if inserting this selector in multiple entries in the
+ // selector map would be worth it. Consider a case like:
+ //
+ // .foo:where(div, #bar)
+ //
+ // There, `bucket` would be `Class(foo)`, and disjoint_buckets would
+ // be `[LocalName { div }, ID(bar)]`.
+ //
+ // Here we choose to insert the selector in the `.foo` bucket in
+ // such a case, as it's likely more worth it than inserting it in
+ // both `div` and `#bar`.
+ //
+ // This is specially true if there's any universal selector in the
+ // `disjoint_selectors` set, at which point we'd just be doing
+ // wasted work.
+ if !disjoint_buckets.is_empty() &&
+ disjoint_buckets
+ .iter()
+ .all(|b| b.more_specific_than(&bucket))
+ {
+ for bucket in &disjoint_buckets {
+ let entry = entry.clone();
+ insert_into_bucket!(entry, *bucket);
+ }
+ return Ok(());
+ }
+ bucket
+ };
+
+ insert_into_bucket!(entry, bucket);
+ Ok(())
+ }
+
+ /// Looks up entries by id, class, local name, namespace, and other (in
+ /// order).
+ ///
+ /// Each entry is passed to the callback, which returns true to continue
+ /// iterating entries, or false to terminate the lookup.
+ ///
+ /// Returns false if the callback ever returns false.
+ ///
+ /// FIXME(bholley) This overlaps with SelectorMap<Rule>::get_all_matching_rules,
+ /// but that function is extremely hot and I'd rather not rearrange it.
+ pub fn lookup<'a, E, F>(
+ &'a self,
+ element: E,
+ quirks_mode: QuirksMode,
+ relevant_attributes: Option<&mut RelevantAttributes>,
+ f: F,
+ ) -> bool
+ where
+ E: TElement,
+ F: FnMut(&'a T) -> bool,
+ {
+ self.lookup_with_state(
+ element,
+ element.state(),
+ quirks_mode,
+ relevant_attributes,
+ f,
+ )
+ }
+
+ #[inline]
+ fn lookup_with_state<'a, E, F>(
+ &'a self,
+ element: E,
+ element_state: ElementState,
+ quirks_mode: QuirksMode,
+ mut relevant_attributes: Option<&mut RelevantAttributes>,
+ mut f: F,
+ ) -> bool
+ where
+ E: TElement,
+ F: FnMut(&'a T) -> bool,
+ {
+ if element.is_root() {
+ for entry in self.root.iter() {
+ if !f(&entry) {
+ return false;
+ }
+ }
+ }
+
+ if let Some(id) = element.id() {
+ if let Some(v) = self.id_hash.get(id, quirks_mode) {
+ for entry in v.iter() {
+ if !f(&entry) {
+ return false;
+ }
+ }
+ }
+ }
+
+ let mut done = false;
+ element.each_class(|class| {
+ if done {
+ return;
+ }
+ if let Some(v) = self.class_hash.get(class, quirks_mode) {
+ for entry in v.iter() {
+ if !f(&entry) {
+ done = true;
+ return;
+ }
+ }
+ }
+ });
+
+ if done {
+ return false;
+ }
+
+ element.each_attr_name(|name| {
+ if done {
+ return;
+ }
+ if let Some(v) = self.attribute_hash.get(name) {
+ if let Some(ref mut relevant_attributes) = relevant_attributes {
+ relevant_attributes.push(name.clone());
+ }
+ for entry in v.iter() {
+ if !f(&entry) {
+ done = true;
+ return;
+ }
+ }
+ }
+ });
+
+ if done {
+ return false;
+ }
+
+ if let Some(v) = self.local_name_hash.get(element.local_name()) {
+ for entry in v.iter() {
+ if !f(&entry) {
+ return false;
+ }
+ }
+ }
+
+ if let Some(v) = self.namespace_hash.get(element.namespace()) {
+ for entry in v.iter() {
+ if !f(&entry) {
+ return false;
+ }
+ }
+ }
+
+ if element_state.intersects(RARE_PSEUDO_CLASS_STATES) {
+ for entry in self.rare_pseudo_classes.iter() {
+ if !f(&entry) {
+ return false;
+ }
+ }
+ }
+
+ for entry in self.other.iter() {
+ if !f(&entry) {
+ return false;
+ }
+ }
+
+ true
+ }
+
+ /// Performs a normal lookup, and also looks up entries for the passed-in
+ /// id and classes.
+ ///
+ /// Each entry is passed to the callback, which returns true to continue
+ /// iterating entries, or false to terminate the lookup.
+ ///
+ /// Returns false if the callback ever returns false.
+ #[inline]
+ pub fn lookup_with_additional<'a, E, F>(
+ &'a self,
+ element: E,
+ quirks_mode: QuirksMode,
+ additional_id: Option<&WeakAtom>,
+ additional_classes: &[Atom],
+ additional_states: ElementState,
+ mut f: F,
+ ) -> bool
+ where
+ E: TElement,
+ F: FnMut(&'a T) -> bool,
+ {
+ // Do the normal lookup.
+ if !self.lookup_with_state(
+ element,
+ element.state() | additional_states,
+ quirks_mode,
+ /* relevant_attributes = */ None,
+ |entry| f(entry),
+ ) {
+ return false;
+ }
+
+ // Check the additional id.
+ if let Some(id) = additional_id {
+ if let Some(v) = self.id_hash.get(id, quirks_mode) {
+ for entry in v.iter() {
+ if !f(&entry) {
+ return false;
+ }
+ }
+ }
+ }
+
+ // Check the additional classes.
+ for class in additional_classes {
+ if let Some(v) = self.class_hash.get(class, quirks_mode) {
+ for entry in v.iter() {
+ if !f(&entry) {
+ return false;
+ }
+ }
+ }
+ }
+
+ true
+ }
+}
+
+enum Bucket<'a> {
+ Universal,
+ Namespace(&'a Namespace),
+ RarePseudoClasses,
+ LocalName {
+ name: &'a LocalName,
+ lower_name: &'a LocalName,
+ },
+ Attribute {
+ name: &'a LocalName,
+ lower_name: &'a LocalName,
+ },
+ Class(&'a Atom),
+ ID(&'a Atom),
+ Root,
+}
+
+impl<'a> Bucket<'a> {
+ /// root > id > class > local name > namespace > pseudo-classes > universal.
+ #[inline]
+ fn specificity(&self) -> usize {
+ match *self {
+ Bucket::Universal => 0,
+ Bucket::Namespace(..) => 1,
+ Bucket::RarePseudoClasses => 2,
+ Bucket::LocalName { .. } => 3,
+ Bucket::Attribute { .. } => 4,
+ Bucket::Class(..) => 5,
+ Bucket::ID(..) => 6,
+ Bucket::Root => 7,
+ }
+ }
+
+ #[inline]
+ fn more_or_equally_specific_than(&self, other: &Self) -> bool {
+ self.specificity() >= other.specificity()
+ }
+
+ #[inline]
+ fn more_specific_than(&self, other: &Self) -> bool {
+ self.specificity() > other.specificity()
+ }
+}
+
+type DisjointBuckets<'a> = SmallVec<[Bucket<'a>; 5]>;
+
+fn specific_bucket_for<'a>(
+ component: &'a Component<SelectorImpl>,
+ disjoint_buckets: &mut DisjointBuckets<'a>,
+) -> Bucket<'a> {
+ match *component {
+ Component::Root => Bucket::Root,
+ Component::ID(ref id) => Bucket::ID(id),
+ Component::Class(ref class) => Bucket::Class(class),
+ Component::AttributeInNoNamespace { ref local_name, .. } => {
+ Bucket::Attribute {
+ name: local_name,
+ lower_name: local_name,
+ }
+ },
+ Component::AttributeInNoNamespaceExists {
+ ref local_name,
+ ref local_name_lower,
+ } => Bucket::Attribute {
+ name: local_name,
+ lower_name: local_name_lower,
+ },
+ Component::AttributeOther(ref selector) => Bucket::Attribute {
+ name: &selector.local_name,
+ lower_name: &selector.local_name_lower,
+ },
+ Component::LocalName(ref selector) => Bucket::LocalName {
+ name: &selector.name,
+ lower_name: &selector.lower_name,
+ },
+ Component::Namespace(_, ref url) | Component::DefaultNamespace(ref url) => {
+ Bucket::Namespace(url)
+ },
+ // ::slotted(..) isn't a normal pseudo-element, so we can insert it on
+ // the rule hash normally without much problem. For example, in a
+ // selector like:
+ //
+ // div::slotted(span)::before
+ //
+ // It looks like:
+ //
+ // [
+ // LocalName(div),
+ // Combinator(SlotAssignment),
+ // Slotted(span),
+ // Combinator::PseudoElement,
+ // PseudoElement(::before),
+ // ]
+ //
+ // So inserting `span` in the rule hash makes sense since we want to
+ // match the slotted <span>.
+ Component::Slotted(ref selector) => {
+ find_bucket(selector.iter(), disjoint_buckets)
+ },
+ Component::Host(Some(ref selector)) => {
+ find_bucket(selector.iter(), disjoint_buckets)
+ },
+ Component::Is(ref list) | Component::Where(ref list) => {
+ if list.len() == 1 {
+ find_bucket(list.slice()[0].iter(), disjoint_buckets)
+ } else {
+ for selector in list.slice() {
+ let bucket = find_bucket(selector.iter(), disjoint_buckets);
+ disjoint_buckets.push(bucket);
+ }
+ Bucket::Universal
+ }
+ },
+ Component::NonTSPseudoClass(ref pseudo_class)
+ if pseudo_class
+ .state_flag()
+ .intersects(RARE_PSEUDO_CLASS_STATES) =>
+ {
+ Bucket::RarePseudoClasses
+ },
+ _ => Bucket::Universal,
+ }
+}
+
+/// Searches a compound selector from left to right, and returns the appropriate
+/// bucket for it.
+///
+/// It also populates disjoint_buckets with dependencies from nested selectors
+/// with any semantics like :is() and :where().
+#[inline(always)]
+fn find_bucket<'a>(
+ mut iter: SelectorIter<'a, SelectorImpl>,
+ disjoint_buckets: &mut DisjointBuckets<'a>,
+) -> Bucket<'a> {
+ let mut current_bucket = Bucket::Universal;
+
+ loop {
+ for ss in &mut iter {
+ let new_bucket = specific_bucket_for(ss, disjoint_buckets);
+ // NOTE: When presented with the choice of multiple specific selectors, use the
+ // rightmost, on the assumption that that's less common, see bug 1829540.
+ if new_bucket.more_or_equally_specific_than(&current_bucket) {
+ current_bucket = new_bucket;
+ }
+ }
+
+ // Effectively, pseudo-elements are ignored, given only state
+ // pseudo-classes may appear before them.
+ if iter.next_sequence() != Some(Combinator::PseudoElement) {
+ break;
+ }
+ }
+
+ current_bucket
+}
+
+/// Wrapper for PrecomputedHashMap that does ASCII-case-insensitive lookup in quirks mode.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct MaybeCaseInsensitiveHashMap<K: PrecomputedHash + Hash + Eq, V>(PrecomputedHashMap<K, V>);
+
+impl<V> Default for MaybeCaseInsensitiveHashMap<Atom, V> {
+ #[inline]
+ fn default() -> Self {
+ MaybeCaseInsensitiveHashMap(PrecomputedHashMap::default())
+ }
+}
+
+impl<V> MaybeCaseInsensitiveHashMap<Atom, V> {
+ /// Empty map
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Shrink the capacity of the map if needed.
+ pub fn shrink_if_needed(&mut self) {
+ self.0.shrink_if_needed()
+ }
+
+ /// HashMap::try_entry
+ pub fn try_entry(
+ &mut self,
+ mut key: Atom,
+ quirks_mode: QuirksMode,
+ ) -> Result<hash_map::Entry<Atom, V>, AllocErr> {
+ if quirks_mode == QuirksMode::Quirks {
+ key = key.to_ascii_lowercase()
+ }
+ self.0.try_reserve(1)?;
+ Ok(self.0.entry(key))
+ }
+
+ /// HashMap::is_empty
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ /// HashMap::iter
+ pub fn iter(&self) -> hash_map::Iter<Atom, V> {
+ self.0.iter()
+ }
+
+ /// HashMap::clear
+ pub fn clear(&mut self) {
+ self.0.clear()
+ }
+
+ /// HashMap::get
+ pub fn get(&self, key: &WeakAtom, quirks_mode: QuirksMode) -> Option<&V> {
+ if quirks_mode == QuirksMode::Quirks {
+ self.0.get(&key.to_ascii_lowercase())
+ } else {
+ self.0.get(key)
+ }
+ }
+}
diff --git a/servo/components/style/selector_parser.rs b/servo/components/style/selector_parser.rs
new file mode 100644
index 0000000000..4eef26f1b7
--- /dev/null
+++ b/servo/components/style/selector_parser.rs
@@ -0,0 +1,240 @@
+/* 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/. */
+
+//! The pseudo-classes and pseudo-elements supported by the style system.
+
+#![deny(missing_docs)]
+
+use crate::stylesheets::{Namespaces, Origin, UrlExtraData};
+use crate::values::serialize_atom_identifier;
+use crate::Atom;
+use cssparser::{Parser as CssParser, ParserInput};
+use dom::ElementState;
+use selectors::parser::{ParseRelative, SelectorList};
+use std::fmt::{self, Debug, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A convenient alias for the type that represents an attribute value used for
+/// selector parser implementation.
+pub type AttrValue = <SelectorImpl as ::selectors::SelectorImpl>::AttrValue;
+
+#[cfg(feature = "servo")]
+pub use crate::servo::selector_parser::*;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::selector_parser::*;
+
+#[cfg(feature = "servo")]
+pub use crate::servo::selector_parser::ServoElementSnapshot as Snapshot;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::snapshot::GeckoElementSnapshot as Snapshot;
+
+#[cfg(feature = "servo")]
+pub use crate::servo::restyle_damage::ServoRestyleDamage as RestyleDamage;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::restyle_damage::GeckoRestyleDamage as RestyleDamage;
+
+/// Servo's selector parser.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct SelectorParser<'a> {
+ /// The origin of the stylesheet we're parsing.
+ pub stylesheet_origin: Origin,
+ /// The namespace set of the stylesheet.
+ pub namespaces: &'a Namespaces,
+ /// The extra URL data of the stylesheet, which is used to look up
+ /// whether we are parsing a chrome:// URL style sheet.
+ pub url_data: &'a UrlExtraData,
+ /// Whether we're parsing selectors for `@supports`
+ pub for_supports_rule: bool,
+}
+
+impl<'a> SelectorParser<'a> {
+ /// Parse a selector list with an author origin and without taking into
+ /// account namespaces.
+ ///
+ /// This is used for some DOM APIs like `querySelector`.
+ pub fn parse_author_origin_no_namespace<'i>(
+ input: &'i str,
+ url_data: &UrlExtraData,
+ ) -> Result<SelectorList<SelectorImpl>, ParseError<'i>> {
+ let namespaces = Namespaces::default();
+ let parser = SelectorParser {
+ stylesheet_origin: Origin::Author,
+ namespaces: &namespaces,
+ url_data,
+ for_supports_rule: false,
+ };
+ let mut input = ParserInput::new(input);
+ SelectorList::parse(&parser, &mut CssParser::new(&mut input), ParseRelative::No)
+ }
+
+ /// Whether we're parsing selectors in a user-agent stylesheet.
+ pub fn in_user_agent_stylesheet(&self) -> bool {
+ matches!(self.stylesheet_origin, Origin::UserAgent)
+ }
+
+ /// Whether we're parsing selectors in a stylesheet that has chrome
+ /// privilege.
+ pub fn chrome_rules_enabled(&self) -> bool {
+ self.url_data.chrome_rules_enabled() || self.stylesheet_origin == Origin::User
+ }
+}
+
+/// This enumeration determines if a pseudo-element is eagerly cascaded or not.
+///
+/// If you're implementing a public selector for `Servo` that the end-user might
+/// customize, then you probably need to make it eager.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum PseudoElementCascadeType {
+ /// Eagerly cascaded pseudo-elements are "normal" pseudo-elements (i.e.
+ /// `::before` and `::after`). They inherit styles normally as another
+ /// selector would do, and they're computed as part of the cascade.
+ Eager,
+ /// Lazy pseudo-elements are affected by selector matching, but they're only
+ /// computed when needed, and not before. They're useful for general
+ /// pseudo-elements that are not very common.
+ ///
+ /// Note that in Servo lazy pseudo-elements are restricted to a subset of
+ /// selectors, so you can't use it for public pseudo-elements. This is not
+ /// the case with Gecko though.
+ Lazy,
+ /// Precomputed pseudo-elements skip the cascade process entirely, mostly as
+ /// an optimisation since they are private pseudo-elements (like
+ /// `::-servo-details-content`).
+ ///
+ /// This pseudo-elements are resolved on the fly using *only* global rules
+ /// (rules of the form `*|*`), and applying them to the parent style.
+ Precomputed,
+}
+
+/// A per-pseudo map, from a given pseudo to a `T`.
+#[derive(Clone, MallocSizeOf)]
+pub struct PerPseudoElementMap<T> {
+ entries: [Option<T>; PSEUDO_COUNT],
+}
+
+impl<T> Default for PerPseudoElementMap<T> {
+ fn default() -> Self {
+ Self {
+ entries: PseudoElement::pseudo_none_array(),
+ }
+ }
+}
+
+impl<T> Debug for PerPseudoElementMap<T>
+where
+ T: Debug,
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_char('[')?;
+ let mut first = true;
+ for entry in self.entries.iter() {
+ if !first {
+ f.write_str(", ")?;
+ }
+ first = false;
+ entry.fmt(f)?;
+ }
+ f.write_char(']')
+ }
+}
+
+impl<T> PerPseudoElementMap<T> {
+ /// Get an entry in the map.
+ pub fn get(&self, pseudo: &PseudoElement) -> Option<&T> {
+ self.entries[pseudo.index()].as_ref()
+ }
+
+ /// Clear this enumerated array.
+ pub fn clear(&mut self) {
+ *self = Self::default();
+ }
+
+ /// Set an entry value.
+ ///
+ /// Returns an error if the element is not a simple pseudo.
+ pub fn set(&mut self, pseudo: &PseudoElement, value: T) {
+ self.entries[pseudo.index()] = Some(value);
+ }
+
+ /// Get an entry for `pseudo`, or create it with calling `f`.
+ pub fn get_or_insert_with<F>(&mut self, pseudo: &PseudoElement, f: F) -> &mut T
+ where
+ F: FnOnce() -> T,
+ {
+ let index = pseudo.index();
+ if self.entries[index].is_none() {
+ self.entries[index] = Some(f());
+ }
+ self.entries[index].as_mut().unwrap()
+ }
+
+ /// Get an iterator for the entries.
+ pub fn iter(&self) -> std::slice::Iter<Option<T>> {
+ self.entries.iter()
+ }
+
+ /// Get a mutable iterator for the entries.
+ pub fn iter_mut(&mut self) -> std::slice::IterMut<Option<T>> {
+ self.entries.iter_mut()
+ }
+}
+
+/// Values for the :dir() pseudo class
+///
+/// "ltr" and "rtl" values are normalized to lowercase.
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Direction(pub Atom);
+
+/// Horizontal values for the :dir() pseudo class
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum HorizontalDirection {
+ /// :dir(ltr)
+ Ltr,
+ /// :dir(rtl)
+ Rtl,
+}
+
+impl Direction {
+ /// Parse a direction value.
+ pub fn parse<'i, 't>(parser: &mut CssParser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ let ident = parser.expect_ident()?;
+ Ok(Direction(match_ignore_ascii_case! { &ident,
+ "rtl" => atom!("rtl"),
+ "ltr" => atom!("ltr"),
+ _ => Atom::from(ident.as_ref()),
+ }))
+ }
+
+ /// Convert this Direction into a HorizontalDirection, if applicable
+ pub fn as_horizontal_direction(&self) -> Option<HorizontalDirection> {
+ if self.0 == atom!("ltr") {
+ Some(HorizontalDirection::Ltr)
+ } else if self.0 == atom!("rtl") {
+ Some(HorizontalDirection::Rtl)
+ } else {
+ None
+ }
+ }
+
+ /// Gets the element state relevant to this :dir() selector.
+ pub fn element_state(&self) -> ElementState {
+ match self.as_horizontal_direction() {
+ Some(HorizontalDirection::Ltr) => ElementState::LTR,
+ Some(HorizontalDirection::Rtl) => ElementState::RTL,
+ None => ElementState::empty(),
+ }
+ }
+}
+
+impl ToCss for Direction {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
diff --git a/servo/components/style/servo/media_queries.rs b/servo/components/style/servo/media_queries.rs
new file mode 100644
index 0000000000..83b12f5ff2
--- /dev/null
+++ b/servo/components/style/servo/media_queries.rs
@@ -0,0 +1,226 @@
+/* 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/. */
+
+//! Servo's media-query device and expression representation.
+
+use crate::context::QuirksMode;
+use crate::custom_properties::CssEnvironment;
+use crate::media_queries::media_feature::{AllowsRanges, ParsingRequirements};
+use crate::media_queries::media_feature::{Evaluator, MediaFeatureDescription};
+use crate::media_queries::media_feature_expression::RangeOrOperator;
+use crate::media_queries::MediaType;
+use crate::properties::ComputedValues;
+use crate::values::computed::CSSPixelLength;
+use crate::values::specified::font::FONT_MEDIUM_PX;
+use crate::values::KeyframesName;
+use app_units::Au;
+use euclid::default::Size2D as UntypedSize2D;
+use euclid::{Scale, SideOffsets2D, Size2D};
+use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+use style_traits::{CSSPixel, DevicePixel};
+
+/// A device is a structure that represents the current media a given document
+/// is displayed in.
+///
+/// This is the struct against which media queries are evaluated.
+#[derive(Debug, MallocSizeOf)]
+pub struct Device {
+ /// The current media type used by de device.
+ media_type: MediaType,
+ /// The current viewport size, in CSS pixels.
+ viewport_size: Size2D<f32, CSSPixel>,
+ /// The current device pixel ratio, from CSS pixels to device pixels.
+ device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
+ /// The current quirks mode.
+ #[ignore_malloc_size_of = "Pure stack type"]
+ quirks_mode: QuirksMode,
+
+ /// The font size of the root element
+ /// This is set when computing the style of the root
+ /// element, and used for rem units in other elements
+ ///
+ /// When computing the style of the root element, there can't be any
+ /// other style being computed at the same time, given we need the style of
+ /// the parent to compute everything else. So it is correct to just use
+ /// a relaxed atomic here.
+ #[ignore_malloc_size_of = "Pure stack type"]
+ root_font_size: AtomicU32,
+ /// Whether any styles computed in the document relied on the root font-size
+ /// by using rem units.
+ #[ignore_malloc_size_of = "Pure stack type"]
+ used_root_font_size: AtomicBool,
+ /// Whether any styles computed in the document relied on the viewport size.
+ #[ignore_malloc_size_of = "Pure stack type"]
+ used_viewport_units: AtomicBool,
+ /// The CssEnvironment object responsible of getting CSS environment
+ /// variables.
+ environment: CssEnvironment,
+}
+
+impl Device {
+ /// Trivially construct a new `Device`.
+ pub fn new(
+ media_type: MediaType,
+ quirks_mode: QuirksMode,
+ viewport_size: Size2D<f32, CSSPixel>,
+ device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>,
+ ) -> Device {
+ Device {
+ media_type,
+ viewport_size,
+ device_pixel_ratio,
+ quirks_mode,
+ // FIXME(bz): Seems dubious?
+ root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()),
+ used_root_font_size: AtomicBool::new(false),
+ used_viewport_units: AtomicBool::new(false),
+ environment: CssEnvironment,
+ }
+ }
+
+ /// Get the relevant environment to resolve `env()` functions.
+ #[inline]
+ pub fn environment(&self) -> &CssEnvironment {
+ &self.environment
+ }
+
+ /// Return the default computed values for this device.
+ pub fn default_computed_values(&self) -> &ComputedValues {
+ // FIXME(bz): This isn't really right, but it's no more wrong
+ // than what we used to do. See
+ // https://github.com/servo/servo/issues/14773 for fixing it properly.
+ ComputedValues::initial_values()
+ }
+
+ /// Get the font size of the root element (for rem)
+ pub fn root_font_size(&self) -> CSSPixelLength {
+ self.used_root_font_size.store(true, Ordering::Relaxed);
+ CSSPixelLength::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed)))
+ }
+
+ /// Set the font size of the root element (for rem)
+ pub fn set_root_font_size(&self, size: CSSPixelLength) {
+ self.root_font_size
+ .store(size.px().to_bits(), Ordering::Relaxed)
+ }
+
+ /// Get the quirks mode of the current device.
+ pub fn quirks_mode(&self) -> QuirksMode {
+ self.quirks_mode
+ }
+
+ /// Sets the body text color for the "inherit color from body" quirk.
+ ///
+ /// <https://quirks.spec.whatwg.org/#the-tables-inherit-color-from-body-quirk>
+ pub fn set_body_text_color(&self, _color: RGBA) {
+ // Servo doesn't implement this quirk (yet)
+ }
+
+ /// Whether a given animation name may be referenced from style.
+ pub fn animation_name_may_be_referenced(&self, _: &KeyframesName) -> bool {
+ // Assume it is, since we don't have any good way to prove it's not.
+ true
+ }
+
+ /// Returns whether we ever looked up the root font size of the Device.
+ pub fn used_root_font_size(&self) -> bool {
+ self.used_root_font_size.load(Ordering::Relaxed)
+ }
+
+ /// Returns the viewport size of the current device in app units, needed,
+ /// among other things, to resolve viewport units.
+ #[inline]
+ pub fn au_viewport_size(&self) -> UntypedSize2D<Au> {
+ Size2D::new(
+ Au::from_f32_px(self.viewport_size.width),
+ Au::from_f32_px(self.viewport_size.height),
+ )
+ }
+
+ /// Like the above, but records that we've used viewport units.
+ pub fn au_viewport_size_for_viewport_unit_resolution(&self) -> UntypedSize2D<Au> {
+ self.used_viewport_units.store(true, Ordering::Relaxed);
+ self.au_viewport_size()
+ }
+
+ /// Whether viewport units were used since the last device change.
+ pub fn used_viewport_units(&self) -> bool {
+ self.used_viewport_units.load(Ordering::Relaxed)
+ }
+
+ /// Returns the device pixel ratio.
+ pub fn device_pixel_ratio(&self) -> Scale<f32, CSSPixel, DevicePixel> {
+ self.device_pixel_ratio
+ }
+
+ /// Return the media type of the current device.
+ pub fn media_type(&self) -> MediaType {
+ self.media_type.clone()
+ }
+
+ /// Returns whether document colors are enabled.
+ pub fn use_document_colors(&self) -> bool {
+ true
+ }
+
+ /// Returns the default background color.
+ pub fn default_background_color(&self) -> RGBA {
+ RGBA::new(255, 255, 255, 255)
+ }
+
+ /// Returns the default color color.
+ pub fn default_color(&self) -> RGBA {
+ RGBA::new(0, 0, 0, 255)
+ }
+
+ /// Returns safe area insets
+ pub fn safe_area_insets(&self) -> SideOffsets2D<f32, CSSPixel> {
+ SideOffsets2D::zero()
+ }
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#width
+fn eval_width(
+ device: &Device,
+ value: Option<CSSPixelLength>,
+ range_or_operator: Option<RangeOrOperator>,
+) -> bool {
+ RangeOrOperator::evaluate(
+ range_or_operator,
+ value.map(Au::from),
+ device.au_viewport_size().width,
+ )
+}
+
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+enum Scan {
+ Progressive,
+ Interlace,
+}
+
+/// https://drafts.csswg.org/mediaqueries-4/#scan
+fn eval_scan(_: &Device, _: Option<Scan>) -> bool {
+ // Since we doesn't support the 'tv' media type, the 'scan' feature never
+ // matches.
+ false
+}
+
+lazy_static! {
+ /// A list with all the media features that Servo supports.
+ pub static ref MEDIA_FEATURES: [MediaFeatureDescription; 2] = [
+ feature!(
+ atom!("width"),
+ AllowsRanges::Yes,
+ Evaluator::Length(eval_width),
+ ParsingRequirements::empty(),
+ ),
+ feature!(
+ atom!("scan"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_scan, Scan),
+ ParsingRequirements::empty(),
+ ),
+ ];
+}
diff --git a/servo/components/style/servo/mod.rs b/servo/components/style/servo/mod.rs
new file mode 100644
index 0000000000..6502d28727
--- /dev/null
+++ b/servo/components/style/servo/mod.rs
@@ -0,0 +1,12 @@
+/* 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/. */
+
+//! Servo-specific bits of the style system.
+//!
+//! These get compiled out on a Gecko build.
+
+pub mod media_queries;
+pub mod restyle_damage;
+pub mod selector_parser;
+pub mod url;
diff --git a/servo/components/style/servo/restyle_damage.rs b/servo/components/style/servo/restyle_damage.rs
new file mode 100644
index 0000000000..fe17fa6198
--- /dev/null
+++ b/servo/components/style/servo/restyle_damage.rs
@@ -0,0 +1,268 @@
+/* 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/. */
+
+//! The restyle damage is a hint that tells layout which kind of operations may
+//! be needed in presence of incremental style changes.
+
+use crate::computed_values::display::T as Display;
+use crate::matching::{StyleChange, StyleDifference};
+use crate::properties::ComputedValues;
+use std::fmt;
+
+bitflags! {
+ /// Individual layout actions that may be necessary after restyling.
+ pub struct ServoRestyleDamage: u8 {
+ /// Repaint the node itself.
+ ///
+ /// Currently unused; need to decide how this propagates.
+ const REPAINT = 0x01;
+
+ /// The stacking-context-relative position of this node or its
+ /// descendants has changed.
+ ///
+ /// Propagates both up and down the flow tree.
+ const REPOSITION = 0x02;
+
+ /// Recompute the overflow regions (bounding box of object and all descendants).
+ ///
+ /// Propagates down the flow tree because the computation is bottom-up.
+ const STORE_OVERFLOW = 0x04;
+
+ /// Recompute intrinsic inline_sizes (minimum and preferred).
+ ///
+ /// Propagates down the flow tree because the computation is.
+ /// bottom-up.
+ const BUBBLE_ISIZES = 0x08;
+
+ /// Recompute actual inline-sizes and block-sizes, only taking
+ /// out-of-flow children into account.
+ ///
+ /// Propagates up the flow tree because the computation is top-down.
+ const REFLOW_OUT_OF_FLOW = 0x10;
+
+ /// Recompute actual inline_sizes and block_sizes.
+ ///
+ /// Propagates up the flow tree because the computation is top-down.
+ const REFLOW = 0x20;
+
+ /// Re-resolve generated content.
+ ///
+ /// Propagates up the flow tree because the computation is inorder.
+ const RESOLVE_GENERATED_CONTENT = 0x40;
+
+ /// The entire flow needs to be reconstructed.
+ const RECONSTRUCT_FLOW = 0x80;
+ }
+}
+
+malloc_size_of_is_0!(ServoRestyleDamage);
+
+impl ServoRestyleDamage {
+ /// Compute the `StyleDifference` (including the appropriate restyle damage)
+ /// for a given style change between `old` and `new`.
+ pub fn compute_style_difference(old: &ComputedValues, new: &ComputedValues) -> StyleDifference {
+ let damage = compute_damage(old, new);
+ let change = if damage.is_empty() {
+ StyleChange::Unchanged
+ } else {
+ // FIXME(emilio): Differentiate between reset and inherited
+ // properties here, and set `reset_only` appropriately so the
+ // optimization to skip the cascade in those cases applies.
+ StyleChange::Changed { reset_only: false }
+ };
+ StyleDifference { damage, change }
+ }
+
+ /// Returns a bitmask that represents a flow that needs to be rebuilt and
+ /// reflowed.
+ ///
+ /// FIXME(bholley): Do we ever actually need this? Shouldn't
+ /// RECONSTRUCT_FLOW imply everything else?
+ pub fn rebuild_and_reflow() -> ServoRestyleDamage {
+ ServoRestyleDamage::REPAINT |
+ ServoRestyleDamage::REPOSITION |
+ ServoRestyleDamage::STORE_OVERFLOW |
+ ServoRestyleDamage::BUBBLE_ISIZES |
+ ServoRestyleDamage::REFLOW_OUT_OF_FLOW |
+ ServoRestyleDamage::REFLOW |
+ ServoRestyleDamage::RECONSTRUCT_FLOW
+ }
+
+ /// Returns a bitmask indicating that the frame needs to be reconstructed.
+ pub fn reconstruct() -> ServoRestyleDamage {
+ ServoRestyleDamage::RECONSTRUCT_FLOW
+ }
+
+ /// Supposing a flow has the given `position` property and this damage,
+ /// returns the damage that we should add to the *parent* of this flow.
+ pub fn damage_for_parent(self, child_is_absolutely_positioned: bool) -> ServoRestyleDamage {
+ if child_is_absolutely_positioned {
+ self & (ServoRestyleDamage::REPAINT |
+ ServoRestyleDamage::REPOSITION |
+ ServoRestyleDamage::STORE_OVERFLOW |
+ ServoRestyleDamage::REFLOW_OUT_OF_FLOW |
+ ServoRestyleDamage::RESOLVE_GENERATED_CONTENT)
+ } else {
+ self & (ServoRestyleDamage::REPAINT |
+ ServoRestyleDamage::REPOSITION |
+ ServoRestyleDamage::STORE_OVERFLOW |
+ ServoRestyleDamage::REFLOW |
+ ServoRestyleDamage::REFLOW_OUT_OF_FLOW |
+ ServoRestyleDamage::RESOLVE_GENERATED_CONTENT)
+ }
+ }
+
+ /// Supposing the *parent* of a flow with the given `position` property has
+ /// this damage, returns the damage that we should add to this flow.
+ pub fn damage_for_child(
+ self,
+ parent_is_absolutely_positioned: bool,
+ child_is_absolutely_positioned: bool,
+ ) -> ServoRestyleDamage {
+ match (
+ parent_is_absolutely_positioned,
+ child_is_absolutely_positioned,
+ ) {
+ (false, true) => {
+ // Absolute children are out-of-flow and therefore insulated from changes.
+ //
+ // FIXME(pcwalton): Au contraire, if the containing block dimensions change!
+ self & (ServoRestyleDamage::REPAINT | ServoRestyleDamage::REPOSITION)
+ },
+ (true, false) => {
+ // Changing the position of an absolutely-positioned block requires us to reflow
+ // its kids.
+ if self.contains(ServoRestyleDamage::REFLOW_OUT_OF_FLOW) {
+ self | ServoRestyleDamage::REFLOW
+ } else {
+ self
+ }
+ },
+ _ => {
+ // TODO(pcwalton): Take floatedness into account.
+ self & (ServoRestyleDamage::REPAINT |
+ ServoRestyleDamage::REPOSITION |
+ ServoRestyleDamage::REFLOW)
+ },
+ }
+ }
+}
+
+impl Default for ServoRestyleDamage {
+ fn default() -> Self {
+ Self::empty()
+ }
+}
+
+impl fmt::Display for ServoRestyleDamage {
+ fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ let mut first_elem = true;
+
+ let to_iter = [
+ (ServoRestyleDamage::REPAINT, "Repaint"),
+ (ServoRestyleDamage::REPOSITION, "Reposition"),
+ (ServoRestyleDamage::STORE_OVERFLOW, "StoreOverflow"),
+ (ServoRestyleDamage::BUBBLE_ISIZES, "BubbleISizes"),
+ (ServoRestyleDamage::REFLOW_OUT_OF_FLOW, "ReflowOutOfFlow"),
+ (ServoRestyleDamage::REFLOW, "Reflow"),
+ (
+ ServoRestyleDamage::RESOLVE_GENERATED_CONTENT,
+ "ResolveGeneratedContent",
+ ),
+ (ServoRestyleDamage::RECONSTRUCT_FLOW, "ReconstructFlow"),
+ ];
+
+ for &(damage, damage_str) in &to_iter {
+ if self.contains(damage) {
+ if !first_elem {
+ write!(f, " | ")?;
+ }
+ write!(f, "{}", damage_str)?;
+ first_elem = false;
+ }
+ }
+
+ if first_elem {
+ write!(f, "NoDamage")?;
+ }
+
+ Ok(())
+ }
+}
+
+fn compute_damage(old: &ComputedValues, new: &ComputedValues) -> ServoRestyleDamage {
+ let mut damage = ServoRestyleDamage::empty();
+
+ // This should check every CSS property, as enumerated in the fields of
+ // https://doc.servo.org/style/properties/struct.ComputedValues.html
+
+ // This uses short-circuiting boolean OR for its side effects and ignores the result.
+ let _ = restyle_damage_rebuild_and_reflow!(
+ old,
+ new,
+ damage,
+ [
+ ServoRestyleDamage::REPAINT,
+ ServoRestyleDamage::REPOSITION,
+ ServoRestyleDamage::STORE_OVERFLOW,
+ ServoRestyleDamage::BUBBLE_ISIZES,
+ ServoRestyleDamage::REFLOW_OUT_OF_FLOW,
+ ServoRestyleDamage::REFLOW,
+ ServoRestyleDamage::RECONSTRUCT_FLOW
+ ]
+ ) || (new.get_box().display == Display::Inline &&
+ restyle_damage_rebuild_and_reflow_inline!(
+ old,
+ new,
+ damage,
+ [
+ ServoRestyleDamage::REPAINT,
+ ServoRestyleDamage::REPOSITION,
+ ServoRestyleDamage::STORE_OVERFLOW,
+ ServoRestyleDamage::BUBBLE_ISIZES,
+ ServoRestyleDamage::REFLOW_OUT_OF_FLOW,
+ ServoRestyleDamage::REFLOW,
+ ServoRestyleDamage::RECONSTRUCT_FLOW
+ ]
+ )) ||
+ restyle_damage_reflow!(
+ old,
+ new,
+ damage,
+ [
+ ServoRestyleDamage::REPAINT,
+ ServoRestyleDamage::REPOSITION,
+ ServoRestyleDamage::STORE_OVERFLOW,
+ ServoRestyleDamage::BUBBLE_ISIZES,
+ ServoRestyleDamage::REFLOW_OUT_OF_FLOW,
+ ServoRestyleDamage::REFLOW
+ ]
+ ) ||
+ restyle_damage_reflow_out_of_flow!(
+ old,
+ new,
+ damage,
+ [
+ ServoRestyleDamage::REPAINT,
+ ServoRestyleDamage::REPOSITION,
+ ServoRestyleDamage::STORE_OVERFLOW,
+ ServoRestyleDamage::REFLOW_OUT_OF_FLOW
+ ]
+ ) ||
+ restyle_damage_repaint!(old, new, damage, [ServoRestyleDamage::REPAINT]);
+
+ // Paint worklets may depend on custom properties,
+ // so if they have changed we should repaint.
+ if !old.custom_properties_equal(new) {
+ damage.insert(ServoRestyleDamage::REPAINT);
+ }
+
+ // If the layer requirements of this flow have changed due to the value
+ // of the transform, then reflow is required to rebuild the layers.
+ if old.transform_requires_layer() != new.transform_requires_layer() {
+ damage.insert(ServoRestyleDamage::rebuild_and_reflow());
+ }
+
+ damage
+}
diff --git a/servo/components/style/servo/selector_parser.rs b/servo/components/style/servo/selector_parser.rs
new file mode 100644
index 0000000000..b20f1754a0
--- /dev/null
+++ b/servo/components/style/servo/selector_parser.rs
@@ -0,0 +1,806 @@
+/* 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/. */
+
+#![deny(missing_docs)]
+
+//! Servo's selector parser.
+
+use crate::attr::{AttrIdentifier, AttrValue};
+use crate::dom::{OpaqueNode, TElement, TNode};
+use crate::invalidation::element::document_state::InvalidationMatchingData;
+use crate::invalidation::element::element_wrapper::ElementSnapshot;
+use crate::properties::longhands::display::computed_value::T as Display;
+use crate::properties::{ComputedValues, PropertyFlags};
+use crate::selector_parser::AttrValue as SelectorAttrValue;
+use crate::selector_parser::{PseudoElementCascadeType, SelectorParser};
+use crate::values::{AtomIdent, AtomString};
+use crate::{Atom, CaseSensitivityExt, LocalName, Namespace, Prefix};
+use cssparser::{serialize_identifier, CowRcStr, Parser as CssParser, SourceLocation, ToCss};
+use dom::{DocumentState, ElementState};
+use fxhash::FxHashMap;
+use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
+use selectors::parser::SelectorParseErrorKind;
+use selectors::visitor::SelectorVisitor;
+use std::fmt;
+use std::mem;
+use std::ops::{Deref, DerefMut};
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// A pseudo-element, both public and private.
+///
+/// NB: If you add to this list, be sure to update `each_simple_pseudo_element` too.
+#[derive(
+ Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(usize)]
+pub enum PseudoElement {
+ // Eager pseudos. Keep these first so that eager_index() works.
+ After = 0,
+ Before,
+ Selection,
+ // If/when :first-letter is added, update is_first_letter accordingly.
+
+ // If/when :first-line is added, update is_first_line accordingly.
+
+ // If/when ::first-letter, ::first-line, or ::placeholder are added, adjust
+ // our property_restriction implementation to do property filtering for
+ // them. Also, make sure the UA sheet has the !important rules some of the
+ // APPLIES_TO_PLACEHOLDER properties expect!
+
+ // Non-eager pseudos.
+ DetailsSummary,
+ DetailsContent,
+ ServoText,
+ ServoInputText,
+ ServoTableWrapper,
+ ServoAnonymousTableWrapper,
+ ServoAnonymousTable,
+ ServoAnonymousTableRow,
+ ServoAnonymousTableCell,
+ ServoAnonymousBlock,
+ ServoInlineBlockWrapper,
+ ServoInlineAbsolute,
+}
+
+/// The count of all pseudo-elements.
+pub const PSEUDO_COUNT: usize = PseudoElement::ServoInlineAbsolute as usize + 1;
+
+impl ::selectors::parser::PseudoElement for PseudoElement {
+ type Impl = SelectorImpl;
+}
+
+impl ToCss for PseudoElement {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use self::PseudoElement::*;
+ dest.write_str(match *self {
+ After => "::after",
+ Before => "::before",
+ Selection => "::selection",
+ DetailsSummary => "::-servo-details-summary",
+ DetailsContent => "::-servo-details-content",
+ ServoText => "::-servo-text",
+ ServoInputText => "::-servo-input-text",
+ ServoTableWrapper => "::-servo-table-wrapper",
+ ServoAnonymousTableWrapper => "::-servo-anonymous-table-wrapper",
+ ServoAnonymousTable => "::-servo-anonymous-table",
+ ServoAnonymousTableRow => "::-servo-anonymous-table-row",
+ ServoAnonymousTableCell => "::-servo-anonymous-table-cell",
+ ServoAnonymousBlock => "::-servo-anonymous-block",
+ ServoInlineBlockWrapper => "::-servo-inline-block-wrapper",
+ ServoInlineAbsolute => "::-servo-inline-absolute",
+ })
+ }
+}
+
+/// The number of eager pseudo-elements. Keep this in sync with cascade_type.
+pub const EAGER_PSEUDO_COUNT: usize = 3;
+
+impl PseudoElement {
+ /// Gets the canonical index of this eagerly-cascaded pseudo-element.
+ #[inline]
+ pub fn eager_index(&self) -> usize {
+ debug_assert!(self.is_eager());
+ self.clone() as usize
+ }
+
+ /// An index for this pseudo-element to be indexed in an enumerated array.
+ #[inline]
+ pub fn index(&self) -> usize {
+ self.clone() as usize
+ }
+
+ /// An array of `None`, one per pseudo-element.
+ pub fn pseudo_none_array<T>() -> [Option<T>; PSEUDO_COUNT] {
+ Default::default()
+ }
+
+ /// Creates a pseudo-element from an eager index.
+ #[inline]
+ pub fn from_eager_index(i: usize) -> Self {
+ assert!(i < EAGER_PSEUDO_COUNT);
+ let result: PseudoElement = unsafe { mem::transmute(i) };
+ debug_assert!(result.is_eager());
+ result
+ }
+
+ /// Whether the current pseudo element is ::before or ::after.
+ #[inline]
+ pub fn is_before_or_after(&self) -> bool {
+ self.is_before() || self.is_after()
+ }
+
+ /// Whether this is an unknown ::-webkit- pseudo-element.
+ #[inline]
+ pub fn is_unknown_webkit_pseudo_element(&self) -> bool {
+ false
+ }
+
+ /// Whether this pseudo-element is the ::marker pseudo.
+ #[inline]
+ pub fn is_marker(&self) -> bool {
+ false
+ }
+
+ /// Whether this pseudo-element is the ::selection pseudo.
+ #[inline]
+ pub fn is_selection(&self) -> bool {
+ *self == PseudoElement::Selection
+ }
+
+ /// Whether this pseudo-element is the ::before pseudo.
+ #[inline]
+ pub fn is_before(&self) -> bool {
+ *self == PseudoElement::Before
+ }
+
+ /// Whether this pseudo-element is the ::after pseudo.
+ #[inline]
+ pub fn is_after(&self) -> bool {
+ *self == PseudoElement::After
+ }
+
+ /// Whether the current pseudo element is :first-letter
+ #[inline]
+ pub fn is_first_letter(&self) -> bool {
+ false
+ }
+
+ /// Whether the current pseudo element is :first-line
+ #[inline]
+ pub fn is_first_line(&self) -> bool {
+ false
+ }
+
+ /// Whether this pseudo-element is the ::-moz-color-swatch pseudo.
+ #[inline]
+ pub fn is_color_swatch(&self) -> bool {
+ false
+ }
+
+ /// Whether this pseudo-element is eagerly-cascaded.
+ #[inline]
+ pub fn is_eager(&self) -> bool {
+ self.cascade_type() == PseudoElementCascadeType::Eager
+ }
+
+ /// Whether this pseudo-element is lazily-cascaded.
+ #[inline]
+ pub fn is_lazy(&self) -> bool {
+ self.cascade_type() == PseudoElementCascadeType::Lazy
+ }
+
+ /// Whether this pseudo-element is for an anonymous box.
+ pub fn is_anon_box(&self) -> bool {
+ self.is_precomputed()
+ }
+
+ /// Whether this pseudo-element skips flex/grid container display-based
+ /// fixup.
+ #[inline]
+ pub fn skip_item_display_fixup(&self) -> bool {
+ !self.is_before_or_after()
+ }
+
+ /// Whether this pseudo-element is precomputed.
+ #[inline]
+ pub fn is_precomputed(&self) -> bool {
+ self.cascade_type() == PseudoElementCascadeType::Precomputed
+ }
+
+ /// Returns which kind of cascade type has this pseudo.
+ ///
+ /// For more info on cascade types, see docs/components/style.md
+ ///
+ /// Note: Keep this in sync with EAGER_PSEUDO_COUNT.
+ #[inline]
+ pub fn cascade_type(&self) -> PseudoElementCascadeType {
+ match *self {
+ PseudoElement::After | PseudoElement::Before | PseudoElement::Selection => {
+ PseudoElementCascadeType::Eager
+ },
+ PseudoElement::DetailsSummary => PseudoElementCascadeType::Lazy,
+ PseudoElement::DetailsContent |
+ PseudoElement::ServoText |
+ PseudoElement::ServoInputText |
+ PseudoElement::ServoTableWrapper |
+ PseudoElement::ServoAnonymousTableWrapper |
+ PseudoElement::ServoAnonymousTable |
+ PseudoElement::ServoAnonymousTableRow |
+ PseudoElement::ServoAnonymousTableCell |
+ PseudoElement::ServoAnonymousBlock |
+ PseudoElement::ServoInlineBlockWrapper |
+ PseudoElement::ServoInlineAbsolute => PseudoElementCascadeType::Precomputed,
+ }
+ }
+
+ /// Covert non-canonical pseudo-element to canonical one, and keep a
+ /// canonical one as it is.
+ pub fn canonical(&self) -> PseudoElement {
+ self.clone()
+ }
+
+ /// Stub, only Gecko needs this
+ pub fn pseudo_info(&self) {
+ ()
+ }
+
+ /// Property flag that properties must have to apply to this pseudo-element.
+ #[inline]
+ pub fn property_restriction(&self) -> Option<PropertyFlags> {
+ None
+ }
+
+ /// Whether this pseudo-element should actually exist if it has
+ /// the given styles.
+ pub fn should_exist(&self, style: &ComputedValues) -> bool {
+ let display = style.get_box().clone_display();
+ if display == Display::None {
+ return false;
+ }
+ if self.is_before_or_after() && style.ineffective_content_property() {
+ return false;
+ }
+
+ true
+ }
+}
+
+/// The type used for storing `:lang` arguments.
+pub type Lang = Box<str>;
+
+/// A non tree-structural pseudo-class.
+/// See https://drafts.csswg.org/selectors-4/#structural-pseudos
+#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)]
+#[allow(missing_docs)]
+pub enum NonTSPseudoClass {
+ Active,
+ AnyLink,
+ Checked,
+ Defined,
+ Disabled,
+ Enabled,
+ Focus,
+ Fullscreen,
+ Hover,
+ Indeterminate,
+ Lang(Lang),
+ Link,
+ PlaceholderShown,
+ ReadWrite,
+ ReadOnly,
+ ServoNonZeroBorder,
+ Target,
+ Visited,
+}
+
+impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass {
+ type Impl = SelectorImpl;
+
+ #[inline]
+ fn is_active_or_hover(&self) -> bool {
+ matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover)
+ }
+
+ #[inline]
+ fn is_user_action_state(&self) -> bool {
+ matches!(
+ *self,
+ NonTSPseudoClass::Active | NonTSPseudoClass::Hover | NonTSPseudoClass::Focus
+ )
+ }
+
+ fn visit<V>(&self, _: &mut V) -> bool
+ where
+ V: SelectorVisitor<Impl = Self::Impl>,
+ {
+ true
+ }
+}
+
+impl ToCss for NonTSPseudoClass {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use self::NonTSPseudoClass::*;
+ if let Lang(ref lang) = *self {
+ dest.write_str(":lang(")?;
+ serialize_identifier(lang, dest)?;
+ return dest.write_char(')');
+ }
+
+ dest.write_str(match *self {
+ Active => ":active",
+ AnyLink => ":any-link",
+ Checked => ":checked",
+ Defined => ":defined",
+ Disabled => ":disabled",
+ Enabled => ":enabled",
+ Focus => ":focus",
+ Fullscreen => ":fullscreen",
+ Hover => ":hover",
+ Indeterminate => ":indeterminate",
+ Link => ":link",
+ PlaceholderShown => ":placeholder-shown",
+ ReadWrite => ":read-write",
+ ReadOnly => ":read-only",
+ ServoNonZeroBorder => ":-servo-nonzero-border",
+ Target => ":target",
+ Visited => ":visited",
+ Lang(_) => unreachable!(),
+ })
+ }
+}
+
+impl NonTSPseudoClass {
+ /// Gets a given state flag for this pseudo-class. This is used to do
+ /// selector matching, and it's set from the DOM.
+ pub fn state_flag(&self) -> ElementState {
+ use self::NonTSPseudoClass::*;
+ match *self {
+ Active => ElementState::IN_ACTIVE_STATE,
+ Focus => ElementState::IN_FOCUS_STATE,
+ Fullscreen => ElementState::IN_FULLSCREEN_STATE,
+ Hover => ElementState::IN_HOVER_STATE,
+ Defined => ElementState::IN_DEFINED_STATE,
+ Enabled => ElementState::IN_ENABLED_STATE,
+ Disabled => ElementState::IN_DISABLED_STATE,
+ Checked => ElementState::IN_CHECKED_STATE,
+ Indeterminate => ElementState::IN_INDETERMINATE_STATE,
+ ReadOnly | ReadWrite => ElementState::IN_READWRITE_STATE,
+ PlaceholderShown => ElementState::IN_PLACEHOLDER_SHOWN_STATE,
+ Target => ElementState::IN_TARGET_STATE,
+
+ AnyLink | Lang(_) | Link | Visited | ServoNonZeroBorder => ElementState::empty(),
+ }
+ }
+
+ /// Get the document state flag associated with a pseudo-class, if any.
+ pub fn document_state_flag(&self) -> DocumentState {
+ DocumentState::empty()
+ }
+
+ /// Returns true if the given pseudoclass should trigger style sharing cache revalidation.
+ pub fn needs_cache_revalidation(&self) -> bool {
+ self.state_flag().is_empty()
+ }
+}
+
+/// The abstract struct we implement the selector parser implementation on top
+/// of.
+#[derive(Clone, Debug, PartialEq)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct SelectorImpl;
+
+impl ::selectors::SelectorImpl for SelectorImpl {
+ type PseudoElement = PseudoElement;
+ type NonTSPseudoClass = NonTSPseudoClass;
+
+ type ExtraMatchingData = InvalidationMatchingData;
+ type AttrValue = String;
+ type Identifier = Atom;
+ type ClassName = Atom;
+ type PartName = Atom;
+ type LocalName = LocalName;
+ type NamespacePrefix = Prefix;
+ type NamespaceUrl = Namespace;
+ type BorrowedLocalName = LocalName;
+ type BorrowedNamespaceUrl = Namespace;
+}
+
+impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> {
+ type Impl = SelectorImpl;
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_non_ts_pseudo_class(
+ &self,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ ) -> Result<NonTSPseudoClass, ParseError<'i>> {
+ use self::NonTSPseudoClass::*;
+ let pseudo_class = match_ignore_ascii_case! { &name,
+ "active" => Active,
+ "any-link" => AnyLink,
+ "checked" => Checked,
+ "defined" => Defined,
+ "disabled" => Disabled,
+ "enabled" => Enabled,
+ "focus" => Focus,
+ "fullscreen" => Fullscreen,
+ "hover" => Hover,
+ "indeterminate" => Indeterminate,
+ "-moz-inert" => MozInert,
+ "link" => Link,
+ "placeholder-shown" => PlaceholderShown,
+ "read-write" => ReadWrite,
+ "read-only" => ReadOnly,
+ "target" => Target,
+ "visited" => Visited,
+ "-servo-nonzero-border" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent("-servo-nonzero-border".into())
+ ))
+ }
+ ServoNonZeroBorder
+ },
+ _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
+ };
+
+ Ok(pseudo_class)
+ }
+
+ fn parse_non_ts_functional_pseudo_class<'t>(
+ &self,
+ name: CowRcStr<'i>,
+ parser: &mut CssParser<'i, 't>,
+ ) -> Result<NonTSPseudoClass, ParseError<'i>> {
+ use self::NonTSPseudoClass::*;
+ let pseudo_class = match_ignore_ascii_case! { &name,
+ "lang" => {
+ Lang(parser.expect_ident_or_string()?.as_ref().into())
+ },
+ _ => return Err(parser.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
+ };
+
+ Ok(pseudo_class)
+ }
+
+ fn parse_pseudo_element(
+ &self,
+ location: SourceLocation,
+ name: CowRcStr<'i>,
+ ) -> Result<PseudoElement, ParseError<'i>> {
+ use self::PseudoElement::*;
+ let pseudo_element = match_ignore_ascii_case! { &name,
+ "before" => Before,
+ "after" => After,
+ "selection" => Selection,
+ "-servo-details-summary" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ DetailsSummary
+ },
+ "-servo-details-content" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ DetailsContent
+ },
+ "-servo-text" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoText
+ },
+ "-servo-input-text" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoInputText
+ },
+ "-servo-table-wrapper" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoTableWrapper
+ },
+ "-servo-anonymous-table-wrapper" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoAnonymousTableWrapper
+ },
+ "-servo-anonymous-table" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoAnonymousTable
+ },
+ "-servo-anonymous-table-row" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoAnonymousTableRow
+ },
+ "-servo-anonymous-table-cell" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoAnonymousTableCell
+ },
+ "-servo-anonymous-block" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoAnonymousBlock
+ },
+ "-servo-inline-block-wrapper" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoInlineBlockWrapper
+ },
+ "-servo-inline-absolute" => {
+ if !self.in_user_agent_stylesheet() {
+ return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+ }
+ ServoInlineAbsolute
+ },
+ _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())))
+
+ };
+
+ Ok(pseudo_element)
+ }
+
+ fn default_namespace(&self) -> Option<Namespace> {
+ self.namespaces.default.as_ref().map(|ns| ns.clone())
+ }
+
+ fn namespace_for_prefix(&self, prefix: &Prefix) -> Option<Namespace> {
+ self.namespaces.prefixes.get(prefix).cloned()
+ }
+}
+
+impl SelectorImpl {
+ /// A helper to traverse each eagerly cascaded pseudo-element, executing
+ /// `fun` on it.
+ #[inline]
+ pub fn each_eagerly_cascaded_pseudo_element<F>(mut fun: F)
+ where
+ F: FnMut(PseudoElement),
+ {
+ for i in 0..EAGER_PSEUDO_COUNT {
+ fun(PseudoElement::from_eager_index(i));
+ }
+ }
+}
+
+/// A map from elements to snapshots for the Servo style back-end.
+#[derive(Debug)]
+pub struct SnapshotMap(FxHashMap<OpaqueNode, ServoElementSnapshot>);
+
+impl SnapshotMap {
+ /// Create a new empty `SnapshotMap`.
+ pub fn new() -> Self {
+ SnapshotMap(FxHashMap::default())
+ }
+
+ /// Get a snapshot given an element.
+ pub fn get<T: TElement>(&self, el: &T) -> Option<&ServoElementSnapshot> {
+ self.0.get(&el.as_node().opaque())
+ }
+}
+
+impl Deref for SnapshotMap {
+ type Target = FxHashMap<OpaqueNode, ServoElementSnapshot>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for SnapshotMap {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+/// Servo's version of an element snapshot.
+#[derive(Debug, Default, MallocSizeOf)]
+pub struct ServoElementSnapshot {
+ /// The stored state of the element.
+ pub state: Option<ElementState>,
+ /// The set of stored attributes and its values.
+ pub attrs: Option<Vec<(AttrIdentifier, AttrValue)>>,
+ /// The set of changed attributes and its values.
+ pub changed_attrs: Vec<LocalName>,
+ /// Whether the class attribute changed or not.
+ pub class_changed: bool,
+ /// Whether the id attribute changed or not.
+ pub id_changed: bool,
+ /// Whether other attributes other than id or class changed or not.
+ pub other_attributes_changed: bool,
+}
+
+impl ServoElementSnapshot {
+ /// Create an empty element snapshot.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Returns whether the id attribute changed or not.
+ pub fn id_changed(&self) -> bool {
+ self.id_changed
+ }
+
+ /// Returns whether the class attribute changed or not.
+ pub fn class_changed(&self) -> bool {
+ self.class_changed
+ }
+
+ /// Returns whether other attributes other than id or class changed or not.
+ pub fn other_attr_changed(&self) -> bool {
+ self.other_attributes_changed
+ }
+
+ fn get_attr(&self, namespace: &Namespace, name: &LocalName) -> Option<&AttrValue> {
+ self.attrs
+ .as_ref()
+ .unwrap()
+ .iter()
+ .find(|&&(ref ident, _)| ident.local_name == *name && ident.namespace == *namespace)
+ .map(|&(_, ref v)| v)
+ }
+
+ /// Executes the callback once for each attribute that changed.
+ #[inline]
+ pub fn each_attr_changed<F>(&self, mut callback: F)
+ where
+ F: FnMut(&LocalName),
+ {
+ for name in &self.changed_attrs {
+ callback(name)
+ }
+ }
+
+ fn any_attr_ignore_ns<F>(&self, name: &LocalName, mut f: F) -> bool
+ where
+ F: FnMut(&AttrValue) -> bool,
+ {
+ self.attrs
+ .as_ref()
+ .unwrap()
+ .iter()
+ .any(|&(ref ident, ref v)| ident.local_name == *name && f(v))
+ }
+}
+
+impl ElementSnapshot for ServoElementSnapshot {
+ fn state(&self) -> Option<ElementState> {
+ self.state.clone()
+ }
+
+ fn has_attrs(&self) -> bool {
+ self.attrs.is_some()
+ }
+
+ fn id_attr(&self) -> Option<&Atom> {
+ self.get_attr(&ns!(), &local_name!("id"))
+ .map(|v| v.as_atom())
+ }
+
+ fn is_part(&self, _name: &AtomIdent) -> bool {
+ false
+ }
+
+ fn imported_part(&self, _: &AtomIdent) -> Option<AtomIdent> {
+ None
+ }
+
+ fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
+ self.get_attr(&ns!(), &local_name!("class"))
+ .map_or(false, |v| {
+ v.as_tokens()
+ .iter()
+ .any(|atom| case_sensitivity.eq_atom(atom, name))
+ })
+ }
+
+ fn each_class<F>(&self, mut callback: F)
+ where
+ F: FnMut(&AtomIdent),
+ {
+ if let Some(v) = self.get_attr(&ns!(), &local_name!("class")) {
+ for class in v.as_tokens() {
+ callback(AtomIdent::cast(class));
+ }
+ }
+ }
+
+ fn lang_attr(&self) -> Option<SelectorAttrValue> {
+ self.get_attr(&ns!(xml), &local_name!("lang"))
+ .or_else(|| self.get_attr(&ns!(), &local_name!("lang")))
+ .map(|v| SelectorAttrValue::from(v as &str))
+ }
+}
+
+impl ServoElementSnapshot {
+ /// selectors::Element::attr_matches
+ pub fn attr_matches(
+ &self,
+ ns: &NamespaceConstraint<&Namespace>,
+ local_name: &LocalName,
+ operation: &AttrSelectorOperation<&AtomString>,
+ ) -> bool {
+ match *ns {
+ NamespaceConstraint::Specific(ref ns) => self
+ .get_attr(ns, local_name)
+ .map_or(false, |value| value.eval_selector(operation)),
+ NamespaceConstraint::Any => {
+ self.any_attr_ignore_ns(local_name, |value| value.eval_selector(operation))
+ },
+ }
+ }
+}
+
+/// Returns whether the language is matched, as defined by
+/// [RFC 4647](https://tools.ietf.org/html/rfc4647#section-3.3.2).
+pub fn extended_filtering(tag: &str, range: &str) -> bool {
+ range.split(',').any(|lang_range| {
+ // step 1
+ let mut range_subtags = lang_range.split('\x2d');
+ let mut tag_subtags = tag.split('\x2d');
+
+ // step 2
+ // Note: [Level-4 spec](https://drafts.csswg.org/selectors/#lang-pseudo) check for wild card
+ if let (Some(range_subtag), Some(tag_subtag)) = (range_subtags.next(), tag_subtags.next()) {
+ if !(range_subtag.eq_ignore_ascii_case(tag_subtag) ||
+ range_subtag.eq_ignore_ascii_case("*"))
+ {
+ return false;
+ }
+ }
+
+ let mut current_tag_subtag = tag_subtags.next();
+
+ // step 3
+ for range_subtag in range_subtags {
+ // step 3a
+ if range_subtag == "*" {
+ continue;
+ }
+ match current_tag_subtag.clone() {
+ Some(tag_subtag) => {
+ // step 3c
+ if range_subtag.eq_ignore_ascii_case(tag_subtag) {
+ current_tag_subtag = tag_subtags.next();
+ continue;
+ }
+ // step 3d
+ if tag_subtag.len() == 1 {
+ return false;
+ }
+ // else step 3e - continue with loop
+ current_tag_subtag = tag_subtags.next();
+ if current_tag_subtag.is_none() {
+ return false;
+ }
+ },
+ // step 3b
+ None => {
+ return false;
+ },
+ }
+ }
+ // step 4
+ true
+ })
+}
diff --git a/servo/components/style/servo/url.rs b/servo/components/style/servo/url.rs
new file mode 100644
index 0000000000..2186be7aab
--- /dev/null
+++ b/servo/components/style/servo/url.rs
@@ -0,0 +1,238 @@
+/* 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/. */
+
+//! Common handling for the specified value CSS url() values.
+
+use crate::parser::{Parse, ParserContext};
+use crate::stylesheets::CorsMode;
+use crate::values::computed::{Context, ToComputedValue};
+use cssparser::Parser;
+use servo_arc::Arc;
+use servo_url::ServoUrl;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A CSS url() value for servo.
+///
+/// Servo eagerly resolves SpecifiedUrls, which it can then take advantage of
+/// when computing values. In contrast, Gecko uses a different URL backend, so
+/// eagerly resolving with rust-url would be duplicated work.
+///
+/// However, this approach is still not necessarily optimal: See
+/// <https://bugzilla.mozilla.org/show_bug.cgi?id=1347435#c6>
+///
+/// TODO(emilio): This should be shrunk by making CssUrl a wrapper type of an
+/// arc, and keep the serialization in that Arc. See gecko/url.rs for example.
+#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize, SpecifiedValueInfo, ToShmem)]
+pub struct CssUrl {
+ /// The original URI. This might be optional since we may insert computed
+ /// values of images into the cascade directly, and we don't bother to
+ /// convert their serialization.
+ ///
+ /// Refcounted since cloning this should be cheap and data: uris can be
+ /// really large.
+ #[ignore_malloc_size_of = "Arc"]
+ original: Option<Arc<String>>,
+
+ /// The resolved value for the url, if valid.
+ resolved: Option<ServoUrl>,
+}
+
+impl CssUrl {
+ /// Try to parse a URL from a string value that is a valid CSS token for a
+ /// URL.
+ ///
+ /// FIXME(emilio): Should honor CorsMode.
+ pub fn parse_from_string(url: String, context: &ParserContext, _: CorsMode) -> Self {
+ let serialization = Arc::new(url);
+ let resolved = context.url_data.join(&serialization).ok();
+ CssUrl {
+ original: Some(serialization),
+ resolved: resolved,
+ }
+ }
+
+ /// Returns true if the URL is definitely invalid. For Servo URLs, we can
+ /// use its |resolved| status.
+ pub fn is_invalid(&self) -> bool {
+ self.resolved.is_none()
+ }
+
+ /// Returns true if this URL looks like a fragment.
+ /// See https://drafts.csswg.org/css-values/#local-urls
+ ///
+ /// Since Servo currently stores resolved URLs, this is hard to implement. We
+ /// either need to change servo to lazily resolve (like Gecko), or note this
+ /// information in the tokenizer.
+ pub fn is_fragment(&self) -> bool {
+ error!("Can't determine whether the url is a fragment.");
+ false
+ }
+
+ /// Returns the resolved url if it was valid.
+ pub fn url(&self) -> Option<&ServoUrl> {
+ self.resolved.as_ref()
+ }
+
+ /// Return the resolved url as string, or the empty string if it's invalid.
+ ///
+ /// TODO(emilio): Should we return the original one if needed?
+ pub fn as_str(&self) -> &str {
+ match self.resolved {
+ Some(ref url) => url.as_str(),
+ None => "",
+ }
+ }
+
+ /// Creates an already specified url value from an already resolved URL
+ /// for insertion in the cascade.
+ pub fn for_cascade(url: ServoUrl) -> Self {
+ CssUrl {
+ original: None,
+ resolved: Some(url),
+ }
+ }
+
+ /// Gets a new url from a string for unit tests.
+ pub fn new_for_testing(url: &str) -> Self {
+ CssUrl {
+ original: Some(Arc::new(url.into())),
+ resolved: ServoUrl::parse(url).ok(),
+ }
+ }
+
+ /// Parses a URL request and records that the corresponding request needs to
+ /// be CORS-enabled.
+ ///
+ /// This is only for shape images and masks in Gecko, thus unimplemented for
+ /// now so somebody notices when trying to do so.
+ pub fn parse_with_cors_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ ) -> Result<Self, ParseError<'i>> {
+ let url = input.expect_url()?;
+ Ok(Self::parse_from_string(
+ url.as_ref().to_owned(),
+ context,
+ cors_mode,
+ ))
+ }
+}
+
+impl Parse for CssUrl {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_cors_mode(context, input, CorsMode::None)
+ }
+}
+
+impl PartialEq for CssUrl {
+ fn eq(&self, other: &Self) -> bool {
+ // TODO(emilio): maybe we care about equality of the specified values if
+ // present? Seems not.
+ self.resolved == other.resolved
+ }
+}
+
+impl Eq for CssUrl {}
+
+impl ToCss for CssUrl {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let string = match self.original {
+ Some(ref original) => &**original,
+ None => match self.resolved {
+ Some(ref url) => url.as_str(),
+ // This can only happen if the url wasn't specified by the
+ // user *and* it's an invalid url that has been transformed
+ // back to specified value via the "uncompute" functionality.
+ None => "about:invalid",
+ },
+ };
+
+ dest.write_str("url(")?;
+ string.to_css(dest)?;
+ dest.write_char(')')
+ }
+}
+
+/// A specified url() value for servo.
+pub type SpecifiedUrl = CssUrl;
+
+impl ToComputedValue for SpecifiedUrl {
+ type ComputedValue = ComputedUrl;
+
+ // If we can't resolve the URL from the specified one, we fall back to the original
+ // but still return it as a ComputedUrl::Invalid
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ match self.resolved {
+ Some(ref url) => ComputedUrl::Valid(url.clone()),
+ None => match self.original {
+ Some(ref url) => ComputedUrl::Invalid(url.clone()),
+ None => {
+ unreachable!("Found specified url with neither resolved or original URI!");
+ },
+ },
+ }
+ }
+
+ fn from_computed_value(computed: &ComputedUrl) -> Self {
+ match *computed {
+ ComputedUrl::Valid(ref url) => SpecifiedUrl {
+ original: None,
+ resolved: Some(url.clone()),
+ },
+ ComputedUrl::Invalid(ref url) => SpecifiedUrl {
+ original: Some(url.clone()),
+ resolved: None,
+ },
+ }
+ }
+}
+
+/// A specified image url() value for servo.
+pub type SpecifiedImageUrl = CssUrl;
+
+/// The computed value of a CSS `url()`, resolved relative to the stylesheet URL.
+#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
+pub enum ComputedUrl {
+ /// The `url()` was invalid or it wasn't specified by the user.
+ Invalid(#[ignore_malloc_size_of = "Arc"] Arc<String>),
+ /// The resolved `url()` relative to the stylesheet URL.
+ Valid(ServoUrl),
+}
+
+impl ComputedUrl {
+ /// Returns the resolved url if it was valid.
+ pub fn url(&self) -> Option<&ServoUrl> {
+ match *self {
+ ComputedUrl::Valid(ref url) => Some(url),
+ _ => None,
+ }
+ }
+}
+
+impl ToCss for ComputedUrl {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let string = match *self {
+ ComputedUrl::Valid(ref url) => url.as_str(),
+ ComputedUrl::Invalid(ref invalid_string) => invalid_string,
+ };
+
+ dest.write_str("url(")?;
+ string.to_css(dest)?;
+ dest.write_char(')')
+ }
+}
+
+/// The computed value of a CSS `url()` for image.
+pub type ComputedImageUrl = ComputedUrl;
diff --git a/servo/components/style/shared_lock.rs b/servo/components/style/shared_lock.rs
new file mode 100644
index 0000000000..55708a9f7b
--- /dev/null
+++ b/servo/components/style/shared_lock.rs
@@ -0,0 +1,374 @@
+/* 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/. */
+
+//! Different objects protected by the same lock
+
+use crate::str::{CssString, CssStringWriter};
+use crate::stylesheets::Origin;
+#[cfg(feature = "gecko")]
+use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
+#[cfg(feature = "servo")]
+use parking_lot::RwLock;
+use servo_arc::Arc;
+use std::cell::UnsafeCell;
+use std::fmt;
+#[cfg(feature = "servo")]
+use std::mem;
+#[cfg(feature = "gecko")]
+use std::ptr;
+use to_shmem::{SharedMemoryBuilder, ToShmem};
+
+/// A shared read/write lock that can protect multiple objects.
+///
+/// In Gecko builds, we don't need the blocking behavior, just the safety. As
+/// such we implement this with an AtomicRefCell instead in Gecko builds,
+/// which is ~2x as fast, and panics (rather than deadlocking) when things go
+/// wrong (which is much easier to debug on CI).
+///
+/// Servo needs the blocking behavior for its unsynchronized animation setup,
+/// but that may not be web-compatible and may need to be changed (at which
+/// point Servo could use AtomicRefCell too).
+///
+/// Gecko also needs the ability to have "read only" SharedRwLocks, which are
+/// used for objects stored in (read only) shared memory. Attempting to acquire
+/// write access to objects protected by a read only SharedRwLock will panic.
+#[derive(Clone)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct SharedRwLock {
+ #[cfg(feature = "servo")]
+ #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")]
+ arc: Arc<RwLock<()>>,
+
+ #[cfg(feature = "gecko")]
+ cell: Option<Arc<AtomicRefCell<SomethingZeroSizedButTyped>>>,
+}
+
+#[cfg(feature = "gecko")]
+struct SomethingZeroSizedButTyped;
+
+impl fmt::Debug for SharedRwLock {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("SharedRwLock")
+ }
+}
+
+impl SharedRwLock {
+ /// Create a new shared lock (servo).
+ #[cfg(feature = "servo")]
+ pub fn new() -> Self {
+ SharedRwLock {
+ arc: Arc::new(RwLock::new(())),
+ }
+ }
+
+ /// Create a new shared lock (gecko).
+ #[cfg(feature = "gecko")]
+ pub fn new() -> Self {
+ SharedRwLock {
+ cell: Some(Arc::new(AtomicRefCell::new(SomethingZeroSizedButTyped))),
+ }
+ }
+
+ /// Create a new global shared lock (servo).
+ #[cfg(feature = "servo")]
+ pub fn new_leaked() -> Self {
+ SharedRwLock {
+ arc: Arc::new_leaked(RwLock::new(())),
+ }
+ }
+
+ /// Create a new global shared lock (gecko).
+ #[cfg(feature = "gecko")]
+ pub fn new_leaked() -> Self {
+ SharedRwLock {
+ cell: Some(Arc::new_leaked(AtomicRefCell::new(
+ SomethingZeroSizedButTyped,
+ ))),
+ }
+ }
+
+ /// Create a new read-only shared lock (gecko).
+ #[cfg(feature = "gecko")]
+ pub fn read_only() -> Self {
+ SharedRwLock { cell: None }
+ }
+
+ #[cfg(feature = "gecko")]
+ #[inline]
+ fn ptr(&self) -> *const SomethingZeroSizedButTyped {
+ self.cell
+ .as_ref()
+ .map(|cell| cell.as_ptr() as *const _)
+ .unwrap_or(ptr::null())
+ }
+
+ /// Wrap the given data to make its access protected by this lock.
+ pub fn wrap<T>(&self, data: T) -> Locked<T> {
+ Locked {
+ shared_lock: self.clone(),
+ data: UnsafeCell::new(data),
+ }
+ }
+
+ /// Obtain the lock for reading (servo).
+ #[cfg(feature = "servo")]
+ pub fn read(&self) -> SharedRwLockReadGuard {
+ mem::forget(self.arc.read());
+ SharedRwLockReadGuard(self)
+ }
+
+ /// Obtain the lock for reading (gecko).
+ #[cfg(feature = "gecko")]
+ pub fn read(&self) -> SharedRwLockReadGuard {
+ SharedRwLockReadGuard(self.cell.as_ref().map(|cell| cell.borrow()))
+ }
+
+ /// Obtain the lock for writing (servo).
+ #[cfg(feature = "servo")]
+ pub fn write(&self) -> SharedRwLockWriteGuard {
+ mem::forget(self.arc.write());
+ SharedRwLockWriteGuard(self)
+ }
+
+ /// Obtain the lock for writing (gecko).
+ #[cfg(feature = "gecko")]
+ pub fn write(&self) -> SharedRwLockWriteGuard {
+ SharedRwLockWriteGuard(self.cell.as_ref().unwrap().borrow_mut())
+ }
+}
+
+/// Proof that a shared lock was obtained for reading (servo).
+#[cfg(feature = "servo")]
+pub struct SharedRwLockReadGuard<'a>(&'a SharedRwLock);
+/// Proof that a shared lock was obtained for reading (gecko).
+#[cfg(feature = "gecko")]
+pub struct SharedRwLockReadGuard<'a>(Option<AtomicRef<'a, SomethingZeroSizedButTyped>>);
+#[cfg(feature = "servo")]
+impl<'a> Drop for SharedRwLockReadGuard<'a> {
+ fn drop(&mut self) {
+ // Unsafe: self.lock is private to this module, only ever set after `read()`,
+ // and never copied or cloned (see `compile_time_assert` below).
+ unsafe { self.0.arc.force_unlock_read() }
+ }
+}
+
+impl<'a> SharedRwLockReadGuard<'a> {
+ #[inline]
+ #[cfg(feature = "gecko")]
+ fn ptr(&self) -> *const SomethingZeroSizedButTyped {
+ self.0
+ .as_ref()
+ .map(|r| &**r as *const _)
+ .unwrap_or(ptr::null())
+ }
+}
+
+/// Proof that a shared lock was obtained for writing (servo).
+#[cfg(feature = "servo")]
+pub struct SharedRwLockWriteGuard<'a>(&'a SharedRwLock);
+/// Proof that a shared lock was obtained for writing (gecko).
+#[cfg(feature = "gecko")]
+pub struct SharedRwLockWriteGuard<'a>(AtomicRefMut<'a, SomethingZeroSizedButTyped>);
+#[cfg(feature = "servo")]
+impl<'a> Drop for SharedRwLockWriteGuard<'a> {
+ fn drop(&mut self) {
+ // Unsafe: self.lock is private to this module, only ever set after `write()`,
+ // and never copied or cloned (see `compile_time_assert` below).
+ unsafe { self.0.arc.force_unlock_write() }
+ }
+}
+
+/// Data protect by a shared lock.
+pub struct Locked<T> {
+ shared_lock: SharedRwLock,
+ data: UnsafeCell<T>,
+}
+
+// Unsafe: the data inside `UnsafeCell` is only accessed in `read_with` and `write_with`,
+// where guards ensure synchronization.
+unsafe impl<T: Send> Send for Locked<T> {}
+unsafe impl<T: Send + Sync> Sync for Locked<T> {}
+
+impl<T: fmt::Debug> fmt::Debug for Locked<T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let guard = self.shared_lock.read();
+ self.read_with(&guard).fmt(f)
+ }
+}
+
+impl<T> Locked<T> {
+ #[cfg(feature = "gecko")]
+ #[inline]
+ fn is_read_only_lock(&self) -> bool {
+ self.shared_lock.cell.is_none()
+ }
+
+ #[cfg(feature = "servo")]
+ fn same_lock_as(&self, lock: &SharedRwLock) -> bool {
+ Arc::ptr_eq(&self.shared_lock.arc, &lock.arc)
+ }
+
+ #[cfg(feature = "gecko")]
+ fn same_lock_as(&self, ptr: *const SomethingZeroSizedButTyped) -> bool {
+ ptr::eq(self.shared_lock.ptr(), ptr)
+ }
+
+ /// Access the data for reading.
+ pub fn read_with<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a T {
+ #[cfg(feature = "gecko")]
+ assert!(
+ self.is_read_only_lock() || self.same_lock_as(guard.ptr()),
+ "Locked::read_with called with a guard from an unrelated SharedRwLock: {:?} vs. {:?}",
+ self.shared_lock.ptr(),
+ guard.ptr(),
+ );
+ #[cfg(not(feature = "gecko"))]
+ assert!(self.same_lock_as(&guard.0));
+
+ let ptr = self.data.get();
+
+ // Unsafe:
+ //
+ // * The guard guarantees that the lock is taken for reading,
+ // and we’ve checked that it’s the correct lock.
+ // * The returned reference borrows *both* the data and the guard,
+ // so that it can outlive neither.
+ unsafe { &*ptr }
+ }
+
+ /// Access the data for reading without verifying the lock. Use with caution.
+ #[cfg(feature = "gecko")]
+ pub unsafe fn read_unchecked<'a>(&'a self) -> &'a T {
+ let ptr = self.data.get();
+ &*ptr
+ }
+
+ /// Access the data for writing.
+ pub fn write_with<'a>(&'a self, guard: &'a mut SharedRwLockWriteGuard) -> &'a mut T {
+ #[cfg(feature = "gecko")]
+ assert!(
+ !self.is_read_only_lock() && self.same_lock_as(&*guard.0),
+ "Locked::write_with called with a guard from a read only or unrelated SharedRwLock"
+ );
+ #[cfg(not(feature = "gecko"))]
+ assert!(self.same_lock_as(&guard.0));
+
+ let ptr = self.data.get();
+
+ // Unsafe:
+ //
+ // * The guard guarantees that the lock is taken for writing,
+ // and we’ve checked that it’s the correct lock.
+ // * The returned reference borrows *both* the data and the guard,
+ // so that it can outlive neither.
+ // * We require a mutable borrow of the guard,
+ // so that one write guard can only be used once at a time.
+ unsafe { &mut *ptr }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl<T: ToShmem> ToShmem for Locked<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ use std::mem::ManuallyDrop;
+
+ let guard = self.shared_lock.read();
+ Ok(ManuallyDrop::new(Locked {
+ shared_lock: SharedRwLock::read_only(),
+ data: UnsafeCell::new(ManuallyDrop::into_inner(
+ self.read_with(&guard).to_shmem(builder)?,
+ )),
+ }))
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<T: ToShmem> ToShmem for Locked<T> {
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ panic!("ToShmem not supported in Servo currently")
+ }
+}
+
+#[allow(dead_code)]
+mod compile_time_assert {
+ use super::{SharedRwLockReadGuard, SharedRwLockWriteGuard};
+
+ trait Marker1 {}
+ impl<T: Clone> Marker1 for T {}
+ impl<'a> Marker1 for SharedRwLockReadGuard<'a> {} // Assert SharedRwLockReadGuard: !Clone
+ impl<'a> Marker1 for SharedRwLockWriteGuard<'a> {} // Assert SharedRwLockWriteGuard: !Clone
+
+ trait Marker2 {}
+ impl<T: Copy> Marker2 for T {}
+ impl<'a> Marker2 for SharedRwLockReadGuard<'a> {} // Assert SharedRwLockReadGuard: !Copy
+ impl<'a> Marker2 for SharedRwLockWriteGuard<'a> {} // Assert SharedRwLockWriteGuard: !Copy
+}
+
+/// Like ToCss, but with a lock guard given by the caller, and with the writer specified
+/// concretely rather than with a parameter.
+pub trait ToCssWithGuard {
+ /// Serialize `self` in CSS syntax, writing to `dest`, using the given lock guard.
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result;
+
+ /// Serialize `self` in CSS syntax using the given lock guard and return a string.
+ ///
+ /// (This is a convenience wrapper for `to_css` and probably should not be overridden.)
+ #[inline]
+ fn to_css_string(&self, guard: &SharedRwLockReadGuard) -> CssString {
+ let mut s = CssString::new();
+ self.to_css(guard, &mut s).unwrap();
+ s
+ }
+}
+
+/// Parameters needed for deep clones.
+#[cfg(feature = "gecko")]
+pub struct DeepCloneParams {
+ /// The new sheet we're cloning rules into.
+ pub reference_sheet: *const crate::gecko_bindings::structs::StyleSheet,
+}
+
+/// Parameters needed for deep clones.
+#[cfg(feature = "servo")]
+pub struct DeepCloneParams;
+
+/// A trait to do a deep clone of a given CSS type. Gets a lock and a read
+/// guard, in order to be able to read and clone nested structures.
+pub trait DeepCloneWithLock: Sized {
+ /// Deep clones this object.
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self;
+}
+
+/// Guards for a document
+#[derive(Clone)]
+pub struct StylesheetGuards<'a> {
+ /// For author-origin stylesheets.
+ pub author: &'a SharedRwLockReadGuard<'a>,
+
+ /// For user-agent-origin and user-origin stylesheets
+ pub ua_or_user: &'a SharedRwLockReadGuard<'a>,
+}
+
+impl<'a> StylesheetGuards<'a> {
+ /// Get the guard for a given stylesheet origin.
+ pub fn for_origin(&self, origin: Origin) -> &SharedRwLockReadGuard<'a> {
+ match origin {
+ Origin::Author => &self.author,
+ _ => &self.ua_or_user,
+ }
+ }
+
+ /// Same guard for all origins
+ pub fn same(guard: &'a SharedRwLockReadGuard<'a>) -> Self {
+ StylesheetGuards {
+ author: guard,
+ ua_or_user: guard,
+ }
+ }
+}
diff --git a/servo/components/style/sharing/checks.rs b/servo/components/style/sharing/checks.rs
new file mode 100644
index 0000000000..dfb7ab7b97
--- /dev/null
+++ b/servo/components/style/sharing/checks.rs
@@ -0,0 +1,166 @@
+/* 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/. */
+
+//! Different checks done during the style sharing process in order to determine
+//! quickly whether it's worth to share style, and whether two different
+//! elements can indeed share the same style.
+
+use crate::bloom::StyleBloom;
+use crate::context::SharedStyleContext;
+use crate::dom::TElement;
+use crate::sharing::{StyleSharingCandidate, StyleSharingTarget};
+use selectors::matching::SelectorCaches;
+
+/// Determines whether a target and a candidate have compatible parents for
+/// sharing.
+pub fn parents_allow_sharing<E>(
+ target: &mut StyleSharingTarget<E>,
+ candidate: &mut StyleSharingCandidate<E>,
+) -> bool
+where
+ E: TElement,
+{
+ // If the identity of the parent style isn't equal, we can't share. We check
+ // this first, because the result is cached.
+ if target.parent_style_identity() != candidate.parent_style_identity() {
+ return false;
+ }
+
+ // Siblings can always share.
+ let parent = target.inheritance_parent().unwrap();
+ let candidate_parent = candidate.element.inheritance_parent().unwrap();
+ if parent == candidate_parent {
+ return true;
+ }
+
+ // If a parent element was already styled and we traversed past it without
+ // restyling it, that may be because our clever invalidation logic was able
+ // to prove that the styles of that element would remain unchanged despite
+ // changes to the id or class attributes. However, style sharing relies in
+ // the strong guarantee that all the classes and ids up the respective parent
+ // chains are identical. As such, if we skipped styling for one (or both) of
+ // the parents on this traversal, we can't share styles across cousins.
+ //
+ // This is a somewhat conservative check. We could tighten it by having the
+ // invalidation logic explicitly flag elements for which it ellided styling.
+ let parent_data = parent.borrow_data().unwrap();
+ let candidate_parent_data = candidate_parent.borrow_data().unwrap();
+ if !parent_data.safe_for_cousin_sharing() || !candidate_parent_data.safe_for_cousin_sharing() {
+ return false;
+ }
+
+ true
+}
+
+/// Whether two elements have the same same style attribute (by pointer identity).
+pub fn have_same_style_attribute<E>(
+ target: &mut StyleSharingTarget<E>,
+ candidate: &mut StyleSharingCandidate<E>,
+) -> bool
+where
+ E: TElement,
+{
+ match (target.style_attribute(), candidate.style_attribute()) {
+ (None, None) => true,
+ (Some(_), None) | (None, Some(_)) => false,
+ (Some(a), Some(b)) => &*a as *const _ == &*b as *const _,
+ }
+}
+
+/// Whether two elements have the same same presentational attributes.
+pub fn have_same_presentational_hints<E>(
+ target: &mut StyleSharingTarget<E>,
+ candidate: &mut StyleSharingCandidate<E>,
+) -> bool
+where
+ E: TElement,
+{
+ target.pres_hints() == candidate.pres_hints()
+}
+
+/// Whether a given element has the same class attribute as a given candidate.
+///
+/// We don't try to share style across elements with different class attributes.
+pub fn have_same_class<E>(
+ target: &mut StyleSharingTarget<E>,
+ candidate: &mut StyleSharingCandidate<E>,
+) -> bool
+where
+ E: TElement,
+{
+ target.class_list() == candidate.class_list()
+}
+
+/// Whether a given element has the same part attribute as a given candidate.
+///
+/// We don't try to share style across elements with different part attributes.
+pub fn have_same_parts<E>(
+ target: &mut StyleSharingTarget<E>,
+ candidate: &mut StyleSharingCandidate<E>,
+) -> bool
+where
+ E: TElement,
+{
+ target.part_list() == candidate.part_list()
+}
+
+/// Whether a given element and a candidate match the same set of "revalidation"
+/// selectors.
+///
+/// Revalidation selectors are those that depend on the DOM structure, like
+/// :first-child, etc, or on attributes that we don't check off-hand (pretty
+/// much every attribute selector except `id` and `class`.
+#[inline]
+pub fn revalidate<E>(
+ target: &mut StyleSharingTarget<E>,
+ candidate: &mut StyleSharingCandidate<E>,
+ shared_context: &SharedStyleContext,
+ bloom: &StyleBloom<E>,
+ selector_caches: &mut SelectorCaches,
+) -> bool
+where
+ E: TElement,
+{
+ let stylist = &shared_context.stylist;
+
+ let for_element = target.revalidation_match_results(stylist, bloom, selector_caches);
+
+ let for_candidate = candidate.revalidation_match_results(stylist, bloom, selector_caches);
+
+ for_element == for_candidate
+}
+
+/// Checks whether we might have rules for either of the two ids.
+#[inline]
+pub fn may_match_different_id_rules<E>(
+ shared_context: &SharedStyleContext,
+ element: E,
+ candidate: E,
+) -> bool
+where
+ E: TElement,
+{
+ let element_id = element.id();
+ let candidate_id = candidate.id();
+
+ if element_id == candidate_id {
+ return false;
+ }
+
+ let stylist = &shared_context.stylist;
+
+ let may_have_rules_for_element = match element_id {
+ Some(id) => stylist.may_have_rules_for_id(id, element),
+ None => false,
+ };
+
+ if may_have_rules_for_element {
+ return true;
+ }
+
+ match candidate_id {
+ Some(id) => stylist.may_have_rules_for_id(id, candidate),
+ None => false,
+ }
+}
diff --git a/servo/components/style/sharing/mod.rs b/servo/components/style/sharing/mod.rs
new file mode 100644
index 0000000000..eeea135c06
--- /dev/null
+++ b/servo/components/style/sharing/mod.rs
@@ -0,0 +1,923 @@
+/* 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/. */
+
+//! Code related to the style sharing cache, an optimization that allows similar
+//! nodes to share style without having to run selector matching twice.
+//!
+//! The basic setup is as follows. We have an LRU cache of style sharing
+//! candidates. When we try to style a target element, we first check whether
+//! we can quickly determine that styles match something in this cache, and if
+//! so we just use the cached style information. This check is done with a
+//! StyleBloom filter set up for the target element, which may not be a correct
+//! state for the cached candidate element if they're cousins instead of
+//! siblings.
+//!
+//! The complicated part is determining that styles match. This is subject to
+//! the following constraints:
+//!
+//! 1) The target and candidate must be inheriting the same styles.
+//! 2) The target and candidate must have exactly the same rules matching them.
+//! 3) The target and candidate must have exactly the same non-selector-based
+//! style information (inline styles, presentation hints).
+//! 4) The target and candidate must have exactly the same rules matching their
+//! pseudo-elements, because an element's style data points to the style
+//! data for its pseudo-elements.
+//!
+//! These constraints are satisfied in the following ways:
+//!
+//! * We check that the parents of the target and the candidate have the same
+//! computed style. This addresses constraint 1.
+//!
+//! * We check that the target and candidate have the same inline style and
+//! presentation hint declarations. This addresses constraint 3.
+//!
+//! * We ensure that a target matches a candidate only if they have the same
+//! matching result for all selectors that target either elements or the
+//! originating elements of pseudo-elements. This addresses constraint 4
+//! (because it prevents a target that has pseudo-element styles from matching
+//! a candidate that has different pseudo-element styles) as well as
+//! constraint 2.
+//!
+//! The actual checks that ensure that elements match the same rules are
+//! conceptually split up into two pieces. First, we do various checks on
+//! elements that make sure that the set of possible rules in all selector maps
+//! in the stylist (for normal styling and for pseudo-elements) that might match
+//! the two elements is the same. For example, we enforce that the target and
+//! candidate must have the same localname and namespace. Second, we have a
+//! selector map of "revalidation selectors" that the stylist maintains that we
+//! actually match against the target and candidate and then check whether the
+//! two sets of results were the same. Due to the up-front selector map checks,
+//! we know that the target and candidate will be matched against the same exact
+//! set of revalidation selectors, so the match result arrays can be compared
+//! directly.
+//!
+//! It's very important that a selector be added to the set of revalidation
+//! selectors any time there are two elements that could pass all the up-front
+//! checks but match differently against some ComplexSelector in the selector.
+//! If that happens, then they can have descendants that might themselves pass
+//! the up-front checks but would have different matching results for the
+//! selector in question. In this case, "descendants" includes pseudo-elements,
+//! so there is a single selector map of revalidation selectors that includes
+//! both selectors targeting elements and selectors targeting pseudo-element
+//! originating elements. We ensure that the pseudo-element parts of all these
+//! selectors are effectively stripped off, so that matching them all against
+//! elements makes sense.
+
+use crate::applicable_declarations::ApplicableDeclarationBlock;
+use crate::bloom::StyleBloom;
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::context::{SharedStyleContext, StyleContext};
+use crate::dom::{SendElement, TElement};
+use crate::properties::ComputedValues;
+use crate::rule_tree::StrongRuleNode;
+use crate::selector_map::RelevantAttributes;
+use crate::style_resolver::{PrimaryStyle, ResolvedElementStyles};
+use crate::stylist::Stylist;
+use crate::values::AtomIdent;
+use atomic_refcell::{AtomicRefCell, AtomicRefMut};
+use owning_ref::OwningHandle;
+use selectors::matching::{NeedsSelectorFlags, SelectorCaches, VisitedHandlingMode};
+use servo_arc::Arc;
+use smallbitvec::SmallBitVec;
+use smallvec::SmallVec;
+use std::marker::PhantomData;
+use std::mem::{self, ManuallyDrop};
+use std::ops::Deref;
+use std::ptr::NonNull;
+use uluru::LRUCache;
+
+mod checks;
+
+/// The amount of nodes that the style sharing candidate cache should hold at
+/// most.
+///
+/// The cache size was chosen by measuring style sharing and resulting
+/// performance on a few pages; sizes up to about 32 were giving good sharing
+/// improvements (e.g. 3x fewer styles having to be resolved than at size 8) and
+/// slight performance improvements. Sizes larger than 32 haven't really been
+/// tested.
+pub const SHARING_CACHE_SIZE: usize = 32;
+
+/// Opaque pointer type to compare ComputedValues identities.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct OpaqueComputedValues(NonNull<()>);
+
+unsafe impl Send for OpaqueComputedValues {}
+unsafe impl Sync for OpaqueComputedValues {}
+
+impl OpaqueComputedValues {
+ fn from(cv: &ComputedValues) -> Self {
+ let p =
+ unsafe { NonNull::new_unchecked(cv as *const ComputedValues as *const () as *mut ()) };
+ OpaqueComputedValues(p)
+ }
+
+ fn eq(&self, cv: &ComputedValues) -> bool {
+ Self::from(cv) == *self
+ }
+}
+
+/// The results from the revalidation step.
+///
+/// Rather than either:
+///
+/// * Plainly rejecting sharing for elements with different attributes (which would be unfortunate
+/// because a lot of elements have different attributes yet those attributes are not
+/// style-relevant).
+///
+/// * Having to give up on per-attribute bucketing, which would be unfortunate because it
+/// increases the cost of revalidation for pages with lots of global attribute selectors (see
+/// bug 1868316).
+///
+/// * We also store the style-relevant attributes for these elements, in order to guarantee that
+/// we end up looking at the same selectors.
+///
+#[derive(Debug, Default)]
+pub struct RevalidationResult {
+ /// A bit for each selector matched. This is sound because we guarantee we look up into the
+ /// same buckets via the pre-revalidation checks and relevant_attributes.
+ pub selectors_matched: SmallBitVec,
+ /// The set of attributes of this element that were relevant for its style.
+ pub relevant_attributes: RelevantAttributes,
+}
+
+impl PartialEq for RevalidationResult {
+ fn eq(&self, other: &Self) -> bool {
+ if self.relevant_attributes != other.relevant_attributes {
+ return false;
+ }
+
+ // This assert "ensures", to some extent, that the two candidates have matched the
+ // same rulehash buckets, and as such, that the bits we're comparing represent the
+ // same set of selectors.
+ debug_assert_eq!(self.selectors_matched.len(), other.selectors_matched.len());
+ self.selectors_matched == other.selectors_matched
+ }
+}
+
+/// Some data we want to avoid recomputing all the time while trying to share
+/// style.
+#[derive(Debug, Default)]
+pub struct ValidationData {
+ /// The class list of this element.
+ ///
+ /// TODO(emilio): Maybe check whether rules for these classes apply to the
+ /// element?
+ class_list: Option<SmallVec<[AtomIdent; 5]>>,
+
+ /// The part list of this element.
+ ///
+ /// TODO(emilio): Maybe check whether rules with these part names apply to
+ /// the element?
+ part_list: Option<SmallVec<[AtomIdent; 5]>>,
+
+ /// The list of presentational attributes of the element.
+ pres_hints: Option<SmallVec<[ApplicableDeclarationBlock; 5]>>,
+
+ /// The pointer identity of the parent ComputedValues.
+ parent_style_identity: Option<OpaqueComputedValues>,
+
+ /// The cached result of matching this entry against the revalidation
+ /// selectors.
+ revalidation_match_results: Option<RevalidationResult>,
+}
+
+impl ValidationData {
+ /// Move the cached data to a new instance, and return it.
+ pub fn take(&mut self) -> Self {
+ mem::replace(self, Self::default())
+ }
+
+ /// Get or compute the list of presentational attributes associated with
+ /// this element.
+ pub fn pres_hints<E>(&mut self, element: E) -> &[ApplicableDeclarationBlock]
+ where
+ E: TElement,
+ {
+ self.pres_hints.get_or_insert_with(|| {
+ let mut pres_hints = SmallVec::new();
+ element.synthesize_presentational_hints_for_legacy_attributes(
+ VisitedHandlingMode::AllLinksUnvisited,
+ &mut pres_hints,
+ );
+ pres_hints
+ })
+ }
+
+ /// Get or compute the part-list associated with this element.
+ pub fn part_list<E>(&mut self, element: E) -> &[AtomIdent]
+ where
+ E: TElement,
+ {
+ if !element.has_part_attr() {
+ return &[];
+ }
+ self.part_list.get_or_insert_with(|| {
+ let mut list = SmallVec::<[_; 5]>::new();
+ element.each_part(|p| list.push(p.clone()));
+ // See below for the reasoning.
+ if !list.spilled() {
+ list.sort_unstable_by_key(|a| a.get_hash());
+ }
+ list
+ })
+ }
+
+ /// Get or compute the class-list associated with this element.
+ pub fn class_list<E>(&mut self, element: E) -> &[AtomIdent]
+ where
+ E: TElement,
+ {
+ self.class_list.get_or_insert_with(|| {
+ let mut list = SmallVec::<[_; 5]>::new();
+ element.each_class(|c| list.push(c.clone()));
+ // Assuming there are a reasonable number of classes (we use the
+ // inline capacity as "reasonable number"), sort them to so that
+ // we don't mistakenly reject sharing candidates when one element
+ // has "foo bar" and the other has "bar foo".
+ if !list.spilled() {
+ list.sort_unstable_by_key(|a| a.get_hash());
+ }
+ list
+ })
+ }
+
+ /// Get or compute the parent style identity.
+ pub fn parent_style_identity<E>(&mut self, el: E) -> OpaqueComputedValues
+ where
+ E: TElement,
+ {
+ self.parent_style_identity
+ .get_or_insert_with(|| {
+ let parent = el.inheritance_parent().unwrap();
+ let values =
+ OpaqueComputedValues::from(parent.borrow_data().unwrap().styles.primary());
+ values
+ })
+ .clone()
+ }
+
+ /// Computes the revalidation results if needed, and returns it.
+ /// Inline so we know at compile time what bloom_known_valid is.
+ #[inline]
+ fn revalidation_match_results<E>(
+ &mut self,
+ element: E,
+ stylist: &Stylist,
+ bloom: &StyleBloom<E>,
+ selector_caches: &mut SelectorCaches,
+ bloom_known_valid: bool,
+ needs_selector_flags: NeedsSelectorFlags,
+ ) -> &RevalidationResult
+ where
+ E: TElement,
+ {
+ self.revalidation_match_results.get_or_insert_with(|| {
+ // The bloom filter may already be set up for our element.
+ // If it is, use it. If not, we must be in a candidate
+ // (i.e. something in the cache), and the element is one
+ // of our cousins, not a sibling. In that case, we'll
+ // just do revalidation selector matching without a bloom
+ // filter, to avoid thrashing the filter.
+ let bloom_to_use = if bloom_known_valid {
+ debug_assert_eq!(bloom.current_parent(), element.traversal_parent());
+ Some(bloom.filter())
+ } else {
+ if bloom.current_parent() == element.traversal_parent() {
+ Some(bloom.filter())
+ } else {
+ None
+ }
+ };
+ stylist.match_revalidation_selectors(
+ element,
+ bloom_to_use,
+ selector_caches,
+ needs_selector_flags,
+ )
+ })
+ }
+}
+
+/// Information regarding a style sharing candidate, that is, an entry in the
+/// style sharing cache.
+///
+/// Note that this information is stored in TLS and cleared after the traversal,
+/// and once here, the style information of the element is immutable, so it's
+/// safe to access.
+///
+/// Important: If you change the members/layout here, You need to do the same for
+/// FakeCandidate below.
+#[derive(Debug)]
+pub struct StyleSharingCandidate<E: TElement> {
+ /// The element.
+ element: E,
+ validation_data: ValidationData,
+}
+
+struct FakeCandidate {
+ _element: usize,
+ _validation_data: ValidationData,
+}
+
+impl<E: TElement> Deref for StyleSharingCandidate<E> {
+ type Target = E;
+
+ fn deref(&self) -> &Self::Target {
+ &self.element
+ }
+}
+
+impl<E: TElement> StyleSharingCandidate<E> {
+ /// Get the classlist of this candidate.
+ fn class_list(&mut self) -> &[AtomIdent] {
+ self.validation_data.class_list(self.element)
+ }
+
+ /// Get the part list of this candidate.
+ fn part_list(&mut self) -> &[AtomIdent] {
+ self.validation_data.part_list(self.element)
+ }
+
+ /// Get the pres hints of this candidate.
+ fn pres_hints(&mut self) -> &[ApplicableDeclarationBlock] {
+ self.validation_data.pres_hints(self.element)
+ }
+
+ /// Get the parent style identity.
+ fn parent_style_identity(&mut self) -> OpaqueComputedValues {
+ self.validation_data.parent_style_identity(self.element)
+ }
+
+ /// Compute the bit vector of revalidation selector match results
+ /// for this candidate.
+ fn revalidation_match_results(
+ &mut self,
+ stylist: &Stylist,
+ bloom: &StyleBloom<E>,
+ selector_caches: &mut SelectorCaches,
+ ) -> &RevalidationResult {
+ self.validation_data.revalidation_match_results(
+ self.element,
+ stylist,
+ bloom,
+ selector_caches,
+ /* bloom_known_valid = */ false,
+ // The candidate must already have the right bits already, if
+ // needed.
+ NeedsSelectorFlags::No,
+ )
+ }
+}
+
+impl<E: TElement> PartialEq<StyleSharingCandidate<E>> for StyleSharingCandidate<E> {
+ fn eq(&self, other: &Self) -> bool {
+ self.element == other.element
+ }
+}
+
+/// An element we want to test against the style sharing cache.
+pub struct StyleSharingTarget<E: TElement> {
+ element: E,
+ validation_data: ValidationData,
+}
+
+impl<E: TElement> Deref for StyleSharingTarget<E> {
+ type Target = E;
+
+ fn deref(&self) -> &Self::Target {
+ &self.element
+ }
+}
+
+impl<E: TElement> StyleSharingTarget<E> {
+ /// Trivially construct a new StyleSharingTarget to test against the cache.
+ pub fn new(element: E) -> Self {
+ Self {
+ element: element,
+ validation_data: ValidationData::default(),
+ }
+ }
+
+ fn class_list(&mut self) -> &[AtomIdent] {
+ self.validation_data.class_list(self.element)
+ }
+
+ fn part_list(&mut self) -> &[AtomIdent] {
+ self.validation_data.part_list(self.element)
+ }
+
+ /// Get the pres hints of this candidate.
+ fn pres_hints(&mut self) -> &[ApplicableDeclarationBlock] {
+ self.validation_data.pres_hints(self.element)
+ }
+
+ /// Get the parent style identity.
+ fn parent_style_identity(&mut self) -> OpaqueComputedValues {
+ self.validation_data.parent_style_identity(self.element)
+ }
+
+ fn revalidation_match_results(
+ &mut self,
+ stylist: &Stylist,
+ bloom: &StyleBloom<E>,
+ selector_caches: &mut SelectorCaches,
+ ) -> &RevalidationResult {
+ // It's important to set the selector flags. Otherwise, if we succeed in
+ // sharing the style, we may not set the slow selector flags for the
+ // right elements (which may not necessarily be |element|), causing
+ // missed restyles after future DOM mutations.
+ //
+ // Gecko's test_bug534804.html exercises this. A minimal testcase is:
+ // <style> #e:empty + span { ... } </style>
+ // <span id="e">
+ // <span></span>
+ // </span>
+ // <span></span>
+ //
+ // The style sharing cache will get a hit for the second span. When the
+ // child span is subsequently removed from the DOM, missing selector
+ // flags would cause us to miss the restyle on the second span.
+ self.validation_data.revalidation_match_results(
+ self.element,
+ stylist,
+ bloom,
+ selector_caches,
+ /* bloom_known_valid = */ true,
+ NeedsSelectorFlags::Yes,
+ )
+ }
+
+ /// Attempts to share a style with another node.
+ pub fn share_style_if_possible(
+ &mut self,
+ context: &mut StyleContext<E>,
+ ) -> Option<ResolvedElementStyles> {
+ let cache = &mut context.thread_local.sharing_cache;
+ let shared_context = &context.shared;
+ let bloom_filter = &context.thread_local.bloom_filter;
+ let selector_caches = &mut context.thread_local.selector_caches;
+
+ if cache.dom_depth != bloom_filter.matching_depth() {
+ debug!(
+ "Can't share style, because DOM depth changed from {:?} to {:?}, element: {:?}",
+ cache.dom_depth,
+ bloom_filter.matching_depth(),
+ self.element
+ );
+ return None;
+ }
+ debug_assert_eq!(
+ bloom_filter.current_parent(),
+ self.element.traversal_parent()
+ );
+
+ cache.share_style_if_possible(shared_context, bloom_filter, selector_caches, self)
+ }
+
+ /// Gets the validation data used to match against this target, if any.
+ pub fn take_validation_data(&mut self) -> ValidationData {
+ self.validation_data.take()
+ }
+}
+
+struct SharingCacheBase<Candidate> {
+ entries: LRUCache<Candidate, SHARING_CACHE_SIZE>,
+}
+
+impl<Candidate> Default for SharingCacheBase<Candidate> {
+ fn default() -> Self {
+ Self {
+ entries: LRUCache::default(),
+ }
+ }
+}
+
+impl<Candidate> SharingCacheBase<Candidate> {
+ fn clear(&mut self) {
+ self.entries.clear();
+ }
+
+ fn is_empty(&self) -> bool {
+ self.entries.len() == 0
+ }
+}
+
+impl<E: TElement> SharingCache<E> {
+ fn insert(
+ &mut self,
+ element: E,
+ validation_data_holder: Option<&mut StyleSharingTarget<E>>,
+ ) {
+ let validation_data = match validation_data_holder {
+ Some(v) => v.take_validation_data(),
+ None => ValidationData::default(),
+ };
+ self.entries.insert(StyleSharingCandidate {
+ element,
+ validation_data,
+ });
+ }
+}
+
+/// Style sharing caches are are large allocations, so we store them in thread-local
+/// storage such that they can be reused across style traversals. Ideally, we'd just
+/// stack-allocate these buffers with uninitialized memory, but right now rustc can't
+/// avoid memmoving the entire cache during setup, which gets very expensive. See
+/// issues like [1] and [2].
+///
+/// Given that the cache stores entries of type TElement, we transmute to usize
+/// before storing in TLS. This is safe as long as we make sure to empty the cache
+/// before we let it go.
+///
+/// [1] https://github.com/rust-lang/rust/issues/42763
+/// [2] https://github.com/rust-lang/rust/issues/13707
+type SharingCache<E> = SharingCacheBase<StyleSharingCandidate<E>>;
+type TypelessSharingCache = SharingCacheBase<FakeCandidate>;
+type StoredSharingCache = Arc<AtomicRefCell<TypelessSharingCache>>;
+
+thread_local! {
+ // See the comment on bloom.rs about why do we leak this.
+ static SHARING_CACHE_KEY: ManuallyDrop<StoredSharingCache> =
+ ManuallyDrop::new(Arc::new_leaked(Default::default()));
+}
+
+/// An LRU cache of the last few nodes seen, so that we can aggressively try to
+/// reuse their styles.
+///
+/// Note that this cache is flushed every time we steal work from the queue, so
+/// storing nodes here temporarily is safe.
+pub struct StyleSharingCache<E: TElement> {
+ /// The LRU cache, with the type cast away to allow persisting the allocation.
+ cache_typeless: OwningHandle<StoredSharingCache, AtomicRefMut<'static, TypelessSharingCache>>,
+ /// Bind this structure to the lifetime of E, since that's what we effectively store.
+ marker: PhantomData<SendElement<E>>,
+ /// The DOM depth we're currently at. This is used as an optimization to
+ /// clear the cache when we change depths, since we know at that point
+ /// nothing in the cache will match.
+ dom_depth: usize,
+}
+
+impl<E: TElement> Drop for StyleSharingCache<E> {
+ fn drop(&mut self) {
+ self.clear();
+ }
+}
+
+impl<E: TElement> StyleSharingCache<E> {
+ #[allow(dead_code)]
+ fn cache(&self) -> &SharingCache<E> {
+ let base: &TypelessSharingCache = &*self.cache_typeless;
+ unsafe { mem::transmute(base) }
+ }
+
+ fn cache_mut(&mut self) -> &mut SharingCache<E> {
+ let base: &mut TypelessSharingCache = &mut *self.cache_typeless;
+ unsafe { mem::transmute(base) }
+ }
+
+ /// Create a new style sharing candidate cache.
+
+ // Forced out of line to limit stack frame sizes after extra inlining from
+ // https://github.com/rust-lang/rust/pull/43931
+ //
+ // See https://github.com/servo/servo/pull/18420#issuecomment-328769322
+ #[inline(never)]
+ pub fn new() -> Self {
+ assert_eq!(
+ mem::size_of::<SharingCache<E>>(),
+ mem::size_of::<TypelessSharingCache>()
+ );
+ assert_eq!(
+ mem::align_of::<SharingCache<E>>(),
+ mem::align_of::<TypelessSharingCache>()
+ );
+ let cache_arc = SHARING_CACHE_KEY.with(|c| Arc::clone(&*c));
+ let cache =
+ OwningHandle::new_with_fn(cache_arc, |x| unsafe { x.as_ref() }.unwrap().borrow_mut());
+ debug_assert!(cache.is_empty());
+
+ StyleSharingCache {
+ cache_typeless: cache,
+ marker: PhantomData,
+ dom_depth: 0,
+ }
+ }
+
+ /// Tries to insert an element in the style sharing cache.
+ ///
+ /// Fails if we know it should never be in the cache.
+ ///
+ /// NB: We pass a source for the validation data, rather than the data itself,
+ /// to avoid memmoving at each function call. See rust issue #42763.
+ pub fn insert_if_possible(
+ &mut self,
+ element: &E,
+ style: &PrimaryStyle,
+ validation_data_holder: Option<&mut StyleSharingTarget<E>>,
+ dom_depth: usize,
+ shared_context: &SharedStyleContext,
+ ) {
+ let parent = match element.traversal_parent() {
+ Some(element) => element,
+ None => {
+ debug!("Failing to insert to the cache: no parent element");
+ return;
+ },
+ };
+
+ if !element.matches_user_and_content_rules() {
+ debug!("Failing to insert into the cache: no tree rules:");
+ return;
+ }
+
+ // We can't share style across shadow hosts right now, because they may
+ // match different :host rules.
+ //
+ // TODO(emilio): We could share across the ones that don't have :host
+ // rules or have the same.
+ if element.shadow_root().is_some() {
+ debug!("Failing to insert into the cache: Shadow Host");
+ return;
+ }
+
+ // If the element has running animations, we can't share style.
+ //
+ // This is distinct from the specifies_{animations,transitions} check below,
+ // because:
+ // * Animations can be triggered directly via the Web Animations API.
+ // * Our computed style can still be affected by animations after we no
+ // longer match any animation rules, since removing animations involves
+ // a sequential task and an additional traversal.
+ if element.has_animations(shared_context) {
+ debug!("Failing to insert to the cache: running animations");
+ return;
+ }
+
+ // In addition to the above running animations check, we also need to
+ // check CSS animation and transition styles since it's possible that
+ // we are about to create CSS animations/transitions.
+ //
+ // These are things we don't check in the candidate match because they
+ // are either uncommon or expensive.
+ let ui_style = style.style().get_ui();
+ if ui_style.specifies_transitions() {
+ debug!("Failing to insert to the cache: transitions");
+ return;
+ }
+
+ if ui_style.specifies_animations() {
+ debug!("Failing to insert to the cache: animations");
+ return;
+ }
+
+ debug!(
+ "Inserting into cache: {:?} with parent {:?}",
+ element, parent
+ );
+
+ if self.dom_depth != dom_depth {
+ debug!(
+ "Clearing cache because depth changed from {:?} to {:?}, element: {:?}",
+ self.dom_depth, dom_depth, element
+ );
+ self.clear();
+ self.dom_depth = dom_depth;
+ }
+ self.cache_mut().insert(
+ *element,
+ validation_data_holder,
+ );
+ }
+
+ /// Clear the style sharing candidate cache.
+ pub fn clear(&mut self) {
+ self.cache_mut().clear();
+ }
+
+ /// Attempts to share a style with another node.
+ fn share_style_if_possible(
+ &mut self,
+ shared_context: &SharedStyleContext,
+ bloom_filter: &StyleBloom<E>,
+ selector_caches: &mut SelectorCaches,
+ target: &mut StyleSharingTarget<E>,
+ ) -> Option<ResolvedElementStyles> {
+ if shared_context.options.disable_style_sharing_cache {
+ debug!(
+ "{:?} Cannot share style: style sharing cache disabled",
+ target.element
+ );
+ return None;
+ }
+
+ if target.inheritance_parent().is_none() {
+ debug!(
+ "{:?} Cannot share style: element has no parent",
+ target.element
+ );
+ return None;
+ }
+
+ if !target.matches_user_and_content_rules() {
+ debug!("{:?} Cannot share style: content rules", target.element);
+ return None;
+ }
+
+ self.cache_mut().entries.lookup(|candidate| {
+ Self::test_candidate(
+ target,
+ candidate,
+ &shared_context,
+ bloom_filter,
+ selector_caches,
+ shared_context,
+ )
+ })
+ }
+
+ fn test_candidate(
+ target: &mut StyleSharingTarget<E>,
+ candidate: &mut StyleSharingCandidate<E>,
+ shared: &SharedStyleContext,
+ bloom: &StyleBloom<E>,
+ selector_caches: &mut SelectorCaches,
+ shared_context: &SharedStyleContext,
+ ) -> Option<ResolvedElementStyles> {
+ debug_assert!(target.matches_user_and_content_rules());
+
+ // Check that we have the same parent, or at least that the parents
+ // share styles and permit sharing across their children. The latter
+ // check allows us to share style between cousins if the parents
+ // shared style.
+ if !checks::parents_allow_sharing(target, candidate) {
+ trace!("Miss: Parent");
+ return None;
+ }
+
+ if target.local_name() != candidate.element.local_name() {
+ trace!("Miss: Local Name");
+ return None;
+ }
+
+ if target.namespace() != candidate.element.namespace() {
+ trace!("Miss: Namespace");
+ return None;
+ }
+
+ // We do not ignore visited state here, because Gecko needs to store
+ // extra bits on visited styles, so these contexts cannot be shared.
+ if target.element.state() != candidate.state() {
+ trace!("Miss: User and Author State");
+ return None;
+ }
+
+ if target.is_link() != candidate.element.is_link() {
+ trace!("Miss: Link");
+ return None;
+ }
+
+ // If two elements belong to different shadow trees, different rules may
+ // apply to them, from the respective trees.
+ if target.element.containing_shadow() != candidate.element.containing_shadow() {
+ trace!("Miss: Different containing shadow roots");
+ return None;
+ }
+
+ // If the elements are not assigned to the same slot they could match
+ // different ::slotted() rules in the slot scope.
+ //
+ // If two elements are assigned to different slots, even within the same
+ // shadow root, they could match different rules, due to the slot being
+ // assigned to yet another slot in another shadow root.
+ if target.element.assigned_slot() != candidate.element.assigned_slot() {
+ // TODO(emilio): We could have a look at whether the shadow roots
+ // actually have slotted rules and such.
+ trace!("Miss: Different assigned slots");
+ return None;
+ }
+
+ if target.element.shadow_root().is_some() {
+ trace!("Miss: Shadow host");
+ return None;
+ }
+
+ if target.element.has_animations(shared_context) {
+ trace!("Miss: Has Animations");
+ return None;
+ }
+
+ if target.matches_user_and_content_rules() !=
+ candidate.element.matches_user_and_content_rules()
+ {
+ trace!("Miss: User and Author Rules");
+ return None;
+ }
+
+ // It's possible that there are no styles for either id.
+ if checks::may_match_different_id_rules(shared, target.element, candidate.element) {
+ trace!("Miss: ID Attr");
+ return None;
+ }
+
+ if !checks::have_same_style_attribute(target, candidate) {
+ trace!("Miss: Style Attr");
+ return None;
+ }
+
+ if !checks::have_same_class(target, candidate) {
+ trace!("Miss: Class");
+ return None;
+ }
+
+ if !checks::have_same_presentational_hints(target, candidate) {
+ trace!("Miss: Pres Hints");
+ return None;
+ }
+
+ if !checks::have_same_parts(target, candidate) {
+ trace!("Miss: Shadow parts");
+ return None;
+ }
+
+ if !checks::revalidate(target, candidate, shared, bloom, selector_caches) {
+ trace!("Miss: Revalidation");
+ return None;
+ }
+
+ debug!(
+ "Sharing allowed between {:?} and {:?}",
+ target.element, candidate.element
+ );
+ Some(candidate.element.borrow_data().unwrap().share_styles())
+ }
+
+ /// Attempts to find an element in the cache with the given primary rule
+ /// node and parent.
+ ///
+ /// FIXME(emilio): re-measure this optimization, and remove if it's not very
+ /// useful... It's probably not worth the complexity / obscure bugs.
+ pub fn lookup_by_rules(
+ &mut self,
+ shared_context: &SharedStyleContext,
+ inherited: &ComputedValues,
+ rules: &StrongRuleNode,
+ visited_rules: Option<&StrongRuleNode>,
+ target: E,
+ ) -> Option<PrimaryStyle> {
+ if shared_context.options.disable_style_sharing_cache {
+ return None;
+ }
+
+ self.cache_mut().entries.lookup(|candidate| {
+ debug_assert_ne!(candidate.element, target);
+ if !candidate.parent_style_identity().eq(inherited) {
+ return None;
+ }
+ let data = candidate.element.borrow_data().unwrap();
+ let style = data.styles.primary();
+ if style.rules.as_ref() != Some(&rules) {
+ return None;
+ }
+ if style.visited_rules() != visited_rules {
+ return None;
+ }
+ // NOTE(emilio): We only need to check name / namespace because we
+ // do name-dependent style adjustments, like the display: contents
+ // to display: none adjustment.
+ if target.namespace() != candidate.element.namespace() ||
+ target.local_name() != candidate.element.local_name()
+ {
+ return None;
+ }
+ // When using container units, inherited style + rules matched aren't enough to
+ // determine whether the style is the same. We could actually do a full container
+ // lookup but for now we just check that our actual traversal parent matches.
+ if data
+ .styles
+ .primary()
+ .flags
+ .intersects(ComputedValueFlags::USES_CONTAINER_UNITS) &&
+ candidate.element.traversal_parent() != target.traversal_parent()
+ {
+ return None;
+ }
+ // Rule nodes and styles are computed independent of the element's actual visitedness,
+ // but at the end of the cascade (in `adjust_for_visited`) we do store the
+ // RELEVANT_LINK_VISITED flag, so we can't share by rule node between visited and
+ // unvisited styles. We don't check for visitedness and just refuse to share for links
+ // entirely, so that visitedness doesn't affect timing.
+ debug_assert_eq!(
+ target.is_link(),
+ candidate.element.is_link(),
+ "Linkness mismatch"
+ );
+ if target.is_link() {
+ return None;
+ }
+
+ Some(data.share_primary_style())
+ })
+ }
+}
diff --git a/servo/components/style/str.rs b/servo/components/style/str.rs
new file mode 100644
index 0000000000..9badcdf413
--- /dev/null
+++ b/servo/components/style/str.rs
@@ -0,0 +1,181 @@
+/* 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/. */
+
+//! String utils for attributes and similar stuff.
+
+#![deny(missing_docs)]
+
+use num_traits::ToPrimitive;
+use std::borrow::Cow;
+use std::convert::AsRef;
+use std::iter::{Filter, Peekable};
+use std::str::Split;
+
+/// A static slice of characters.
+pub type StaticCharVec = &'static [char];
+
+/// A static slice of `str`s.
+pub type StaticStringVec = &'static [&'static str];
+
+/// A "space character" according to:
+///
+/// <https://html.spec.whatwg.org/multipage/#space-character>
+pub static HTML_SPACE_CHARACTERS: StaticCharVec =
+ &['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}'];
+
+/// Whether a character is a HTML whitespace character.
+#[inline]
+pub fn char_is_whitespace(c: char) -> bool {
+ HTML_SPACE_CHARACTERS.contains(&c)
+}
+
+/// Whether all the string is HTML whitespace.
+#[inline]
+pub fn is_whitespace(s: &str) -> bool {
+ s.chars().all(char_is_whitespace)
+}
+
+#[inline]
+fn not_empty(&split: &&str) -> bool {
+ !split.is_empty()
+}
+
+/// Split a string on HTML whitespace.
+#[inline]
+pub fn split_html_space_chars<'a>(
+ s: &'a str,
+) -> Filter<Split<'a, StaticCharVec>, fn(&&str) -> bool> {
+ s.split(HTML_SPACE_CHARACTERS)
+ .filter(not_empty as fn(&&str) -> bool)
+}
+
+/// Split a string on commas.
+#[inline]
+pub fn split_commas<'a>(s: &'a str) -> Filter<Split<'a, char>, fn(&&str) -> bool> {
+ s.split(',').filter(not_empty as fn(&&str) -> bool)
+}
+
+/// Character is ascii digit
+pub fn is_ascii_digit(c: &char) -> bool {
+ match *c {
+ '0'..='9' => true,
+ _ => false,
+ }
+}
+
+fn is_decimal_point(c: char) -> bool {
+ c == '.'
+}
+
+fn is_exponent_char(c: char) -> bool {
+ match c {
+ 'e' | 'E' => true,
+ _ => false,
+ }
+}
+
+/// Read a set of ascii digits and read them into a number.
+pub fn read_numbers<I: Iterator<Item = char>>(mut iter: Peekable<I>) -> (Option<i64>, usize) {
+ match iter.peek() {
+ Some(c) if is_ascii_digit(c) => (),
+ _ => return (None, 0),
+ }
+
+ iter.take_while(is_ascii_digit)
+ .map(|d| d as i64 - '0' as i64)
+ .fold((Some(0i64), 0), |accumulator, d| {
+ let digits = accumulator
+ .0
+ .and_then(|accumulator| accumulator.checked_mul(10))
+ .and_then(|accumulator| accumulator.checked_add(d));
+ (digits, accumulator.1 + 1)
+ })
+}
+
+/// Read a decimal fraction.
+pub fn read_fraction<I: Iterator<Item = char>>(
+ mut iter: Peekable<I>,
+ mut divisor: f64,
+ value: f64,
+) -> (f64, usize) {
+ match iter.peek() {
+ Some(c) if is_decimal_point(*c) => (),
+ _ => return (value, 0),
+ }
+ iter.next();
+
+ iter.take_while(is_ascii_digit)
+ .map(|d| d as i64 - '0' as i64)
+ .fold((value, 1), |accumulator, d| {
+ divisor *= 10f64;
+ (accumulator.0 + d as f64 / divisor, accumulator.1 + 1)
+ })
+}
+
+/// Reads an exponent from an iterator over chars, for example `e100`.
+pub fn read_exponent<I: Iterator<Item = char>>(mut iter: Peekable<I>) -> Option<i32> {
+ match iter.peek() {
+ Some(c) if is_exponent_char(*c) => (),
+ _ => return None,
+ }
+ iter.next();
+
+ match iter.peek() {
+ None => None,
+ Some(&'-') => {
+ iter.next();
+ read_numbers(iter).0.map(|exp| -exp.to_i32().unwrap_or(0))
+ },
+ Some(&'+') => {
+ iter.next();
+ read_numbers(iter).0.map(|exp| exp.to_i32().unwrap_or(0))
+ },
+ Some(_) => read_numbers(iter).0.map(|exp| exp.to_i32().unwrap_or(0)),
+ }
+}
+
+/// Join a set of strings with a given delimiter `join`.
+pub fn str_join<I, T>(strs: I, join: &str) -> String
+where
+ I: IntoIterator<Item = T>,
+ T: AsRef<str>,
+{
+ strs.into_iter()
+ .enumerate()
+ .fold(String::new(), |mut acc, (i, s)| {
+ if i > 0 {
+ acc.push_str(join);
+ }
+ acc.push_str(s.as_ref());
+ acc
+ })
+}
+
+/// Returns true if a given string has a given prefix with case-insensitive match.
+pub fn starts_with_ignore_ascii_case(string: &str, prefix: &str) -> bool {
+ string.len() >= prefix.len() &&
+ string.as_bytes()[0..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes())
+}
+
+/// Returns an ascii lowercase version of a string, only allocating if needed.
+pub fn string_as_ascii_lowercase<'a>(input: &'a str) -> Cow<'a, str> {
+ if input.bytes().any(|c| matches!(c, b'A'..=b'Z')) {
+ input.to_ascii_lowercase().into()
+ } else {
+ // Already ascii lowercase.
+ Cow::Borrowed(input)
+ }
+}
+
+/// To avoid accidentally instantiating multiple monomorphizations of large
+/// serialization routines, we define explicit concrete types and require
+/// them in those routines. This avoids accidental mixing of String and
+/// nsACString arguments in Gecko, which would cause code size to blow up.
+#[cfg(feature = "gecko")]
+pub type CssStringWriter = ::nsstring::nsACString;
+
+/// String type that coerces to CssStringWriter, used when serialization code
+/// needs to allocate a temporary string.
+#[cfg(feature = "gecko")]
+pub type CssString = ::nsstring::nsCString;
diff --git a/servo/components/style/style_adjuster.rs b/servo/components/style/style_adjuster.rs
new file mode 100644
index 0000000000..a993d79d6a
--- /dev/null
+++ b/servo/components/style/style_adjuster.rs
@@ -0,0 +1,1009 @@
+/* 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/. */
+
+//! A struct to encapsulate all the style fixups and flags propagations
+//! a computed style needs in order for it to adhere to the CSS spec.
+
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::dom::TElement;
+use crate::properties::longhands::contain::computed_value::T as Contain;
+use crate::properties::longhands::container_type::computed_value::T as ContainerType;
+use crate::properties::longhands::content_visibility::computed_value::T as ContentVisibility;
+use crate::properties::longhands::display::computed_value::T as Display;
+use crate::properties::longhands::float::computed_value::T as Float;
+use crate::properties::longhands::overflow_x::computed_value::T as Overflow;
+use crate::properties::longhands::position::computed_value::T as Position;
+use crate::properties::{self, ComputedValues, StyleBuilder};
+
+/// A struct that implements all the adjustment methods.
+///
+/// NOTE(emilio): If new adjustments are introduced that depend on reset
+/// properties of the parent, you may need tweaking the
+/// `ChildCascadeRequirement` code in `matching.rs`.
+///
+/// NOTE(emilio): Also, if new adjustments are introduced that break the
+/// following invariant:
+///
+/// Given same tag name, namespace, rules and parent style, two elements would
+/// end up with exactly the same style.
+///
+/// Then you need to adjust the lookup_by_rules conditions in the sharing cache.
+pub struct StyleAdjuster<'a, 'b: 'a> {
+ style: &'a mut StyleBuilder<'b>,
+}
+
+#[cfg(feature = "gecko")]
+fn is_topmost_svg_svg_element<E>(e: E) -> bool
+where
+ E: TElement,
+{
+ debug_assert!(e.is_svg_element());
+ if e.local_name() != &*atom!("svg") {
+ return false;
+ }
+
+ let parent = match e.traversal_parent() {
+ Some(n) => n,
+ None => return true,
+ };
+
+ if !parent.is_svg_element() {
+ return true;
+ }
+
+ parent.local_name() == &*atom!("foreignObject")
+}
+
+// https://drafts.csswg.org/css-display/#unbox
+#[cfg(feature = "gecko")]
+fn is_effective_display_none_for_display_contents<E>(element: E) -> bool
+where
+ E: TElement,
+{
+ use crate::Atom;
+
+ const SPECIAL_HTML_ELEMENTS: [Atom; 16] = [
+ atom!("br"),
+ atom!("wbr"),
+ atom!("meter"),
+ atom!("progress"),
+ atom!("canvas"),
+ atom!("embed"),
+ atom!("object"),
+ atom!("audio"),
+ atom!("iframe"),
+ atom!("img"),
+ atom!("video"),
+ atom!("frame"),
+ atom!("frameset"),
+ atom!("input"),
+ atom!("textarea"),
+ atom!("select"),
+ ];
+
+ // https://drafts.csswg.org/css-display/#unbox-svg
+ //
+ // There's a note about "Unknown elements", but there's not a good way to
+ // know what that means, or to get that information from here, and no other
+ // UA implements this either.
+ const SPECIAL_SVG_ELEMENTS: [Atom; 6] = [
+ atom!("svg"),
+ atom!("a"),
+ atom!("g"),
+ atom!("use"),
+ atom!("tspan"),
+ atom!("textPath"),
+ ];
+
+ // https://drafts.csswg.org/css-display/#unbox-html
+ if element.is_html_element() {
+ let local_name = element.local_name();
+ return SPECIAL_HTML_ELEMENTS
+ .iter()
+ .any(|name| &**name == local_name);
+ }
+
+ // https://drafts.csswg.org/css-display/#unbox-svg
+ if element.is_svg_element() {
+ if is_topmost_svg_svg_element(element) {
+ return true;
+ }
+ let local_name = element.local_name();
+ return !SPECIAL_SVG_ELEMENTS
+ .iter()
+ .any(|name| &**name == local_name);
+ }
+
+ // https://drafts.csswg.org/css-display/#unbox-mathml
+ //
+ // We always treat XUL as display: none. We don't use display:
+ // contents in XUL anyway, so should be fine to be consistent with
+ // MathML unless there's a use case for it.
+ if element.is_mathml_element() || element.is_xul_element() {
+ return true;
+ }
+
+ false
+}
+
+impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> {
+ /// Trivially constructs a new StyleAdjuster.
+ #[inline]
+ pub fn new(style: &'a mut StyleBuilder<'b>) -> Self {
+ StyleAdjuster { style }
+ }
+
+ /// <https://fullscreen.spec.whatwg.org/#new-stacking-layer>
+ ///
+ /// Any position value other than 'absolute' and 'fixed' are
+ /// computed to 'absolute' if the element is in a top layer.
+ ///
+ fn adjust_for_top_layer(&mut self) {
+ if !self.style.in_top_layer() {
+ return;
+ }
+ if !self.style.is_absolutely_positioned() {
+ self.style.mutate_box().set_position(Position::Absolute);
+ }
+ if self.style.get_box().clone_display().is_contents() {
+ self.style.mutate_box().set_display(Display::Block);
+ }
+ }
+
+ /// -webkit-box with line-clamp and vertical orientation gets turned into
+ /// flow-root at computed-value time.
+ ///
+ /// This makes the element not be a flex container, with all that it
+ /// implies, but it should be safe. It matches blink, see
+ /// https://bugzilla.mozilla.org/show_bug.cgi?id=1786147#c10
+ fn adjust_for_webkit_line_clamp(&mut self) {
+ use crate::properties::longhands::_moz_box_orient::computed_value::T as BoxOrient;
+ use crate::values::specified::box_::{DisplayInside, DisplayOutside};
+ let box_style = self.style.get_box();
+ if box_style.clone__webkit_line_clamp().is_none() {
+ return;
+ }
+ let disp = box_style.clone_display();
+ if disp.inside() != DisplayInside::WebkitBox {
+ return;
+ }
+ if self.style.get_xul().clone__moz_box_orient() != BoxOrient::Vertical {
+ return;
+ }
+ let new_display = if disp.outside() == DisplayOutside::Block {
+ Display::FlowRoot
+ } else {
+ debug_assert_eq!(disp.outside(), DisplayOutside::Inline);
+ Display::InlineBlock
+ };
+ self.style
+ .mutate_box()
+ .set_adjusted_display(new_display, false);
+ }
+
+ /// CSS 2.1 section 9.7:
+ ///
+ /// If 'position' has the value 'absolute' or 'fixed', [...] the computed
+ /// value of 'float' is 'none'.
+ ///
+ fn adjust_for_position(&mut self) {
+ if self.style.is_absolutely_positioned() && self.style.is_floating() {
+ self.style.mutate_box().set_float(Float::None);
+ }
+ }
+
+ /// Whether we should skip any item-based display property blockification on
+ /// this element.
+ fn skip_item_display_fixup<E>(&self, element: Option<E>) -> bool
+ where
+ E: TElement,
+ {
+ if let Some(pseudo) = self.style.pseudo {
+ return pseudo.skip_item_display_fixup();
+ }
+
+ element.map_or(false, |e| e.skip_item_display_fixup())
+ }
+
+ /// Apply the blockification rules based on the table in CSS 2.2 section 9.7.
+ /// <https://drafts.csswg.org/css2/visuren.html#dis-pos-flo>
+ /// A ::marker pseudo-element with 'list-style-position:outside' needs to
+ /// have its 'display' blockified, unless the ::marker is for an inline
+ /// list-item (for which 'list-style-position:outside' behaves as 'inside').
+ /// https://drafts.csswg.org/css-lists-3/#list-style-position-property
+ fn blockify_if_necessary<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>)
+ where
+ E: TElement,
+ {
+ let mut blockify = false;
+ macro_rules! blockify_if {
+ ($if_what:expr) => {
+ if !blockify {
+ blockify = $if_what;
+ }
+ };
+ }
+
+ blockify_if!(self.style.is_root_element);
+ if !self.skip_item_display_fixup(element) {
+ let parent_display = layout_parent_style.get_box().clone_display();
+ blockify_if!(parent_display.is_item_container());
+ }
+
+ let is_item_or_root = blockify;
+
+ blockify_if!(self.style.is_floating());
+ blockify_if!(self.style.is_absolutely_positioned());
+
+ if !blockify {
+ return;
+ }
+
+ let display = self.style.get_box().clone_display();
+ let blockified_display = display.equivalent_block_display(self.style.is_root_element);
+ if display != blockified_display {
+ self.style
+ .mutate_box()
+ .set_adjusted_display(blockified_display, is_item_or_root);
+ }
+ }
+
+ /// Compute a few common flags for both text and element's style.
+ fn set_bits(&mut self) {
+ let box_style = self.style.get_box();
+ let display = box_style.clone_display();
+
+ if !display.is_contents() {
+ if !self
+ .style
+ .get_text()
+ .clone_text_decoration_line()
+ .is_empty()
+ {
+ self.style
+ .add_flags(ComputedValueFlags::HAS_TEXT_DECORATION_LINES);
+ }
+
+ if self.style.get_effects().clone_opacity() == 0. {
+ self.style
+ .add_flags(ComputedValueFlags::IS_IN_OPACITY_ZERO_SUBTREE);
+ }
+ }
+
+ if self.style.is_pseudo_element() {
+ self.style
+ .add_flags(ComputedValueFlags::IS_IN_PSEUDO_ELEMENT_SUBTREE);
+ }
+
+ if self.style.is_root_element {
+ self.style
+ .add_flags(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE);
+ }
+
+ if box_style
+ .clone_effective_containment()
+ .contains(Contain::STYLE)
+ {
+ self.style
+ .add_flags(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_CONTAIN_STYLE);
+ }
+
+ if box_style.clone_container_type().is_size_container_type() {
+ self.style
+ .add_flags(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE);
+ }
+
+ #[cfg(feature = "servo-layout-2013")]
+ {
+ if self.style.get_parent_column().is_multicol() {
+ self.style.add_flags(ComputedValueFlags::CAN_BE_FRAGMENTED);
+ }
+ }
+ }
+
+ /// Adjust the style for text style.
+ ///
+ /// The adjustments here are a subset of the adjustments generally, because
+ /// text only inherits properties.
+ ///
+ /// Note that this, for Gecko, comes through Servo_ComputedValues_Inherit.
+ #[cfg(feature = "gecko")]
+ pub fn adjust_for_text(&mut self) {
+ debug_assert!(!self.style.is_root_element);
+ self.adjust_for_text_combine_upright();
+ self.adjust_for_text_in_ruby();
+ self.set_bits();
+ }
+
+ /// Change writing mode of the text frame for text-combine-upright.
+ ///
+ /// It is safe to look at our own style because we are looking at inherited
+ /// properties, and text is just plain inheritance.
+ ///
+ /// TODO(emilio): we should (Gecko too) revise these adjustments in presence
+ /// of display: contents.
+ ///
+ /// FIXME(emilio): How does this play with logical properties? Doesn't
+ /// mutating writing-mode change the potential physical sides chosen?
+ #[cfg(feature = "gecko")]
+ fn adjust_for_text_combine_upright(&mut self) {
+ use crate::computed_values::text_combine_upright::T as TextCombineUpright;
+ use crate::computed_values::writing_mode::T as WritingMode;
+ use crate::logical_geometry;
+
+ let writing_mode = self.style.get_inherited_box().clone_writing_mode();
+ let text_combine_upright = self.style.get_inherited_text().clone_text_combine_upright();
+
+ if matches!(
+ writing_mode,
+ WritingMode::VerticalRl | WritingMode::VerticalLr
+ ) && text_combine_upright == TextCombineUpright::All
+ {
+ self.style.add_flags(ComputedValueFlags::IS_TEXT_COMBINED);
+ self.style
+ .mutate_inherited_box()
+ .set_writing_mode(WritingMode::HorizontalTb);
+ self.style.writing_mode =
+ logical_geometry::WritingMode::new(self.style.get_inherited_box());
+ }
+ }
+
+ /// Unconditionally propagates the line break suppression flag to text, and
+ /// additionally it applies it if it is in any ruby box.
+ ///
+ /// This is necessary because its parent may not itself have the flag set
+ /// (e.g. ruby or ruby containers), thus we may not inherit the flag from
+ /// them.
+ #[cfg(feature = "gecko")]
+ fn adjust_for_text_in_ruby(&mut self) {
+ let parent_display = self.style.get_parent_box().clone_display();
+ if parent_display.is_ruby_type() ||
+ self.style
+ .get_parent_flags()
+ .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK)
+ {
+ self.style
+ .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK);
+ }
+ }
+
+ /// <https://drafts.csswg.org/css-writing-modes-3/#block-flow:>
+ ///
+ /// If a box has a different writing-mode value than its containing
+ /// block:
+ ///
+ /// - If the box has a specified display of inline, its display
+ /// computes to inline-block. [CSS21]
+ ///
+ /// This matches the adjustment that Gecko does, not exactly following
+ /// the spec. See also:
+ ///
+ /// <https://lists.w3.org/Archives/Public/www-style/2017Mar/0045.html>
+ /// <https://github.com/servo/servo/issues/15754>
+ fn adjust_for_writing_mode(&mut self, layout_parent_style: &ComputedValues) {
+ let our_writing_mode = self.style.get_inherited_box().clone_writing_mode();
+ let parent_writing_mode = layout_parent_style.get_inherited_box().clone_writing_mode();
+
+ if our_writing_mode != parent_writing_mode &&
+ self.style.get_box().clone_display() == Display::Inline
+ {
+ // TODO(emilio): Figure out if we can just set the adjusted display
+ // on Gecko too and unify this code path.
+ if cfg!(feature = "servo") {
+ self.style
+ .mutate_box()
+ .set_adjusted_display(Display::InlineBlock, false);
+ } else {
+ self.style.mutate_box().set_display(Display::InlineBlock);
+ }
+ }
+ }
+
+ /// This implements an out-of-date spec. The new spec moves the handling of
+ /// this to layout, which Gecko implements but Servo doesn't.
+ ///
+ /// See https://github.com/servo/servo/issues/15229
+ #[cfg(feature = "servo")]
+ fn adjust_for_alignment(&mut self, layout_parent_style: &ComputedValues) {
+ use crate::computed_values::align_items::T as AlignItems;
+ use crate::computed_values::align_self::T as AlignSelf;
+
+ if self.style.get_position().clone_align_self() == AlignSelf::Auto &&
+ !self.style.is_absolutely_positioned()
+ {
+ let self_align = match layout_parent_style.get_position().clone_align_items() {
+ AlignItems::Stretch => AlignSelf::Stretch,
+ AlignItems::Baseline => AlignSelf::Baseline,
+ AlignItems::FlexStart => AlignSelf::FlexStart,
+ AlignItems::FlexEnd => AlignSelf::FlexEnd,
+ AlignItems::Center => AlignSelf::Center,
+ };
+ self.style.mutate_position().set_align_self(self_align);
+ }
+ }
+
+ /// The initial value of border-*-width may be changed at computed value
+ /// time.
+ ///
+ /// This is moved to properties.rs for convenience.
+ fn adjust_for_border_width(&mut self) {
+ properties::adjust_border_width(self.style);
+ }
+
+ /// column-rule-style: none causes a computed column-rule-width of zero
+ /// at computed value time.
+ fn adjust_for_column_rule_width(&mut self) {
+ let column_style = self.style.get_column();
+ if !column_style.clone_column_rule_style().none_or_hidden() {
+ return;
+ }
+ if !column_style.column_rule_has_nonzero_width() {
+ return;
+ }
+ self.style
+ .mutate_column()
+ .set_column_rule_width(crate::Zero::zero());
+ }
+
+ /// outline-style: none causes a computed outline-width of zero at computed
+ /// value time.
+ fn adjust_for_outline_width(&mut self) {
+ let outline = self.style.get_outline();
+ if !outline.clone_outline_style().none_or_hidden() {
+ return;
+ }
+ if !outline.outline_has_nonzero_width() {
+ return;
+ }
+ self.style
+ .mutate_outline()
+ .set_outline_width(crate::Zero::zero());
+ }
+
+ /// CSS overflow-x and overflow-y require some fixup as well in some cases.
+ /// https://drafts.csswg.org/css-overflow-3/#overflow-properties
+ /// "Computed value: as specified, except with `visible`/`clip` computing to
+ /// `auto`/`hidden` (respectively) if one of `overflow-x` or `overflow-y` is
+ /// neither `visible` nor `clip`."
+ fn adjust_for_overflow(&mut self) {
+ let overflow_x = self.style.get_box().clone_overflow_x();
+ let overflow_y = self.style.get_box().clone_overflow_y();
+ if overflow_x == overflow_y {
+ return; // optimization for the common case
+ }
+
+ if overflow_x.is_scrollable() != overflow_y.is_scrollable() {
+ let box_style = self.style.mutate_box();
+ box_style.set_overflow_x(overflow_x.to_scrollable());
+ box_style.set_overflow_y(overflow_y.to_scrollable());
+ }
+ }
+
+ fn adjust_for_contain(&mut self) {
+ let box_style = self.style.get_box();
+ let container_type = box_style.clone_container_type();
+ let content_visibility = box_style.clone_content_visibility();
+ if container_type == ContainerType::Normal &&
+ content_visibility == ContentVisibility::Visible
+ {
+ debug_assert_eq!(
+ box_style.clone_contain(),
+ box_style.clone_effective_containment()
+ );
+ return;
+ }
+ let old_contain = box_style.clone_contain();
+ let mut new_contain = old_contain;
+ match content_visibility {
+ ContentVisibility::Visible => {},
+ // `content-visibility:auto` also applies size containment when content
+ // is not relevant (and therefore skipped). This is checked in
+ // nsIFrame::GetContainSizeAxes.
+ ContentVisibility::Auto => {
+ new_contain.insert(Contain::LAYOUT | Contain::PAINT | Contain::STYLE)
+ },
+ ContentVisibility::Hidden => new_contain
+ .insert(Contain::LAYOUT | Contain::PAINT | Contain::SIZE | Contain::STYLE),
+ }
+ match container_type {
+ ContainerType::Normal => {},
+ // https://drafts.csswg.org/css-contain-3/#valdef-container-type-inline-size:
+ // Applies layout containment, style containment, and inline-size
+ // containment to the principal box.
+ ContainerType::InlineSize => {
+ new_contain.insert(Contain::LAYOUT | Contain::STYLE | Contain::INLINE_SIZE)
+ },
+ // https://drafts.csswg.org/css-contain-3/#valdef-container-type-size:
+ // Applies layout containment, style containment, and size
+ // containment to the principal box.
+ ContainerType::Size => {
+ new_contain.insert(Contain::LAYOUT | Contain::STYLE | Contain::SIZE)
+ },
+ }
+ if new_contain == old_contain {
+ debug_assert_eq!(
+ box_style.clone_contain(),
+ box_style.clone_effective_containment()
+ );
+ return;
+ }
+ self.style
+ .mutate_box()
+ .set_effective_containment(new_contain);
+ }
+
+ /// content-visibility: auto should force contain-intrinsic-size to gain
+ /// an auto value
+ ///
+ /// <https://github.com/w3c/csswg-drafts/issues/8407>
+ fn adjust_for_contain_intrinsic_size(&mut self) {
+ let content_visibility = self.style.get_box().clone_content_visibility();
+ if content_visibility != ContentVisibility::Auto {
+ return;
+ }
+
+ let pos = self.style.get_position();
+ let new_width = pos.clone_contain_intrinsic_width().add_auto_if_needed();
+ let new_height = pos.clone_contain_intrinsic_height().add_auto_if_needed();
+ if new_width.is_none() && new_height.is_none() {
+ return;
+ }
+
+ let pos = self.style.mutate_position();
+ if let Some(width) = new_width {
+ pos.set_contain_intrinsic_width(width);
+ }
+ if let Some(height) = new_height {
+ pos.set_contain_intrinsic_height(height);
+ }
+ }
+
+ /// Handles the relevant sections in:
+ ///
+ /// https://drafts.csswg.org/css-display/#unbox-html
+ ///
+ /// And forbidding display: contents in pseudo-elements, at least for now.
+ #[cfg(feature = "gecko")]
+ fn adjust_for_prohibited_display_contents<E>(&mut self, element: Option<E>)
+ where
+ E: TElement,
+ {
+ if self.style.get_box().clone_display() != Display::Contents {
+ return;
+ }
+
+ // FIXME(emilio): ::before and ::after should support display: contents,
+ // see bug 1418138.
+ if self.style.pseudo.is_some() {
+ self.style.mutate_box().set_display(Display::Inline);
+ return;
+ }
+
+ let element = match element {
+ Some(e) => e,
+ None => return,
+ };
+
+ if is_effective_display_none_for_display_contents(element) {
+ self.style.mutate_box().set_display(Display::None);
+ }
+ }
+
+ /// <textarea>'s editor root needs to inherit the overflow value from its
+ /// parent, but we need to make sure it's still scrollable.
+ #[cfg(feature = "gecko")]
+ fn adjust_for_text_control_editing_root(&mut self) {
+ use crate::selector_parser::PseudoElement;
+
+ if self.style.pseudo != Some(&PseudoElement::MozTextControlEditingRoot) {
+ return;
+ }
+
+ let box_style = self.style.get_box();
+ let overflow_x = box_style.clone_overflow_x();
+ let overflow_y = box_style.clone_overflow_y();
+
+ // If at least one is scrollable we'll adjust the other one in
+ // adjust_for_overflow if needed.
+ if overflow_x.is_scrollable() || overflow_y.is_scrollable() {
+ return;
+ }
+
+ let box_style = self.style.mutate_box();
+ box_style.set_overflow_x(Overflow::Auto);
+ box_style.set_overflow_y(Overflow::Auto);
+ }
+
+ /// If a <fieldset> has grid/flex display type, we need to inherit
+ /// this type into its ::-moz-fieldset-content anonymous box.
+ ///
+ /// NOTE(emilio): We don't need to handle the display change for this case
+ /// in matching.rs because anonymous box restyling works separately to the
+ /// normal cascading process.
+ #[cfg(feature = "gecko")]
+ fn adjust_for_fieldset_content(&mut self, layout_parent_style: &ComputedValues) {
+ use crate::selector_parser::PseudoElement;
+
+ if self.style.pseudo != Some(&PseudoElement::FieldsetContent) {
+ return;
+ }
+
+ // TODO We actually want style from parent rather than layout
+ // parent, so that this fixup doesn't happen incorrectly when
+ // when <fieldset> has "display: contents".
+ let parent_display = layout_parent_style.get_box().clone_display();
+ let new_display = match parent_display {
+ Display::Flex | Display::InlineFlex => Some(Display::Flex),
+ Display::Grid | Display::InlineGrid => Some(Display::Grid),
+ _ => None,
+ };
+ if let Some(new_display) = new_display {
+ self.style.mutate_box().set_display(new_display);
+ }
+ }
+
+ /// -moz-center, -moz-left and -moz-right are used for HTML's alignment.
+ ///
+ /// This is covering the <div align="right"><table>...</table></div> case.
+ ///
+ /// In this case, we don't want to inherit the text alignment into the
+ /// table.
+ #[cfg(feature = "gecko")]
+ fn adjust_for_table_text_align(&mut self) {
+ use crate::properties::longhands::text_align::computed_value::T as TextAlign;
+ if self.style.get_box().clone_display() != Display::Table {
+ return;
+ }
+
+ match self.style.get_inherited_text().clone_text_align() {
+ TextAlign::MozLeft | TextAlign::MozCenter | TextAlign::MozRight => {},
+ _ => return,
+ }
+
+ self.style
+ .mutate_inherited_text()
+ .set_text_align(TextAlign::Start)
+ }
+
+ /// Computes the used text decoration for Servo.
+ ///
+ /// FIXME(emilio): This is a layout tree concept, should move away from
+ /// style, since otherwise we're going to have the same subtle bugs WebKit
+ /// and Blink have with this very same thing.
+ #[cfg(feature = "servo")]
+ fn adjust_for_text_decorations_in_effect(&mut self) {
+ use crate::values::computed::text::TextDecorationsInEffect;
+
+ let decorations_in_effect = TextDecorationsInEffect::from_style(&self.style);
+ if self.style.get_inherited_text().text_decorations_in_effect != decorations_in_effect {
+ self.style
+ .mutate_inherited_text()
+ .text_decorations_in_effect = decorations_in_effect;
+ }
+ }
+
+ #[cfg(feature = "gecko")]
+ fn should_suppress_linebreak<E>(
+ &self,
+ layout_parent_style: &ComputedValues,
+ element: Option<E>,
+ ) -> bool
+ where
+ E: TElement,
+ {
+ // Line break suppression should only be propagated to in-flow children.
+ if self.style.is_floating() || self.style.is_absolutely_positioned() {
+ return false;
+ }
+ let parent_display = layout_parent_style.get_box().clone_display();
+ if layout_parent_style
+ .flags
+ .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK)
+ {
+ // Line break suppression is propagated to any children of
+ // line participants.
+ if parent_display.is_line_participant() {
+ return true;
+ }
+ }
+ match self.style.get_box().clone_display() {
+ // Ruby base and text are always non-breakable.
+ Display::RubyBase | Display::RubyText => true,
+ // Ruby base container and text container are breakable.
+ // Non-HTML elements may not form ruby base / text container because
+ // they may not respect ruby-internal display values, so we can't
+ // make them escaped from line break suppression.
+ // Note that, when certain HTML tags, e.g. form controls, have ruby
+ // level container display type, they could also escape from the
+ // line break suppression flag while they shouldn't. However, it is
+ // generally fine as far as they can't break the line inside them.
+ Display::RubyBaseContainer | Display::RubyTextContainer
+ if element.map_or(true, |e| e.is_html_element()) =>
+ {
+ false
+ },
+ // Anything else is non-breakable if and only if its layout parent
+ // has a ruby display type, because any of the ruby boxes can be
+ // anonymous.
+ _ => parent_display.is_ruby_type(),
+ }
+ }
+
+ /// Do ruby-related style adjustments, which include:
+ /// * propagate the line break suppression flag,
+ /// * inlinify block descendants,
+ /// * suppress border and padding for ruby level containers,
+ /// * correct unicode-bidi.
+ #[cfg(feature = "gecko")]
+ fn adjust_for_ruby<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>)
+ where
+ E: TElement,
+ {
+ use crate::properties::longhands::unicode_bidi::computed_value::T as UnicodeBidi;
+
+ let self_display = self.style.get_box().clone_display();
+ // Check whether line break should be suppressed for this element.
+ if self.should_suppress_linebreak(layout_parent_style, element) {
+ self.style
+ .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK);
+ // Inlinify the display type if allowed.
+ if !self.skip_item_display_fixup(element) {
+ let inline_display = self_display.inlinify();
+ if self_display != inline_display {
+ self.style
+ .mutate_box()
+ .set_adjusted_display(inline_display, false);
+ }
+ }
+ }
+ // Suppress border and padding for ruby level containers.
+ // This is actually not part of the spec. It is currently unspecified
+ // how border and padding should be handled for ruby level container,
+ // and suppressing them here make it easier for layout to handle.
+ if self_display.is_ruby_level_container() {
+ self.style.reset_border_struct();
+ self.style.reset_padding_struct();
+ }
+
+ // Force bidi isolation on all internal ruby boxes and ruby container
+ // per spec https://drafts.csswg.org/css-ruby-1/#bidi
+ if self_display.is_ruby_type() {
+ let new_value = match self.style.get_text().clone_unicode_bidi() {
+ UnicodeBidi::Normal | UnicodeBidi::Embed => Some(UnicodeBidi::Isolate),
+ UnicodeBidi::BidiOverride => Some(UnicodeBidi::IsolateOverride),
+ _ => None,
+ };
+ if let Some(new_value) = new_value {
+ self.style.mutate_text().set_unicode_bidi(new_value);
+ }
+ }
+ }
+
+ /// Computes the RELEVANT_LINK_VISITED flag based on the parent style and on
+ /// whether we're a relevant link.
+ ///
+ /// NOTE(emilio): We don't do this for text styles, which is... dubious, but
+ /// Gecko doesn't seem to do it either. It's extremely easy to do if needed
+ /// though.
+ ///
+ /// FIXME(emilio): This isn't technically a style adjustment thingie, could
+ /// it move somewhere else?
+ fn adjust_for_visited<E>(&mut self, element: Option<E>)
+ where
+ E: TElement,
+ {
+ if !self.style.has_visited_style() {
+ return;
+ }
+
+ let is_link_element = self.style.pseudo.is_none() && element.map_or(false, |e| e.is_link());
+
+ if !is_link_element {
+ return;
+ }
+
+ if element.unwrap().is_visited_link() {
+ self.style
+ .add_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED);
+ } else {
+ // Need to remove to handle unvisited link inside visited.
+ self.style
+ .remove_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED);
+ }
+ }
+
+ /// Resolves "justify-items: legacy" based on the inherited style if needed
+ /// to comply with:
+ ///
+ /// <https://drafts.csswg.org/css-align/#valdef-justify-items-legacy>
+ #[cfg(feature = "gecko")]
+ fn adjust_for_justify_items(&mut self) {
+ use crate::values::specified::align;
+ let justify_items = self.style.get_position().clone_justify_items();
+ if justify_items.specified.0 != align::AlignFlags::LEGACY {
+ return;
+ }
+
+ let parent_justify_items = self.style.get_parent_position().clone_justify_items();
+
+ if !parent_justify_items
+ .computed
+ .0
+ .contains(align::AlignFlags::LEGACY)
+ {
+ return;
+ }
+
+ if parent_justify_items.computed == justify_items.computed {
+ return;
+ }
+
+ self.style
+ .mutate_position()
+ .set_computed_justify_items(parent_justify_items.computed);
+ }
+
+ /// If '-webkit-appearance' is 'menulist' on a <select> element then
+ /// the computed value of 'line-height' is 'normal'.
+ ///
+ /// https://github.com/w3c/csswg-drafts/issues/3257
+ #[cfg(feature = "gecko")]
+ fn adjust_for_appearance<E>(&mut self, element: Option<E>)
+ where
+ E: TElement,
+ {
+ use crate::properties::longhands::appearance::computed_value::T as Appearance;
+ use crate::properties::longhands::line_height::computed_value::T as LineHeight;
+
+ let box_ = self.style.get_box();
+ let appearance = match box_.clone_appearance() {
+ Appearance::Auto => box_.clone__moz_default_appearance(),
+ a => a,
+ };
+
+ if appearance == Appearance::Menulist {
+ if self.style.get_font().clone_line_height() == LineHeight::normal() {
+ return;
+ }
+ if self.style.pseudo.is_some() {
+ return;
+ }
+ let is_html_select_element = element.map_or(false, |e| {
+ e.is_html_element() && e.local_name() == &*atom!("select")
+ });
+ if !is_html_select_element {
+ return;
+ }
+ self.style
+ .mutate_font()
+ .set_line_height(LineHeight::normal());
+ }
+ }
+
+ /// A legacy ::marker (i.e. no 'content') without an author-specified 'font-family'
+ /// and 'list-style-type:disc|circle|square|disclosure-closed|disclosure-open'
+ /// is assigned 'font-family:-moz-bullet-font'. (This is for <ul><li> etc.)
+ /// We don't want synthesized italic/bold for this font, so turn that off too.
+ /// Likewise for 'letter/word-spacing' -- unless the author specified it then reset
+ /// them to their initial value because traditionally we never added such spacing
+ /// between a legacy bullet and the list item's content, so we keep that behavior
+ /// for web-compat reasons.
+ /// We intentionally don't check 'list-style-image' below since we want it to use
+ /// the same font as its fallback ('list-style-type') in case it fails to load.
+ #[cfg(feature = "gecko")]
+ fn adjust_for_marker_pseudo(&mut self) {
+ use crate::values::computed::counters::Content;
+ use crate::values::computed::font::{FontFamily, FontSynthesis};
+ use crate::values::computed::text::{LetterSpacing, WordSpacing};
+
+ let is_legacy_marker = self.style.pseudo.map_or(false, |p| p.is_marker()) &&
+ self.style.get_list().clone_list_style_type().is_bullet() &&
+ self.style.get_counters().clone_content() == Content::Normal;
+ if !is_legacy_marker {
+ return;
+ }
+ let flags = self.style.flags.get();
+ if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_FAMILY) {
+ self.style
+ .mutate_font()
+ .set_font_family(FontFamily::moz_bullet().clone());
+
+ // FIXME(mats): We can remove this if support for font-synthesis is added to @font-face rules.
+ // Then we can add it to the @font-face rule in html.css instead.
+ // https://github.com/w3c/csswg-drafts/issues/6081
+ if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT) {
+ self.style
+ .mutate_font()
+ .set_font_synthesis_weight(FontSynthesis::None);
+ }
+ if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE) {
+ self.style
+ .mutate_font()
+ .set_font_synthesis_style(FontSynthesis::None);
+ }
+ }
+ if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_LETTER_SPACING) {
+ self.style
+ .mutate_inherited_text()
+ .set_letter_spacing(LetterSpacing::normal());
+ }
+ if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_WORD_SPACING) {
+ self.style
+ .mutate_inherited_text()
+ .set_word_spacing(WordSpacing::normal());
+ }
+ }
+
+ /// Adjusts the style to account for various fixups that don't fit naturally
+ /// into the cascade.
+ ///
+ /// When comparing to Gecko, this is similar to the work done by
+ /// `ComputedStyle::ApplyStyleFixups`, plus some parts of
+ /// `nsStyleSet::GetContext`.
+ pub fn adjust<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>)
+ where
+ E: TElement,
+ {
+ if cfg!(debug_assertions) {
+ if element.map_or(false, |e| e.is_pseudo_element()) {
+ // It'd be nice to assert `self.style.pseudo == Some(&pseudo)`,
+ // but we do resolve ::-moz-list pseudos on ::before / ::after
+ // content, sigh.
+ debug_assert!(self.style.pseudo.is_some(), "Someone really messed up");
+ }
+ }
+ // FIXME(emilio): The apply_declarations callsite in Servo's
+ // animation, and the font stuff for Gecko
+ // (Stylist::compute_for_declarations) should pass an element to
+ // cascade(), then we can make this assertion hold everywhere.
+ // debug_assert!(
+ // element.is_some() || self.style.pseudo.is_some(),
+ // "Should always have an element around for non-pseudo styles"
+ // );
+
+ self.adjust_for_visited(element);
+ #[cfg(feature = "gecko")]
+ {
+ self.adjust_for_prohibited_display_contents(element);
+ self.adjust_for_fieldset_content(layout_parent_style);
+ // NOTE: It's important that this happens before
+ // adjust_for_overflow.
+ self.adjust_for_text_control_editing_root();
+ }
+ self.adjust_for_top_layer();
+ self.blockify_if_necessary(layout_parent_style, element);
+ self.adjust_for_webkit_line_clamp();
+ self.adjust_for_position();
+ self.adjust_for_overflow();
+ self.adjust_for_contain();
+ self.adjust_for_contain_intrinsic_size();
+ #[cfg(feature = "gecko")]
+ {
+ self.adjust_for_table_text_align();
+ self.adjust_for_justify_items();
+ }
+ #[cfg(feature = "servo")]
+ {
+ self.adjust_for_alignment(layout_parent_style);
+ }
+ self.adjust_for_border_width();
+ self.adjust_for_column_rule_width();
+ self.adjust_for_outline_width();
+ self.adjust_for_writing_mode(layout_parent_style);
+ #[cfg(feature = "gecko")]
+ {
+ self.adjust_for_ruby(layout_parent_style, element);
+ }
+ #[cfg(feature = "servo")]
+ {
+ self.adjust_for_text_decorations_in_effect();
+ }
+ #[cfg(feature = "gecko")]
+ {
+ self.adjust_for_appearance(element);
+ self.adjust_for_marker_pseudo();
+ }
+ self.set_bits();
+ }
+}
diff --git a/servo/components/style/style_resolver.rs b/servo/components/style/style_resolver.rs
new file mode 100644
index 0000000000..5c940ad2be
--- /dev/null
+++ b/servo/components/style/style_resolver.rs
@@ -0,0 +1,585 @@
+/* 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/. */
+
+//! Style resolution for a given element or pseudo-element.
+
+use crate::applicable_declarations::ApplicableDeclarationList;
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::context::{CascadeInputs, ElementCascadeInputs, StyleContext};
+use crate::data::{EagerPseudoStyles, ElementStyles};
+use crate::dom::TElement;
+use crate::matching::MatchMethods;
+use crate::properties::longhands::display::computed_value::T as Display;
+use crate::properties::{ComputedValues, FirstLineReparenting};
+use crate::rule_tree::StrongRuleNode;
+use crate::selector_parser::{PseudoElement, SelectorImpl};
+use crate::stylist::RuleInclusion;
+use log::Level::Trace;
+use selectors::matching::{
+ MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, VisitedHandlingMode,
+};
+use servo_arc::Arc;
+
+/// Whether pseudo-elements should be resolved or not.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum PseudoElementResolution {
+ /// Only resolve pseudo-styles if possibly applicable.
+ IfApplicable,
+ /// Force pseudo-element resolution.
+ Force,
+}
+
+/// A struct that takes care of resolving the style of a given element.
+pub struct StyleResolverForElement<'a, 'ctx, 'le, E>
+where
+ 'ctx: 'a,
+ 'le: 'ctx,
+ E: TElement + MatchMethods + 'le,
+{
+ element: E,
+ context: &'a mut StyleContext<'ctx, E>,
+ rule_inclusion: RuleInclusion,
+ pseudo_resolution: PseudoElementResolution,
+ _marker: ::std::marker::PhantomData<&'le E>,
+}
+
+struct MatchingResults {
+ rule_node: StrongRuleNode,
+ flags: ComputedValueFlags,
+}
+
+/// A style returned from the resolver machinery.
+pub struct ResolvedStyle(pub Arc<ComputedValues>);
+
+/// The primary style of an element or an element-backed pseudo-element.
+pub struct PrimaryStyle {
+ /// The style itself.
+ pub style: ResolvedStyle,
+ /// Whether the style was reused from another element via the rule node (see
+ /// `StyleSharingCache::lookup_by_rules`).
+ pub reused_via_rule_node: bool,
+}
+
+/// A set of style returned from the resolver machinery.
+pub struct ResolvedElementStyles {
+ /// Primary style.
+ pub primary: PrimaryStyle,
+ /// Pseudo styles.
+ pub pseudos: EagerPseudoStyles,
+}
+
+impl ResolvedElementStyles {
+ /// Convenience accessor for the primary style.
+ pub fn primary_style(&self) -> &Arc<ComputedValues> {
+ &self.primary.style.0
+ }
+
+ /// Convenience mutable accessor for the style.
+ pub fn primary_style_mut(&mut self) -> &mut Arc<ComputedValues> {
+ &mut self.primary.style.0
+ }
+}
+
+impl PrimaryStyle {
+ /// Convenience accessor for the style.
+ pub fn style(&self) -> &ComputedValues {
+ &*self.style.0
+ }
+}
+
+impl From<ResolvedElementStyles> for ElementStyles {
+ fn from(r: ResolvedElementStyles) -> ElementStyles {
+ ElementStyles {
+ primary: Some(r.primary.style.0),
+ pseudos: r.pseudos,
+ }
+ }
+}
+
+fn with_default_parent_styles<E, F, R>(element: E, f: F) -> R
+where
+ E: TElement,
+ F: FnOnce(Option<&ComputedValues>, Option<&ComputedValues>) -> R,
+{
+ let parent_el = element.inheritance_parent();
+ let parent_data = parent_el.as_ref().and_then(|e| e.borrow_data());
+ let parent_style = parent_data.as_ref().map(|d| d.styles.primary());
+
+ let mut layout_parent_el = parent_el.clone();
+ let layout_parent_data;
+ let mut layout_parent_style = parent_style;
+ if parent_style.map_or(false, |s| s.is_display_contents()) {
+ layout_parent_el = Some(layout_parent_el.unwrap().layout_parent());
+ layout_parent_data = layout_parent_el.as_ref().unwrap().borrow_data().unwrap();
+ layout_parent_style = Some(layout_parent_data.styles.primary());
+ }
+
+ f(
+ parent_style.map(|x| &**x),
+ layout_parent_style.map(|s| &**s),
+ )
+}
+
+fn layout_parent_style_for_pseudo<'a>(
+ primary_style: &'a PrimaryStyle,
+ layout_parent_style: Option<&'a ComputedValues>,
+) -> Option<&'a ComputedValues> {
+ if primary_style.style().is_display_contents() {
+ layout_parent_style
+ } else {
+ Some(primary_style.style())
+ }
+}
+
+fn eager_pseudo_is_definitely_not_generated(
+ pseudo: &PseudoElement,
+ style: &ComputedValues,
+) -> bool {
+ if !pseudo.is_before_or_after() {
+ return false;
+ }
+
+ if !style
+ .flags
+ .intersects(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE) &&
+ style.get_box().clone_display() == Display::None
+ {
+ return true;
+ }
+
+ if !style
+ .flags
+ .intersects(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE) &&
+ style.ineffective_content_property()
+ {
+ return true;
+ }
+
+ false
+}
+
+impl<'a, 'ctx, 'le, E> StyleResolverForElement<'a, 'ctx, 'le, E>
+where
+ 'ctx: 'a,
+ 'le: 'ctx,
+ E: TElement + MatchMethods + 'le,
+{
+ /// Trivially construct a new StyleResolverForElement.
+ pub fn new(
+ element: E,
+ context: &'a mut StyleContext<'ctx, E>,
+ rule_inclusion: RuleInclusion,
+ pseudo_resolution: PseudoElementResolution,
+ ) -> Self {
+ Self {
+ element,
+ context,
+ rule_inclusion,
+ pseudo_resolution,
+ _marker: ::std::marker::PhantomData,
+ }
+ }
+
+ /// Resolve just the style of a given element.
+ pub fn resolve_primary_style(
+ &mut self,
+ parent_style: Option<&ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ ) -> PrimaryStyle {
+ let primary_results = self.match_primary(VisitedHandlingMode::AllLinksUnvisited);
+
+ let inside_link = parent_style.map_or(false, |s| s.visited_style().is_some());
+
+ let visited_rules = if self.context.shared.visited_styles_enabled &&
+ (inside_link || self.element.is_link())
+ {
+ let visited_matching_results =
+ self.match_primary(VisitedHandlingMode::RelevantLinkVisited);
+ Some(visited_matching_results.rule_node)
+ } else {
+ None
+ };
+
+ self.cascade_primary_style(
+ CascadeInputs {
+ rules: Some(primary_results.rule_node),
+ visited_rules,
+ flags: primary_results.flags,
+ },
+ parent_style,
+ layout_parent_style,
+ )
+ }
+
+ fn cascade_primary_style(
+ &mut self,
+ inputs: CascadeInputs,
+ parent_style: Option<&ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ ) -> PrimaryStyle {
+ // Before doing the cascade, check the sharing cache and see if we can
+ // reuse the style via rule node identity.
+ let may_reuse = self.element.matches_user_and_content_rules() &&
+ parent_style.is_some() &&
+ inputs.rules.is_some() &&
+ // If this style was considered in any way for relative selector matching,
+ // we do not want to lose that fact by sharing a style with something that
+ // did not.
+ !inputs.flags.contains(ComputedValueFlags::CONSIDERED_RELATIVE_SELECTOR);
+
+ if may_reuse {
+ let cached = self.context.thread_local.sharing_cache.lookup_by_rules(
+ self.context.shared,
+ parent_style.unwrap(),
+ inputs.rules.as_ref().unwrap(),
+ inputs.visited_rules.as_ref(),
+ self.element,
+ );
+ if let Some(mut primary_style) = cached {
+ self.context.thread_local.statistics.styles_reused += 1;
+ primary_style.reused_via_rule_node |= true;
+ return primary_style;
+ }
+ }
+
+ // No style to reuse. Cascade the style, starting with visited style
+ // if necessary.
+ PrimaryStyle {
+ style: self.cascade_style_and_visited(
+ inputs,
+ parent_style,
+ layout_parent_style,
+ /* pseudo = */ None,
+ ),
+ reused_via_rule_node: false,
+ }
+ }
+
+ /// Resolve the style of a given element, and all its eager pseudo-elements.
+ pub fn resolve_style(
+ &mut self,
+ parent_style: Option<&ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ ) -> ResolvedElementStyles {
+ let primary_style = self.resolve_primary_style(parent_style, layout_parent_style);
+
+ let mut pseudo_styles = EagerPseudoStyles::default();
+
+ if !self.element.is_pseudo_element() {
+ let layout_parent_style_for_pseudo =
+ layout_parent_style_for_pseudo(&primary_style, layout_parent_style);
+ SelectorImpl::each_eagerly_cascaded_pseudo_element(|pseudo| {
+ let pseudo_style = self.resolve_pseudo_style(
+ &pseudo,
+ &primary_style,
+ layout_parent_style_for_pseudo,
+ );
+
+ if let Some(style) = pseudo_style {
+ if !matches!(self.pseudo_resolution, PseudoElementResolution::Force) &&
+ eager_pseudo_is_definitely_not_generated(&pseudo, &style.0)
+ {
+ return;
+ }
+ pseudo_styles.set(&pseudo, style.0);
+ }
+ })
+ }
+
+ ResolvedElementStyles {
+ primary: primary_style,
+ pseudos: pseudo_styles,
+ }
+ }
+
+ /// Resolve an element's styles with the default inheritance parent/layout
+ /// parents.
+ pub fn resolve_style_with_default_parents(&mut self) -> ResolvedElementStyles {
+ with_default_parent_styles(self.element, |parent_style, layout_parent_style| {
+ self.resolve_style(parent_style, layout_parent_style)
+ })
+ }
+
+ /// Cascade a set of rules, using the default parent for inheritance.
+ pub fn cascade_style_and_visited_with_default_parents(
+ &mut self,
+ inputs: CascadeInputs,
+ ) -> ResolvedStyle {
+ with_default_parent_styles(self.element, |parent_style, layout_parent_style| {
+ self.cascade_style_and_visited(
+ inputs,
+ parent_style,
+ layout_parent_style,
+ /* pseudo = */ None,
+ )
+ })
+ }
+
+ /// Cascade a set of rules for pseudo element, using the default parent for inheritance.
+ pub fn cascade_style_and_visited_for_pseudo_with_default_parents(
+ &mut self,
+ inputs: CascadeInputs,
+ pseudo: &PseudoElement,
+ primary_style: &PrimaryStyle,
+ ) -> ResolvedStyle {
+ with_default_parent_styles(self.element, |_, layout_parent_style| {
+ let layout_parent_style_for_pseudo =
+ layout_parent_style_for_pseudo(primary_style, layout_parent_style);
+
+ self.cascade_style_and_visited(
+ inputs,
+ Some(primary_style.style()),
+ layout_parent_style_for_pseudo,
+ Some(pseudo),
+ )
+ })
+ }
+
+ fn cascade_style_and_visited(
+ &mut self,
+ inputs: CascadeInputs,
+ parent_style: Option<&ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ pseudo: Option<&PseudoElement>,
+ ) -> ResolvedStyle {
+ debug_assert!(pseudo.map_or(true, |p| p.is_eager()));
+
+ let implemented_pseudo = self.element.implemented_pseudo_element();
+ let pseudo = pseudo.or(implemented_pseudo.as_ref());
+
+ let mut conditions = Default::default();
+ let values = self.context.shared.stylist.cascade_style_and_visited(
+ Some(self.element),
+ pseudo,
+ inputs,
+ &self.context.shared.guards,
+ parent_style,
+ layout_parent_style,
+ FirstLineReparenting::No,
+ Some(&self.context.thread_local.rule_cache),
+ &mut conditions,
+ );
+
+ self.context.thread_local.rule_cache.insert_if_possible(
+ &self.context.shared.guards,
+ &values,
+ pseudo,
+ &conditions,
+ );
+
+ ResolvedStyle(values)
+ }
+
+ /// Cascade the element and pseudo-element styles with the default parents.
+ pub fn cascade_styles_with_default_parents(
+ &mut self,
+ inputs: ElementCascadeInputs,
+ ) -> ResolvedElementStyles {
+ with_default_parent_styles(self.element, move |parent_style, layout_parent_style| {
+ let primary_style =
+ self.cascade_primary_style(inputs.primary, parent_style, layout_parent_style);
+
+ let mut pseudo_styles = EagerPseudoStyles::default();
+ if let Some(mut pseudo_array) = inputs.pseudos.into_array() {
+ let layout_parent_style_for_pseudo = if primary_style.style().is_display_contents()
+ {
+ layout_parent_style
+ } else {
+ Some(primary_style.style())
+ };
+
+ for (i, inputs) in pseudo_array.iter_mut().enumerate() {
+ if let Some(inputs) = inputs.take() {
+ let pseudo = PseudoElement::from_eager_index(i);
+
+ let style = self.cascade_style_and_visited(
+ inputs,
+ Some(primary_style.style()),
+ layout_parent_style_for_pseudo,
+ Some(&pseudo),
+ );
+
+ if !matches!(self.pseudo_resolution, PseudoElementResolution::Force) &&
+ eager_pseudo_is_definitely_not_generated(&pseudo, &style.0)
+ {
+ continue;
+ }
+
+ pseudo_styles.set(&pseudo, style.0);
+ }
+ }
+ }
+
+ ResolvedElementStyles {
+ primary: primary_style,
+ pseudos: pseudo_styles,
+ }
+ })
+ }
+
+ fn resolve_pseudo_style(
+ &mut self,
+ pseudo: &PseudoElement,
+ originating_element_style: &PrimaryStyle,
+ layout_parent_style: Option<&ComputedValues>,
+ ) -> Option<ResolvedStyle> {
+ let MatchingResults {
+ rule_node,
+ mut flags,
+ } = self.match_pseudo(
+ &originating_element_style.style.0,
+ pseudo,
+ VisitedHandlingMode::AllLinksUnvisited,
+ )?;
+
+ let mut visited_rules = None;
+ if originating_element_style.style().visited_style().is_some() {
+ visited_rules = self
+ .match_pseudo(
+ &originating_element_style.style.0,
+ pseudo,
+ VisitedHandlingMode::RelevantLinkVisited,
+ )
+ .map(|results| {
+ flags |= results.flags;
+ results.rule_node
+ });
+ }
+
+ Some(self.cascade_style_and_visited(
+ CascadeInputs {
+ rules: Some(rule_node),
+ visited_rules,
+ flags,
+ },
+ Some(originating_element_style.style()),
+ layout_parent_style,
+ Some(pseudo),
+ ))
+ }
+
+ fn match_primary(&mut self, visited_handling: VisitedHandlingMode) -> MatchingResults {
+ debug!(
+ "Match primary for {:?}, visited: {:?}",
+ self.element, visited_handling
+ );
+ let mut applicable_declarations = ApplicableDeclarationList::new();
+
+ let bloom_filter = self.context.thread_local.bloom_filter.filter();
+ let selector_caches = &mut self.context.thread_local.selector_caches;
+ let mut matching_context = MatchingContext::new_for_visited(
+ MatchingMode::Normal,
+ Some(bloom_filter),
+ selector_caches,
+ visited_handling,
+ self.context.shared.quirks_mode(),
+ NeedsSelectorFlags::Yes,
+ MatchingForInvalidation::No,
+ );
+
+ let stylist = &self.context.shared.stylist;
+ let implemented_pseudo = self.element.implemented_pseudo_element();
+ // Compute the primary rule node.
+ stylist.push_applicable_declarations(
+ self.element,
+ implemented_pseudo.as_ref(),
+ self.element.style_attribute(),
+ self.element.smil_override(),
+ self.element.animation_declarations(self.context.shared),
+ self.rule_inclusion,
+ &mut applicable_declarations,
+ &mut matching_context,
+ );
+
+ // FIXME(emilio): This is a hack for animations, and should go away.
+ self.element.unset_dirty_style_attribute();
+
+ let rule_node = stylist
+ .rule_tree()
+ .compute_rule_node(&mut applicable_declarations, &self.context.shared.guards);
+
+ if log_enabled!(Trace) {
+ trace!("Matched rules for {:?}:", self.element);
+ for rn in rule_node.self_and_ancestors() {
+ let source = rn.style_source();
+ if source.is_some() {
+ trace!(" > {:?}", source);
+ }
+ }
+ }
+
+ MatchingResults {
+ rule_node,
+ flags: matching_context.extra_data.cascade_input_flags,
+ }
+ }
+
+ fn match_pseudo(
+ &mut self,
+ originating_element_style: &ComputedValues,
+ pseudo_element: &PseudoElement,
+ visited_handling: VisitedHandlingMode,
+ ) -> Option<MatchingResults> {
+ debug!(
+ "Match pseudo {:?} for {:?}, visited: {:?}",
+ self.element, pseudo_element, visited_handling
+ );
+ debug_assert!(pseudo_element.is_eager());
+ debug_assert!(
+ !self.element.is_pseudo_element(),
+ "Element pseudos can't have any other eager pseudo."
+ );
+
+ let mut applicable_declarations = ApplicableDeclarationList::new();
+
+ let stylist = &self.context.shared.stylist;
+
+ if !self
+ .element
+ .may_generate_pseudo(pseudo_element, originating_element_style)
+ {
+ return None;
+ }
+
+ let bloom_filter = self.context.thread_local.bloom_filter.filter();
+ let selector_caches = &mut self.context.thread_local.selector_caches;
+
+ let mut matching_context = MatchingContext::<'_, E::Impl>::new_for_visited(
+ MatchingMode::ForStatelessPseudoElement,
+ Some(bloom_filter),
+ selector_caches,
+ visited_handling,
+ self.context.shared.quirks_mode(),
+ NeedsSelectorFlags::Yes,
+ MatchingForInvalidation::No,
+ );
+ matching_context.extra_data.originating_element_style = Some(originating_element_style);
+
+ // NB: We handle animation rules for ::before and ::after when
+ // traversing them.
+ stylist.push_applicable_declarations(
+ self.element,
+ Some(pseudo_element),
+ None,
+ None,
+ /* animation_declarations = */ Default::default(),
+ self.rule_inclusion,
+ &mut applicable_declarations,
+ &mut matching_context,
+ );
+
+ if applicable_declarations.is_empty() {
+ return None;
+ }
+
+ let rule_node = stylist
+ .rule_tree()
+ .compute_rule_node(&mut applicable_declarations, &self.context.shared.guards);
+
+ Some(MatchingResults {
+ rule_node,
+ flags: matching_context.extra_data.cascade_input_flags,
+ })
+ }
+}
diff --git a/servo/components/style/stylesheet_set.rs b/servo/components/style/stylesheet_set.rs
new file mode 100644
index 0000000000..b93bbfe2fd
--- /dev/null
+++ b/servo/components/style/stylesheet_set.rs
@@ -0,0 +1,705 @@
+/* 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/. */
+
+//! A centralized set of stylesheets for a document.
+
+use crate::dom::TElement;
+use crate::invalidation::stylesheets::{RuleChangeKind, StylesheetInvalidationSet};
+use crate::media_queries::Device;
+use crate::selector_parser::SnapshotMap;
+use crate::shared_lock::SharedRwLockReadGuard;
+use crate::stylesheets::{
+ CssRule, Origin, OriginSet, OriginSetIterator, PerOrigin, StylesheetInDocument,
+};
+use std::{mem, slice};
+
+/// Entry for a StylesheetSet.
+#[derive(MallocSizeOf)]
+struct StylesheetSetEntry<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The sheet.
+ sheet: S,
+
+ /// Whether this sheet has been part of at least one flush.
+ committed: bool,
+}
+
+impl<S> StylesheetSetEntry<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ fn new(sheet: S) -> Self {
+ Self {
+ sheet,
+ committed: false,
+ }
+ }
+}
+
+/// A iterator over the stylesheets of a list of entries in the StylesheetSet.
+pub struct StylesheetCollectionIterator<'a, S>(slice::Iter<'a, StylesheetSetEntry<S>>)
+where
+ S: StylesheetInDocument + PartialEq + 'static;
+
+impl<'a, S> Clone for StylesheetCollectionIterator<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ fn clone(&self) -> Self {
+ StylesheetCollectionIterator(self.0.clone())
+ }
+}
+
+impl<'a, S> Iterator for StylesheetCollectionIterator<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ type Item = &'a S;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.0.next().map(|entry| &entry.sheet)
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.0.size_hint()
+ }
+}
+
+/// An iterator over the flattened view of the stylesheet collections.
+#[derive(Clone)]
+pub struct StylesheetIterator<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ origins: OriginSetIterator,
+ collections: &'a PerOrigin<SheetCollection<S>>,
+ current: Option<(Origin, StylesheetCollectionIterator<'a, S>)>,
+}
+
+impl<'a, S> Iterator for StylesheetIterator<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ type Item = (&'a S, Origin);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ if self.current.is_none() {
+ let next_origin = self.origins.next()?;
+
+ self.current = Some((
+ next_origin,
+ self.collections.borrow_for_origin(&next_origin).iter(),
+ ));
+ }
+
+ {
+ let (origin, ref mut iter) = *self.current.as_mut().unwrap();
+ if let Some(s) = iter.next() {
+ return Some((s, origin));
+ }
+ }
+
+ self.current = None;
+ }
+ }
+}
+
+/// The validity of the data in a given cascade origin.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)]
+pub enum DataValidity {
+ /// The origin is clean, all the data already there is valid, though we may
+ /// have new sheets at the end.
+ Valid = 0,
+
+ /// The cascade data is invalid, but not the invalidation data (which is
+ /// order-independent), and thus only the cascade data should be inserted.
+ CascadeInvalid = 1,
+
+ /// Everything needs to be rebuilt.
+ FullyInvalid = 2,
+}
+
+impl Default for DataValidity {
+ fn default() -> Self {
+ DataValidity::Valid
+ }
+}
+
+/// A struct to iterate over the different stylesheets to be flushed.
+pub struct DocumentStylesheetFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ collections: &'a mut PerOrigin<SheetCollection<S>>,
+ had_invalidations: bool,
+}
+
+/// The type of rebuild that we need to do for a given stylesheet.
+#[derive(Clone, Copy, Debug)]
+pub enum SheetRebuildKind {
+ /// A full rebuild, of both cascade data and invalidation data.
+ Full,
+ /// A partial rebuild, of only the cascade data.
+ CascadeOnly,
+}
+
+impl SheetRebuildKind {
+ /// Whether the stylesheet invalidation data should be rebuilt.
+ pub fn should_rebuild_invalidation(&self) -> bool {
+ matches!(*self, SheetRebuildKind::Full)
+ }
+}
+
+impl<'a, S> DocumentStylesheetFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Returns a flusher for `origin`.
+ pub fn flush_origin(&mut self, origin: Origin) -> SheetCollectionFlusher<S> {
+ self.collections.borrow_mut_for_origin(&origin).flush()
+ }
+
+ /// Returns the list of stylesheets for `origin`.
+ ///
+ /// Only used for UA sheets.
+ pub fn origin_sheets(&mut self, origin: Origin) -> StylesheetCollectionIterator<S> {
+ self.collections.borrow_mut_for_origin(&origin).iter()
+ }
+
+ /// Returns whether any DOM invalidations were processed as a result of the
+ /// stylesheet flush.
+ #[inline]
+ pub fn had_invalidations(&self) -> bool {
+ self.had_invalidations
+ }
+}
+
+/// A flusher struct for a given collection, that takes care of returning the
+/// appropriate stylesheets that need work.
+pub struct SheetCollectionFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ // TODO: This can be made an iterator again once
+ // https://github.com/rust-lang/rust/pull/82771 lands on stable.
+ entries: &'a mut [StylesheetSetEntry<S>],
+ validity: DataValidity,
+ dirty: bool,
+}
+
+impl<'a, S> SheetCollectionFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Whether the collection was originally dirty.
+ #[inline]
+ pub fn dirty(&self) -> bool {
+ self.dirty
+ }
+
+ /// What the state of the sheet data is.
+ #[inline]
+ pub fn data_validity(&self) -> DataValidity {
+ self.validity
+ }
+
+ /// Returns an iterator over the remaining list of sheets to consume.
+ pub fn sheets<'b>(&'b self) -> impl Iterator<Item = &'b S> {
+ self.entries.iter().map(|entry| &entry.sheet)
+ }
+}
+
+impl<'a, S> SheetCollectionFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Iterates over all sheets and values that we have to invalidate.
+ ///
+ /// TODO(emilio): This would be nicer as an iterator but we can't do that
+ /// until https://github.com/rust-lang/rust/pull/82771 stabilizes.
+ ///
+ /// Since we don't have a good use-case for partial iteration, this does the
+ /// trick for now.
+ pub fn each(self, mut callback: impl FnMut(&S, SheetRebuildKind) -> bool) {
+ for potential_sheet in self.entries.iter_mut() {
+ let committed = mem::replace(&mut potential_sheet.committed, true);
+ let rebuild_kind = if !committed {
+ // If the sheet was uncommitted, we need to do a full rebuild
+ // anyway.
+ SheetRebuildKind::Full
+ } else {
+ match self.validity {
+ DataValidity::Valid => continue,
+ DataValidity::CascadeInvalid => SheetRebuildKind::CascadeOnly,
+ DataValidity::FullyInvalid => SheetRebuildKind::Full,
+ }
+ };
+
+ if !callback(&potential_sheet.sheet, rebuild_kind) {
+ return;
+ }
+ }
+ }
+}
+
+#[derive(MallocSizeOf)]
+struct SheetCollection<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The actual list of stylesheets.
+ ///
+ /// This is only a list of top-level stylesheets, and as such it doesn't
+ /// include recursive `@import` rules.
+ entries: Vec<StylesheetSetEntry<S>>,
+
+ /// The validity of the data that was already there for a given origin.
+ ///
+ /// Note that an origin may appear on `origins_dirty`, but still have
+ /// `DataValidity::Valid`, if only sheets have been appended into it (in
+ /// which case the existing data is valid, but the origin needs to be
+ /// rebuilt).
+ data_validity: DataValidity,
+
+ /// Whether anything in the collection has changed. Note that this is
+ /// different from `data_validity`, in the sense that after a sheet append,
+ /// the data validity is still `Valid`, but we need to be marked as dirty.
+ dirty: bool,
+}
+
+impl<S> Default for SheetCollection<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ fn default() -> Self {
+ Self {
+ entries: vec![],
+ data_validity: DataValidity::Valid,
+ dirty: false,
+ }
+ }
+}
+
+impl<S> SheetCollection<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Returns the number of stylesheets in the set.
+ fn len(&self) -> usize {
+ self.entries.len()
+ }
+
+ /// Returns the `index`th stylesheet in the set if present.
+ fn get(&self, index: usize) -> Option<&S> {
+ self.entries.get(index).map(|e| &e.sheet)
+ }
+
+ fn remove(&mut self, sheet: &S) {
+ let index = self.entries.iter().position(|entry| entry.sheet == *sheet);
+ if cfg!(feature = "gecko") && index.is_none() {
+ // FIXME(emilio): Make Gecko's PresShell::AddUserSheet not suck.
+ return;
+ }
+ let sheet = self.entries.remove(index.unwrap());
+ // Removing sheets makes us tear down the whole cascade and invalidation
+ // data, but only if the sheet has been involved in at least one flush.
+ // Checking whether the sheet has been committed allows us to avoid
+ // rebuilding the world when sites quickly append and remove a
+ // stylesheet.
+ //
+ // See bug 1434756.
+ if sheet.committed {
+ self.set_data_validity_at_least(DataValidity::FullyInvalid);
+ } else {
+ self.dirty = true;
+ }
+ }
+
+ fn contains(&self, sheet: &S) -> bool {
+ self.entries.iter().any(|e| e.sheet == *sheet)
+ }
+
+ /// Appends a given sheet into the collection.
+ fn append(&mut self, sheet: S) {
+ debug_assert!(!self.contains(&sheet));
+ self.entries.push(StylesheetSetEntry::new(sheet));
+ // Appending sheets doesn't alter the validity of the existing data, so
+ // we don't need to change `data_validity` here.
+ //
+ // But we need to be marked as dirty, otherwise we'll never add the new
+ // sheet!
+ self.dirty = true;
+ }
+
+ fn insert_before(&mut self, sheet: S, before_sheet: &S) {
+ debug_assert!(!self.contains(&sheet));
+
+ let index = self
+ .entries
+ .iter()
+ .position(|entry| entry.sheet == *before_sheet)
+ .expect("`before_sheet` stylesheet not found");
+
+ // Inserting stylesheets somewhere but at the end changes the validity
+ // of the cascade data, but not the invalidation data.
+ self.set_data_validity_at_least(DataValidity::CascadeInvalid);
+ self.entries.insert(index, StylesheetSetEntry::new(sheet));
+ }
+
+ fn set_data_validity_at_least(&mut self, validity: DataValidity) {
+ use std::cmp;
+
+ debug_assert_ne!(validity, DataValidity::Valid);
+
+ self.dirty = true;
+ self.data_validity = cmp::max(validity, self.data_validity);
+ }
+
+ /// Returns an iterator over the current list of stylesheets.
+ fn iter(&self) -> StylesheetCollectionIterator<S> {
+ StylesheetCollectionIterator(self.entries.iter())
+ }
+
+ fn flush(&mut self) -> SheetCollectionFlusher<S> {
+ let dirty = mem::replace(&mut self.dirty, false);
+ let validity = mem::replace(&mut self.data_validity, DataValidity::Valid);
+
+ SheetCollectionFlusher {
+ entries: &mut self.entries,
+ dirty,
+ validity,
+ }
+ }
+}
+
+/// The set of stylesheets effective for a given document.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct DocumentStylesheetSet<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The collections of sheets per each origin.
+ collections: PerOrigin<SheetCollection<S>>,
+
+ /// The invalidations for stylesheets added or removed from this document.
+ invalidations: StylesheetInvalidationSet,
+}
+
+/// This macro defines methods common to DocumentStylesheetSet and
+/// AuthorStylesheetSet.
+///
+/// We could simplify the setup moving invalidations to SheetCollection, but
+/// that would imply not sharing invalidations across origins of the same
+/// documents, which is slightly annoying.
+macro_rules! sheet_set_methods {
+ ($set_name:expr) => {
+ fn collect_invalidations_for(
+ &mut self,
+ device: Option<&Device>,
+ sheet: &S,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ if let Some(device) = device {
+ self.invalidations
+ .collect_invalidations_for(device, sheet, guard);
+ }
+ }
+
+ /// Appends a new stylesheet to the current set.
+ ///
+ /// No device implies not computing invalidations.
+ pub fn append_stylesheet(
+ &mut self,
+ device: Option<&Device>,
+ sheet: S,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ debug!(concat!($set_name, "::append_stylesheet"));
+ self.collect_invalidations_for(device, &sheet, guard);
+ let collection = self.collection_for(&sheet);
+ collection.append(sheet);
+ }
+
+ /// Insert a given stylesheet before another stylesheet in the document.
+ pub fn insert_stylesheet_before(
+ &mut self,
+ device: Option<&Device>,
+ sheet: S,
+ before_sheet: S,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ debug!(concat!($set_name, "::insert_stylesheet_before"));
+ self.collect_invalidations_for(device, &sheet, guard);
+
+ let collection = self.collection_for(&sheet);
+ collection.insert_before(sheet, &before_sheet);
+ }
+
+ /// Remove a given stylesheet from the set.
+ pub fn remove_stylesheet(
+ &mut self,
+ device: Option<&Device>,
+ sheet: S,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ debug!(concat!($set_name, "::remove_stylesheet"));
+ self.collect_invalidations_for(device, &sheet, guard);
+
+ let collection = self.collection_for(&sheet);
+ collection.remove(&sheet)
+ }
+
+ /// Notify the set that a rule from a given stylesheet has changed
+ /// somehow.
+ pub fn rule_changed(
+ &mut self,
+ device: Option<&Device>,
+ sheet: &S,
+ rule: &CssRule,
+ guard: &SharedRwLockReadGuard,
+ change_kind: RuleChangeKind,
+ ) {
+ if let Some(device) = device {
+ let quirks_mode = device.quirks_mode();
+ self.invalidations.rule_changed(
+ sheet,
+ rule,
+ guard,
+ device,
+ quirks_mode,
+ change_kind,
+ );
+ }
+
+ let validity = match change_kind {
+ // Insertion / Removals need to rebuild both the cascade and
+ // invalidation data. For generic changes this is conservative,
+ // could be optimized on a per-case basis.
+ RuleChangeKind::Generic | RuleChangeKind::Insertion | RuleChangeKind::Removal => {
+ DataValidity::FullyInvalid
+ },
+ // TODO(emilio): This, in theory, doesn't need to invalidate
+ // style data, if the rule we're modifying is actually in the
+ // CascadeData already.
+ //
+ // But this is actually a bit tricky to prove, because when we
+ // copy-on-write a stylesheet we don't bother doing a rebuild,
+ // so we may still have rules from the original stylesheet
+ // instead of the cloned one that we're modifying. So don't
+ // bother for now and unconditionally rebuild, it's no worse
+ // than what we were already doing anyway.
+ //
+ // Maybe we could record whether we saw a clone in this flush,
+ // and if so do the conservative thing, otherwise just
+ // early-return.
+ RuleChangeKind::StyleRuleDeclarations => DataValidity::FullyInvalid,
+ };
+
+ let collection = self.collection_for(&sheet);
+ collection.set_data_validity_at_least(validity);
+ }
+ };
+}
+
+impl<S> DocumentStylesheetSet<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Create a new empty DocumentStylesheetSet.
+ pub fn new() -> Self {
+ Self {
+ collections: Default::default(),
+ invalidations: StylesheetInvalidationSet::new(),
+ }
+ }
+
+ fn collection_for(&mut self, sheet: &S) -> &mut SheetCollection<S> {
+ let origin = sheet.contents().origin;
+ self.collections.borrow_mut_for_origin(&origin)
+ }
+
+ sheet_set_methods!("DocumentStylesheetSet");
+
+ /// Returns the number of stylesheets in the set.
+ pub fn len(&self) -> usize {
+ self.collections
+ .iter_origins()
+ .fold(0, |s, (item, _)| s + item.len())
+ }
+
+ /// Returns the count of stylesheets for a given origin.
+ #[inline]
+ pub fn sheet_count(&self, origin: Origin) -> usize {
+ self.collections.borrow_for_origin(&origin).len()
+ }
+
+ /// Returns the `index`th stylesheet in the set for the given origin.
+ #[inline]
+ pub fn get(&self, origin: Origin, index: usize) -> Option<&S> {
+ self.collections.borrow_for_origin(&origin).get(index)
+ }
+
+ /// Returns whether the given set has changed from the last flush.
+ pub fn has_changed(&self) -> bool {
+ !self.invalidations.is_empty() ||
+ self.collections
+ .iter_origins()
+ .any(|(collection, _)| collection.dirty)
+ }
+
+ /// Flush the current set, unmarking it as dirty, and returns a
+ /// `DocumentStylesheetFlusher` in order to rebuild the stylist.
+ pub fn flush<E>(
+ &mut self,
+ document_element: Option<E>,
+ snapshots: Option<&SnapshotMap>,
+ ) -> DocumentStylesheetFlusher<S>
+ where
+ E: TElement,
+ {
+ debug!("DocumentStylesheetSet::flush");
+
+ let had_invalidations = self.invalidations.flush(document_element, snapshots);
+
+ DocumentStylesheetFlusher {
+ collections: &mut self.collections,
+ had_invalidations,
+ }
+ }
+
+ /// Flush stylesheets, but without running any of the invalidation passes.
+ #[cfg(feature = "servo")]
+ pub fn flush_without_invalidation(&mut self) -> OriginSet {
+ debug!("DocumentStylesheetSet::flush_without_invalidation");
+
+ let mut origins = OriginSet::empty();
+ self.invalidations.clear();
+
+ for (collection, origin) in self.collections.iter_mut_origins() {
+ if collection.flush().dirty() {
+ origins |= origin;
+ }
+ }
+
+ origins
+ }
+
+ /// Return an iterator over the flattened view of all the stylesheets.
+ pub fn iter(&self) -> StylesheetIterator<S> {
+ StylesheetIterator {
+ origins: OriginSet::all().iter_origins(),
+ collections: &self.collections,
+ current: None,
+ }
+ }
+
+ /// Mark the stylesheets for the specified origin as dirty, because
+ /// something external may have invalidated it.
+ pub fn force_dirty(&mut self, origins: OriginSet) {
+ self.invalidations.invalidate_fully();
+ for origin in origins.iter_origins() {
+ // We don't know what happened, assume the worse.
+ self.collections
+ .borrow_mut_for_origin(&origin)
+ .set_data_validity_at_least(DataValidity::FullyInvalid);
+ }
+ }
+}
+
+/// The set of stylesheets effective for a given Shadow Root.
+#[derive(MallocSizeOf)]
+pub struct AuthorStylesheetSet<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The actual style sheets.
+ collection: SheetCollection<S>,
+ /// The set of invalidations scheduled for this collection.
+ invalidations: StylesheetInvalidationSet,
+}
+
+/// A struct to flush an author style sheet collection.
+pub struct AuthorStylesheetFlusher<'a, S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// The actual flusher for the collection.
+ pub sheets: SheetCollectionFlusher<'a, S>,
+ /// Whether any sheet invalidation matched.
+ pub had_invalidations: bool,
+}
+
+impl<S> AuthorStylesheetSet<S>
+where
+ S: StylesheetInDocument + PartialEq + 'static,
+{
+ /// Create a new empty AuthorStylesheetSet.
+ #[inline]
+ pub fn new() -> Self {
+ Self {
+ collection: Default::default(),
+ invalidations: StylesheetInvalidationSet::new(),
+ }
+ }
+
+ /// Whether anything has changed since the last time this was flushed.
+ pub fn dirty(&self) -> bool {
+ self.collection.dirty
+ }
+
+ /// Whether the collection is empty.
+ pub fn is_empty(&self) -> bool {
+ self.collection.len() == 0
+ }
+
+ /// Returns the `index`th stylesheet in the collection of author styles if present.
+ pub fn get(&self, index: usize) -> Option<&S> {
+ self.collection.get(index)
+ }
+
+ /// Returns the number of author stylesheets.
+ pub fn len(&self) -> usize {
+ self.collection.len()
+ }
+
+ fn collection_for(&mut self, _sheet: &S) -> &mut SheetCollection<S> {
+ &mut self.collection
+ }
+
+ sheet_set_methods!("AuthorStylesheetSet");
+
+ /// Iterate over the list of stylesheets.
+ pub fn iter(&self) -> StylesheetCollectionIterator<S> {
+ self.collection.iter()
+ }
+
+ /// Mark the sheet set dirty, as appropriate.
+ pub fn force_dirty(&mut self) {
+ self.invalidations.invalidate_fully();
+ self.collection
+ .set_data_validity_at_least(DataValidity::FullyInvalid);
+ }
+
+ /// Flush the stylesheets for this author set.
+ ///
+ /// `host` is the root of the affected subtree, like the shadow host, for
+ /// example.
+ pub fn flush<E>(
+ &mut self,
+ host: Option<E>,
+ snapshots: Option<&SnapshotMap>,
+ ) -> AuthorStylesheetFlusher<S>
+ where
+ E: TElement,
+ {
+ let had_invalidations = self.invalidations.flush(host, snapshots);
+ AuthorStylesheetFlusher {
+ sheets: self.collection.flush(),
+ had_invalidations,
+ }
+ }
+}
diff --git a/servo/components/style/stylesheets/container_rule.rs b/servo/components/style/stylesheets/container_rule.rs
new file mode 100644
index 0000000000..28c387fde2
--- /dev/null
+++ b/servo/components/style/stylesheets/container_rule.rs
@@ -0,0 +1,642 @@
+/* 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/. */
+
+//! A [`@container`][container] rule.
+//!
+//! [container]: https://drafts.csswg.org/css-contain-3/#container-rule
+
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::dom::TElement;
+use crate::logical_geometry::{LogicalSize, WritingMode};
+use crate::parser::ParserContext;
+use crate::properties::ComputedValues;
+use crate::queries::condition::KleeneValue;
+use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription};
+use crate::queries::values::Orientation;
+use crate::queries::{FeatureType, QueryCondition};
+use crate::shared_lock::{
+ DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard,
+};
+use crate::str::CssStringWriter;
+use crate::stylesheets::CssRules;
+use crate::stylist::Stylist;
+use crate::values::computed::{CSSPixelLength, ContainerType, Context, Ratio};
+use crate::values::specified::ContainerName;
+use app_units::Au;
+use cssparser::{Parser, SourceLocation};
+use euclid::default::Size2D;
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A container rule.
+#[derive(Debug, ToShmem)]
+pub struct ContainerRule {
+ /// The container query and name.
+ pub condition: Arc<ContainerCondition>,
+ /// The nested rules inside the block.
+ pub rules: Arc<Locked<CssRules>>,
+ /// The source position where this rule was found.
+ pub source_location: SourceLocation,
+}
+
+impl ContainerRule {
+ /// Returns the query condition.
+ pub fn query_condition(&self) -> &QueryCondition {
+ &self.condition.condition
+ }
+
+ /// Returns the query name filter.
+ pub fn container_name(&self) -> &ContainerName {
+ &self.condition.name
+ }
+
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ // Measurement of other fields may be added later.
+ self.rules.unconditional_shallow_size_of(ops) +
+ self.rules.read_with(guard).size_of(guard, ops)
+ }
+}
+
+impl DeepCloneWithLock for ContainerRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ let rules = self.rules.read_with(guard);
+ Self {
+ condition: self.condition.clone(),
+ rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+impl ToCssWithGuard for ContainerRule {
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@container ")?;
+ {
+ let mut writer = CssWriter::new(dest);
+ if !self.condition.name.is_none() {
+ self.condition.name.to_css(&mut writer)?;
+ writer.write_char(' ')?;
+ }
+ self.condition.condition.to_css(&mut writer)?;
+ }
+ self.rules.read_with(guard).to_css_block(guard, dest)
+ }
+}
+
+/// A container condition and filter, combined.
+#[derive(Debug, ToShmem, ToCss)]
+pub struct ContainerCondition {
+ #[css(skip_if = "ContainerName::is_none")]
+ name: ContainerName,
+ condition: QueryCondition,
+ #[css(skip)]
+ flags: FeatureFlags,
+}
+
+/// The result of a successful container query lookup.
+pub struct ContainerLookupResult<E> {
+ /// The relevant container.
+ pub element: E,
+ /// The sizing / writing-mode information of the container.
+ pub info: ContainerInfo,
+ /// The style of the element.
+ pub style: Arc<ComputedValues>,
+}
+
+fn container_type_axes(ty_: ContainerType, wm: WritingMode) -> FeatureFlags {
+ match ty_ {
+ ContainerType::Size => FeatureFlags::all_container_axes(),
+ ContainerType::InlineSize => {
+ let physical_axis = if wm.is_vertical() {
+ FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS
+ } else {
+ FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS
+ };
+ FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS | physical_axis
+ },
+ ContainerType::Normal => FeatureFlags::empty(),
+ }
+}
+
+enum TraversalResult<T> {
+ InProgress,
+ StopTraversal,
+ Done(T),
+}
+
+fn traverse_container<E, F, R>(
+ mut e: E,
+ originating_element_style: Option<&ComputedValues>,
+ evaluator: F,
+) -> Option<(E, R)>
+where
+ E: TElement,
+ F: Fn(E, Option<&ComputedValues>) -> TraversalResult<R>,
+{
+ if originating_element_style.is_some() {
+ match evaluator(e, originating_element_style) {
+ TraversalResult::InProgress => {},
+ TraversalResult::StopTraversal => return None,
+ TraversalResult::Done(result) => return Some((e, result)),
+ }
+ }
+ while let Some(element) = e.traversal_parent() {
+ match evaluator(element, None) {
+ TraversalResult::InProgress => {},
+ TraversalResult::StopTraversal => return None,
+ TraversalResult::Done(result) => return Some((element, result)),
+ }
+ e = element;
+ }
+
+ None
+}
+
+impl ContainerCondition {
+ /// Parse a container condition.
+ pub fn parse<'a>(
+ context: &ParserContext,
+ input: &mut Parser<'a, '_>,
+ ) -> Result<Self, ParseError<'a>> {
+ let name = input
+ .try_parse(|input| ContainerName::parse_for_query(context, input))
+ .ok()
+ .unwrap_or_else(ContainerName::none);
+ let condition = QueryCondition::parse(context, input, FeatureType::Container)?;
+ let flags = condition.cumulative_flags();
+ Ok(Self {
+ name,
+ condition,
+ flags,
+ })
+ }
+
+ fn valid_container_info<E>(
+ &self,
+ potential_container: E,
+ originating_element_style: Option<&ComputedValues>,
+ ) -> TraversalResult<ContainerLookupResult<E>>
+ where
+ E: TElement,
+ {
+ let data;
+ let style = match originating_element_style {
+ Some(s) => s,
+ None => {
+ data = match potential_container.borrow_data() {
+ Some(d) => d,
+ None => return TraversalResult::InProgress,
+ };
+ &**data.styles.primary()
+ },
+ };
+ let wm = style.writing_mode;
+ let box_style = style.get_box();
+
+ // Filter by container-type.
+ let container_type = box_style.clone_container_type();
+ let available_axes = container_type_axes(container_type, wm);
+ if !available_axes.contains(self.flags.container_axes()) {
+ return TraversalResult::InProgress;
+ }
+
+ // Filter by container-name.
+ let container_name = box_style.clone_container_name();
+ for filter_name in self.name.0.iter() {
+ if !container_name.0.contains(filter_name) {
+ return TraversalResult::InProgress;
+ }
+ }
+
+ let size = potential_container.query_container_size(&box_style.clone_display());
+ let style = style.to_arc();
+ TraversalResult::Done(ContainerLookupResult {
+ element: potential_container,
+ info: ContainerInfo { size, wm },
+ style,
+ })
+ }
+
+ /// Performs container lookup for a given element.
+ pub fn find_container<E>(
+ &self,
+ e: E,
+ originating_element_style: Option<&ComputedValues>,
+ ) -> Option<ContainerLookupResult<E>>
+ where
+ E: TElement,
+ {
+ match traverse_container(
+ e,
+ originating_element_style,
+ |element, originating_element_style| {
+ self.valid_container_info(element, originating_element_style)
+ },
+ ) {
+ Some((_, result)) => Some(result),
+ None => None,
+ }
+ }
+
+ /// Tries to match a container query condition for a given element.
+ pub(crate) fn matches<E>(
+ &self,
+ stylist: &Stylist,
+ element: E,
+ originating_element_style: Option<&ComputedValues>,
+ invalidation_flags: &mut ComputedValueFlags,
+ ) -> KleeneValue
+ where
+ E: TElement,
+ {
+ let result = self.find_container(element, originating_element_style);
+ let (container, info) = match result {
+ Some(r) => (Some(r.element), Some((r.info, r.style))),
+ None => (None, None),
+ };
+ // Set up the lookup for the container in question, as the condition may be using container
+ // query lengths.
+ let size_query_container_lookup = ContainerSizeQuery::for_option_element(
+ container, /* known_parent_style = */ None, /* is_pseudo = */ false,
+ );
+ Context::for_container_query_evaluation(
+ stylist.device(),
+ Some(stylist),
+ info,
+ size_query_container_lookup,
+ |context| {
+ let matches = self.condition.matches(context);
+ if context
+ .style()
+ .flags()
+ .contains(ComputedValueFlags::USES_VIEWPORT_UNITS)
+ {
+ // TODO(emilio): Might need something similar to improve
+ // invalidation of font relative container-query lengths.
+ invalidation_flags
+ .insert(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES);
+ }
+ matches
+ },
+ )
+ }
+}
+
+/// Information needed to evaluate an individual container query.
+#[derive(Copy, Clone)]
+pub struct ContainerInfo {
+ size: Size2D<Option<Au>>,
+ wm: WritingMode,
+}
+
+impl ContainerInfo {
+ fn size(&self) -> Option<Size2D<Au>> {
+ Some(Size2D::new(self.size.width?, self.size.height?))
+ }
+}
+
+fn eval_width(context: &Context) -> Option<CSSPixelLength> {
+ let info = context.container_info.as_ref()?;
+ Some(CSSPixelLength::new(info.size.width?.to_f32_px()))
+}
+
+fn eval_height(context: &Context) -> Option<CSSPixelLength> {
+ let info = context.container_info.as_ref()?;
+ Some(CSSPixelLength::new(info.size.height?.to_f32_px()))
+}
+
+fn eval_inline_size(context: &Context) -> Option<CSSPixelLength> {
+ let info = context.container_info.as_ref()?;
+ Some(CSSPixelLength::new(
+ LogicalSize::from_physical(info.wm, info.size)
+ .inline?
+ .to_f32_px(),
+ ))
+}
+
+fn eval_block_size(context: &Context) -> Option<CSSPixelLength> {
+ let info = context.container_info.as_ref()?;
+ Some(CSSPixelLength::new(
+ LogicalSize::from_physical(info.wm, info.size)
+ .block?
+ .to_f32_px(),
+ ))
+}
+
+fn eval_aspect_ratio(context: &Context) -> Option<Ratio> {
+ let info = context.container_info.as_ref()?;
+ Some(Ratio::new(
+ info.size.width?.0 as f32,
+ info.size.height?.0 as f32,
+ ))
+}
+
+fn eval_orientation(context: &Context, value: Option<Orientation>) -> KleeneValue {
+ let size = match context.container_info.as_ref().and_then(|info| info.size()) {
+ Some(size) => size,
+ None => return KleeneValue::Unknown,
+ };
+ KleeneValue::from(Orientation::eval(size, value))
+}
+
+/// https://drafts.csswg.org/css-contain-3/#container-features
+///
+/// TODO: Support style queries, perhaps.
+pub static CONTAINER_FEATURES: [QueryFeatureDescription; 6] = [
+ feature!(
+ atom!("width"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalLength(eval_width),
+ FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS,
+ ),
+ feature!(
+ atom!("height"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalLength(eval_height),
+ FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS,
+ ),
+ feature!(
+ atom!("inline-size"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalLength(eval_inline_size),
+ FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS,
+ ),
+ feature!(
+ atom!("block-size"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalLength(eval_block_size),
+ FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS,
+ ),
+ feature!(
+ atom!("aspect-ratio"),
+ AllowsRanges::Yes,
+ Evaluator::OptionalNumberRatio(eval_aspect_ratio),
+ // XXX from_bits_truncate is const, but the pipe operator isn't, so this
+ // works around it.
+ FeatureFlags::from_bits_truncate(
+ FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() |
+ FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits()
+ ),
+ ),
+ feature!(
+ atom!("orientation"),
+ AllowsRanges::No,
+ keyword_evaluator!(eval_orientation, Orientation),
+ FeatureFlags::from_bits_truncate(
+ FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() |
+ FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits()
+ ),
+ ),
+];
+
+/// Result of a container size query, signifying the hypothetical containment boundary in terms of physical axes.
+/// Defined by up to two size containers. Queries on logical axes are resolved with respect to the querying
+/// element's writing mode.
+#[derive(Copy, Clone, Default)]
+pub struct ContainerSizeQueryResult {
+ width: Option<Au>,
+ height: Option<Au>,
+}
+
+impl ContainerSizeQueryResult {
+ fn get_viewport_size(context: &Context) -> Size2D<Au> {
+ use crate::values::specified::ViewportVariant;
+ context.viewport_size_for_viewport_unit_resolution(ViewportVariant::Small)
+ }
+
+ fn get_logical_viewport_size(context: &Context) -> LogicalSize<Au> {
+ LogicalSize::from_physical(
+ context.builder.writing_mode,
+ Self::get_viewport_size(context),
+ )
+ }
+
+ /// Get the inline-size of the query container.
+ pub fn get_container_inline_size(&self, context: &Context) -> Au {
+ if context.builder.writing_mode.is_horizontal() {
+ if let Some(w) = self.width {
+ return w;
+ }
+ } else {
+ if let Some(h) = self.height {
+ return h;
+ }
+ }
+ Self::get_logical_viewport_size(context).inline
+ }
+
+ /// Get the block-size of the query container.
+ pub fn get_container_block_size(&self, context: &Context) -> Au {
+ if context.builder.writing_mode.is_horizontal() {
+ self.get_container_height(context)
+ } else {
+ self.get_container_width(context)
+ }
+ }
+
+ /// Get the width of the query container.
+ pub fn get_container_width(&self, context: &Context) -> Au {
+ if let Some(w) = self.width {
+ return w;
+ }
+ Self::get_viewport_size(context).width
+ }
+
+ /// Get the height of the query container.
+ pub fn get_container_height(&self, context: &Context) -> Au {
+ if let Some(h) = self.height {
+ return h;
+ }
+ Self::get_viewport_size(context).height
+ }
+
+ // Merge the result of a subsequent lookup, preferring the initial result.
+ fn merge(self, new_result: Self) -> Self {
+ let mut result = self;
+ if let Some(width) = new_result.width {
+ result.width.get_or_insert(width);
+ }
+ if let Some(height) = new_result.height {
+ result.height.get_or_insert(height);
+ }
+ result
+ }
+
+ fn is_complete(&self) -> bool {
+ self.width.is_some() && self.height.is_some()
+ }
+}
+
+/// Unevaluated lazy container size query.
+pub enum ContainerSizeQuery<'a> {
+ /// Query prior to evaluation.
+ NotEvaluated(Box<dyn Fn() -> ContainerSizeQueryResult + 'a>),
+ /// Cached evaluated result.
+ Evaluated(ContainerSizeQueryResult),
+}
+
+impl<'a> ContainerSizeQuery<'a> {
+ fn evaluate_potential_size_container<E>(
+ e: E,
+ originating_element_style: Option<&ComputedValues>,
+ ) -> TraversalResult<ContainerSizeQueryResult>
+ where
+ E: TElement,
+ {
+ let data;
+ let style = match originating_element_style {
+ Some(s) => s,
+ None => {
+ data = match e.borrow_data() {
+ Some(d) => d,
+ None => return TraversalResult::InProgress,
+ };
+ &**data.styles.primary()
+ },
+ };
+ if !style
+ .flags
+ .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE)
+ {
+ // We know we won't find a size container.
+ return TraversalResult::StopTraversal;
+ }
+
+ let wm = style.writing_mode;
+ let box_style = style.get_box();
+
+ let container_type = box_style.clone_container_type();
+ let size = e.query_container_size(&box_style.clone_display());
+ match container_type {
+ ContainerType::Size => TraversalResult::Done(ContainerSizeQueryResult {
+ width: size.width,
+ height: size.height,
+ }),
+ ContainerType::InlineSize => {
+ if wm.is_horizontal() {
+ TraversalResult::Done(ContainerSizeQueryResult {
+ width: size.width,
+ height: None,
+ })
+ } else {
+ TraversalResult::Done(ContainerSizeQueryResult {
+ width: None,
+ height: size.height,
+ })
+ }
+ },
+ ContainerType::Normal => TraversalResult::InProgress,
+ }
+ }
+
+ /// Find the query container size for a given element. Meant to be used as a callback for new().
+ fn lookup<E>(
+ element: E,
+ originating_element_style: Option<&ComputedValues>,
+ ) -> ContainerSizeQueryResult
+ where
+ E: TElement + 'a,
+ {
+ match traverse_container(
+ element,
+ originating_element_style,
+ |e, originating_element_style| {
+ Self::evaluate_potential_size_container(e, originating_element_style)
+ },
+ ) {
+ Some((container, result)) => {
+ if result.is_complete() {
+ result
+ } else {
+ // Traverse up from the found size container to see if we can get a complete containment.
+ result.merge(Self::lookup(container, None))
+ }
+ },
+ None => ContainerSizeQueryResult::default(),
+ }
+ }
+
+ /// Create a new instance of the container size query for given element, with a deferred lookup callback.
+ pub fn for_element<E>(
+ element: E,
+ known_parent_style: Option<&'a ComputedValues>,
+ is_pseudo: bool,
+ ) -> Self
+ where
+ E: TElement + 'a,
+ {
+ let parent;
+ let data;
+ let parent_style = match known_parent_style {
+ Some(s) => Some(s),
+ None => {
+ // No need to bother if we're the top element.
+ parent = match element.traversal_parent() {
+ Some(parent) => parent,
+ None => return Self::none(),
+ };
+ data = parent.borrow_data();
+ data.as_ref().map(|data| &**data.styles.primary())
+ },
+ };
+
+ // If there's no style, such as being `display: none` or so, we still want to show a
+ // correct computed value, so give it a try.
+ let should_traverse = parent_style.map_or(true, |s| {
+ s.flags
+ .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE)
+ });
+ if !should_traverse {
+ return Self::none();
+ }
+ return Self::NotEvaluated(Box::new(move || {
+ Self::lookup(element, if is_pseudo { known_parent_style } else { None })
+ }));
+ }
+
+ /// Create a new instance, but with optional element.
+ pub fn for_option_element<E>(
+ element: Option<E>,
+ known_parent_style: Option<&'a ComputedValues>,
+ is_pseudo: bool,
+ ) -> Self
+ where
+ E: TElement + 'a,
+ {
+ if let Some(e) = element {
+ Self::for_element(e, known_parent_style, is_pseudo)
+ } else {
+ Self::none()
+ }
+ }
+
+ /// Create a query that evaluates to empty, for cases where container size query is not required.
+ pub fn none() -> Self {
+ ContainerSizeQuery::Evaluated(ContainerSizeQueryResult::default())
+ }
+
+ /// Get the result of the container size query, doing the lookup if called for the first time.
+ pub fn get(&mut self) -> ContainerSizeQueryResult {
+ match self {
+ Self::NotEvaluated(lookup) => {
+ *self = Self::Evaluated((lookup)());
+ match self {
+ Self::Evaluated(info) => *info,
+ _ => unreachable!("Just evaluated but not set?"),
+ }
+ },
+ Self::Evaluated(info) => *info,
+ }
+ }
+}
diff --git a/servo/components/style/stylesheets/counter_style_rule.rs b/servo/components/style/stylesheets/counter_style_rule.rs
new file mode 100644
index 0000000000..974b76b806
--- /dev/null
+++ b/servo/components/style/stylesheets/counter_style_rule.rs
@@ -0,0 +1,7 @@
+/* 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/. */
+
+#![allow(missing_docs)]
+
+pub use crate::counter_style::CounterStyleRuleData as CounterStyleRule;
diff --git a/servo/components/style/stylesheets/document_rule.rs b/servo/components/style/stylesheets/document_rule.rs
new file mode 100644
index 0000000000..d043691e4c
--- /dev/null
+++ b/servo/components/style/stylesheets/document_rule.rs
@@ -0,0 +1,299 @@
+/* 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/. */
+
+//! [@document rules](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document)
+//! initially in CSS Conditional Rules Module Level 3, @document has been postponed to the level 4.
+//! We implement the prefixed `@-moz-document`.
+
+use crate::media_queries::Device;
+use crate::parser::{Parse, ParserContext};
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::stylesheets::CssRules;
+use crate::values::CssUrl;
+use cssparser::{BasicParseErrorKind, Parser, SourceLocation};
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+#[derive(Debug, ToShmem)]
+/// A @-moz-document rule
+pub struct DocumentRule {
+ /// The parsed condition
+ pub condition: DocumentCondition,
+ /// Child rules
+ pub rules: Arc<Locked<CssRules>>,
+ /// The line and column of the rule's source code.
+ pub source_location: SourceLocation,
+}
+
+impl DocumentRule {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ // Measurement of other fields may be added later.
+ self.rules.unconditional_shallow_size_of(ops) +
+ self.rules.read_with(guard).size_of(guard, ops)
+ }
+}
+
+impl ToCssWithGuard for DocumentRule {
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@-moz-document ")?;
+ self.condition.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str(" {")?;
+ for rule in self.rules.read_with(guard).0.iter() {
+ dest.write_char(' ')?;
+ rule.to_css(guard, dest)?;
+ }
+ dest.write_str(" }")
+ }
+}
+
+impl DeepCloneWithLock for DocumentRule {
+ /// Deep clones this DocumentRule.
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ let rules = self.rules.read_with(guard);
+ DocumentRule {
+ condition: self.condition.clone(),
+ rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+/// The kind of media document that the rule will match.
+#[derive(Clone, Copy, Debug, Parse, PartialEq, ToCss, ToShmem)]
+#[allow(missing_docs)]
+pub enum MediaDocumentKind {
+ All,
+ Plugin,
+ Image,
+ Video,
+}
+
+/// A matching function for a `@document` rule's condition.
+#[derive(Clone, Debug, ToCss, ToShmem)]
+pub enum DocumentMatchingFunction {
+ /// Exact URL matching function. It evaluates to true whenever the
+ /// URL of the document being styled is exactly the URL given.
+ Url(CssUrl),
+ /// URL prefix matching function. It evaluates to true whenever the
+ /// URL of the document being styled has the argument to the
+ /// function as an initial substring (which is true when the two
+ /// strings are equal). When the argument is the empty string,
+ /// it evaluates to true for all documents.
+ #[css(function)]
+ UrlPrefix(String),
+ /// Domain matching function. It evaluates to true whenever the URL
+ /// of the document being styled has a host subcomponent and that
+ /// host subcomponent is exactly the argument to the ‘domain()’
+ /// function or a final substring of the host component is a
+ /// period (U+002E) immediately followed by the argument to the
+ /// ‘domain()’ function.
+ #[css(function)]
+ Domain(String),
+ /// Regular expression matching function. It evaluates to true
+ /// whenever the regular expression matches the entirety of the URL
+ /// of the document being styled.
+ #[css(function)]
+ Regexp(String),
+ /// Matching function for a media document.
+ #[css(function)]
+ MediaDocument(MediaDocumentKind),
+ /// Matching function for a plain-text document.
+ #[css(function)]
+ PlainTextDocument(()),
+ /// Matching function for a document that can be observed by other content
+ /// documents.
+ #[css(function)]
+ UnobservableDocument(()),
+}
+
+macro_rules! parse_quoted_or_unquoted_string {
+ ($input:ident, $url_matching_function:expr) => {
+ $input.parse_nested_block(|input| {
+ let start = input.position();
+ input
+ .parse_entirely(|input| {
+ let string = input.expect_string()?;
+ Ok($url_matching_function(string.as_ref().to_owned()))
+ })
+ .or_else(|_: ParseError| {
+ while let Ok(_) = input.next() {}
+ Ok($url_matching_function(input.slice_from(start).to_string()))
+ })
+ })
+ };
+}
+
+impl DocumentMatchingFunction {
+ /// Parse a URL matching function for a`@document` rule's condition.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(url) = input.try_parse(|input| CssUrl::parse(context, input)) {
+ return Ok(DocumentMatchingFunction::Url(url));
+ }
+
+ let location = input.current_source_location();
+ let function = input.expect_function()?.clone();
+ match_ignore_ascii_case! { &function,
+ "url-prefix" => {
+ parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::UrlPrefix)
+ },
+ "domain" => {
+ parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::Domain)
+ },
+ "regexp" => {
+ input.parse_nested_block(|input| {
+ Ok(DocumentMatchingFunction::Regexp(
+ input.expect_string()?.as_ref().to_owned(),
+ ))
+ })
+ },
+ "media-document" => {
+ input.parse_nested_block(|input| {
+ let kind = MediaDocumentKind::parse(input)?;
+ Ok(DocumentMatchingFunction::MediaDocument(kind))
+ })
+ },
+
+ "plain-text-document" => {
+ input.parse_nested_block(|input| {
+ input.expect_exhausted()?;
+ Ok(DocumentMatchingFunction::PlainTextDocument(()))
+ })
+ },
+
+ "unobservable-document" => {
+ input.parse_nested_block(|input| {
+ input.expect_exhausted()?;
+ Ok(DocumentMatchingFunction::UnobservableDocument(()))
+ })
+ },
+
+ _ => {
+ Err(location.new_custom_error(
+ StyleParseErrorKind::UnexpectedFunction(function.clone())
+ ))
+ },
+ }
+ }
+
+ #[cfg(feature = "gecko")]
+ /// Evaluate a URL matching function.
+ pub fn evaluate(&self, device: &Device) -> bool {
+ use crate::gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation;
+ use crate::gecko_bindings::structs::DocumentMatchingFunction as GeckoDocumentMatchingFunction;
+ use nsstring::nsCStr;
+
+ let func = match *self {
+ DocumentMatchingFunction::Url(_) => GeckoDocumentMatchingFunction::URL,
+ DocumentMatchingFunction::UrlPrefix(_) => GeckoDocumentMatchingFunction::URLPrefix,
+ DocumentMatchingFunction::Domain(_) => GeckoDocumentMatchingFunction::Domain,
+ DocumentMatchingFunction::Regexp(_) => GeckoDocumentMatchingFunction::RegExp,
+ DocumentMatchingFunction::MediaDocument(_) => {
+ GeckoDocumentMatchingFunction::MediaDocument
+ },
+ DocumentMatchingFunction::PlainTextDocument(..) => {
+ GeckoDocumentMatchingFunction::PlainTextDocument
+ },
+ DocumentMatchingFunction::UnobservableDocument(..) => {
+ GeckoDocumentMatchingFunction::UnobservableDocument
+ },
+ };
+
+ let pattern = nsCStr::from(match *self {
+ DocumentMatchingFunction::Url(ref url) => url.as_str(),
+ DocumentMatchingFunction::UrlPrefix(ref pat) |
+ DocumentMatchingFunction::Domain(ref pat) |
+ DocumentMatchingFunction::Regexp(ref pat) => pat,
+ DocumentMatchingFunction::MediaDocument(kind) => match kind {
+ MediaDocumentKind::All => "all",
+ MediaDocumentKind::Image => "image",
+ MediaDocumentKind::Plugin => "plugin",
+ MediaDocumentKind::Video => "video",
+ },
+ DocumentMatchingFunction::PlainTextDocument(()) |
+ DocumentMatchingFunction::UnobservableDocument(()) => "",
+ });
+ unsafe { Gecko_DocumentRule_UseForPresentation(device.document(), &*pattern, func) }
+ }
+
+ #[cfg(not(feature = "gecko"))]
+ /// Evaluate a URL matching function.
+ pub fn evaluate(&self, _: &Device) -> bool {
+ false
+ }
+}
+
+/// A `@document` rule's condition.
+///
+/// <https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document>
+///
+/// The `@document` rule's condition is written as a comma-separated list of
+/// URL matching functions, and the condition evaluates to true whenever any
+/// one of those functions evaluates to true.
+#[derive(Clone, Debug, ToCss, ToShmem)]
+#[css(comma)]
+pub struct DocumentCondition(#[css(iterable)] Vec<DocumentMatchingFunction>);
+
+impl DocumentCondition {
+ /// Parse a document condition.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let conditions =
+ input.parse_comma_separated(|input| DocumentMatchingFunction::parse(context, input))?;
+
+ let condition = DocumentCondition(conditions);
+ if !condition.allowed_in(context) {
+ return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid("-moz-document".into())));
+ }
+ Ok(condition)
+ }
+
+ /// Evaluate a document condition.
+ pub fn evaluate(&self, device: &Device) -> bool {
+ self.0
+ .iter()
+ .any(|url_matching_function| url_matching_function.evaluate(device))
+ }
+
+ #[cfg(feature = "servo")]
+ fn allowed_in(&self, _: &ParserContext) -> bool {
+ false
+ }
+
+ #[cfg(feature = "gecko")]
+ fn allowed_in(&self, context: &ParserContext) -> bool {
+ if context.chrome_rules_enabled() {
+ return true;
+ }
+
+ // Allow a single url-prefix() for compatibility.
+ //
+ // See bug 1446470 and dependencies.
+ if self.0.len() != 1 {
+ return false;
+ }
+
+ // NOTE(emilio): This technically allows url-prefix("") too, but...
+ match self.0[0] {
+ DocumentMatchingFunction::UrlPrefix(ref prefix) => prefix.is_empty(),
+ _ => false,
+ }
+ }
+}
diff --git a/servo/components/style/stylesheets/font_face_rule.rs b/servo/components/style/stylesheets/font_face_rule.rs
new file mode 100644
index 0000000000..78f3b338b2
--- /dev/null
+++ b/servo/components/style/stylesheets/font_face_rule.rs
@@ -0,0 +1,7 @@
+/* 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/. */
+
+#![allow(missing_docs)]
+
+pub use crate::font_face::FontFaceRuleData as FontFaceRule;
diff --git a/servo/components/style/stylesheets/font_feature_values_rule.rs b/servo/components/style/stylesheets/font_feature_values_rule.rs
new file mode 100644
index 0000000000..06016ec2bd
--- /dev/null
+++ b/servo/components/style/stylesheets/font_feature_values_rule.rs
@@ -0,0 +1,490 @@
+/* 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/. */
+
+//! The [`@font-feature-values`][font-feature-values] at-rule.
+//!
+//! [font-feature-values]: https://drafts.csswg.org/css-fonts-3/#at-font-feature-values-rule
+
+use crate::error_reporting::ContextualParseError;
+#[cfg(feature = "gecko")]
+use crate::gecko_bindings::bindings::Gecko_AppendFeatureValueHashEntry;
+#[cfg(feature = "gecko")]
+use crate::gecko_bindings::structs::{self, gfxFontFeatureValueSet, nsTArray};
+use crate::parser::{Parse, ParserContext};
+use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::stylesheets::CssRuleType;
+use crate::values::computed::font::FamilyName;
+use crate::values::serialize_atom_identifier;
+use crate::Atom;
+use cssparser::{
+ AtRuleParser, BasicParseErrorKind, CowRcStr, DeclarationParser, Parser, ParserState,
+ QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, Token,
+};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// A @font-feature-values block declaration.
+/// It is `<ident>: <integer>+`.
+/// This struct can take 3 value types.
+/// - `SingleValue` is to keep just one unsigned integer value.
+/// - `PairValues` is to keep one or two unsigned integer values.
+/// - `VectorValues` is to keep a list of unsigned integer values.
+#[derive(Clone, Debug, PartialEq, ToShmem)]
+pub struct FFVDeclaration<T> {
+ /// An `<ident>` for declaration name.
+ pub name: Atom,
+ /// An `<integer>+` for declaration value.
+ pub value: T,
+}
+
+impl<T: ToCss> ToCss for FFVDeclaration<T> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.name, dest)?;
+ dest.write_str(": ")?;
+ self.value.to_css(dest)?;
+ dest.write_char(';')
+ }
+}
+
+/// A trait for @font-feature-values rule to gecko values conversion.
+#[cfg(feature = "gecko")]
+pub trait ToGeckoFontFeatureValues {
+ /// Sets the equivalent of declaration to gecko `nsTArray<u32>` array.
+ fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>);
+}
+
+/// A @font-feature-values block declaration value that keeps one value.
+#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)]
+pub struct SingleValue(pub u32);
+
+impl Parse for SingleValue {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<SingleValue, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Number {
+ int_value: Some(v), ..
+ } if v >= 0 => Ok(SingleValue(v as u32)),
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl ToGeckoFontFeatureValues for SingleValue {
+ fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) {
+ unsafe {
+ array.set_len_pod(1);
+ }
+ array[0] = self.0 as u32;
+ }
+}
+
+/// A @font-feature-values block declaration value that keeps one or two values.
+#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)]
+pub struct PairValues(pub u32, pub Option<u32>);
+
+impl Parse for PairValues {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<PairValues, ParseError<'i>> {
+ let location = input.current_source_location();
+ let first = match *input.next()? {
+ Token::Number {
+ int_value: Some(a), ..
+ } if a >= 0 => a as u32,
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ };
+ let location = input.current_source_location();
+ match input.next() {
+ Ok(&Token::Number {
+ int_value: Some(b), ..
+ }) if b >= 0 => Ok(PairValues(first, Some(b as u32))),
+ // It can't be anything other than number.
+ Ok(t) => Err(location.new_unexpected_token_error(t.clone())),
+ // It can be just one value.
+ Err(_) => Ok(PairValues(first, None)),
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl ToGeckoFontFeatureValues for PairValues {
+ fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) {
+ let len = if self.1.is_some() { 2 } else { 1 };
+
+ unsafe {
+ array.set_len_pod(len);
+ }
+ array[0] = self.0 as u32;
+ if let Some(second) = self.1 {
+ array[1] = second as u32;
+ };
+ }
+}
+
+/// A @font-feature-values block declaration value that keeps a list of values.
+#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)]
+pub struct VectorValues(#[css(iterable)] pub Vec<u32>);
+
+impl Parse for VectorValues {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<VectorValues, ParseError<'i>> {
+ let mut vec = vec![];
+ loop {
+ let location = input.current_source_location();
+ match input.next() {
+ Ok(&Token::Number {
+ int_value: Some(a), ..
+ }) if a >= 0 => {
+ vec.push(a as u32);
+ },
+ // It can't be anything other than number.
+ Ok(t) => return Err(location.new_unexpected_token_error(t.clone())),
+ Err(_) => break,
+ }
+ }
+
+ if vec.len() == 0 {
+ return Err(input.new_error(BasicParseErrorKind::EndOfInput));
+ }
+
+ Ok(VectorValues(vec))
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl ToGeckoFontFeatureValues for VectorValues {
+ fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) {
+ array.assign_from_iter_pod(self.0.iter().map(|v| *v));
+ }
+}
+
+/// Parses a list of `FamilyName`s.
+pub fn parse_family_name_list<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<Vec<FamilyName>, ParseError<'i>> {
+ input
+ .parse_comma_separated(|i| FamilyName::parse(context, i))
+ .map_err(|e| e.into())
+}
+
+/// @font-feature-values inside block parser. Parses a list of `FFVDeclaration`.
+/// (`<ident>: <integer>+`)
+struct FFVDeclarationsParser<'a, 'b: 'a, T: 'a> {
+ context: &'a ParserContext<'b>,
+ declarations: &'a mut Vec<FFVDeclaration<T>>,
+}
+
+/// Default methods reject all at rules.
+impl<'a, 'b, 'i, T> AtRuleParser<'i> for FFVDeclarationsParser<'a, 'b, T> {
+ type Prelude = ();
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i, T> QualifiedRuleParser<'i> for FFVDeclarationsParser<'a, 'b, T> {
+ type Prelude = ();
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i, T> DeclarationParser<'i> for FFVDeclarationsParser<'a, 'b, T>
+where
+ T: Parse,
+{
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_value<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ let value = input.parse_entirely(|i| T::parse(self.context, i))?;
+ let new = FFVDeclaration {
+ name: Atom::from(&*name),
+ value,
+ };
+ update_or_push(&mut self.declarations, new);
+ Ok(())
+ }
+}
+
+impl<'a, 'b, 'i, T> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>>
+ for FFVDeclarationsParser<'a, 'b, T>
+where
+ T: Parse,
+{
+ fn parse_declarations(&self) -> bool {
+ true
+ }
+ fn parse_qualified(&self) -> bool {
+ false
+ }
+}
+
+macro_rules! font_feature_values_blocks {
+ (
+ blocks = [
+ $( #[$doc: meta] $name: tt $ident: ident / $ident_camel: ident / $gecko_enum: ident: $ty: ty, )*
+ ]
+ ) => {
+ /// The [`@font-feature-values`][font-feature-values] at-rule.
+ ///
+ /// [font-feature-values]: https://drafts.csswg.org/css-fonts-3/#at-font-feature-values-rule
+ #[derive(Clone, Debug, PartialEq, ToShmem)]
+ pub struct FontFeatureValuesRule {
+ /// Font family list for @font-feature-values rule.
+ /// Family names cannot contain generic families. FamilyName
+ /// also accepts only non-generic names.
+ pub family_names: Vec<FamilyName>,
+ $(
+ #[$doc]
+ pub $ident: Vec<FFVDeclaration<$ty>>,
+ )*
+ /// The line and column of the rule's source code.
+ pub source_location: SourceLocation,
+ }
+
+ impl FontFeatureValuesRule {
+ /// Creates an empty FontFeatureValuesRule with given location and family name list.
+ fn new(family_names: Vec<FamilyName>, location: SourceLocation) -> Self {
+ FontFeatureValuesRule {
+ family_names: family_names,
+ $(
+ $ident: vec![],
+ )*
+ source_location: location,
+ }
+ }
+
+ /// Parses a `FontFeatureValuesRule`.
+ pub fn parse(
+ context: &ParserContext,
+ input: &mut Parser,
+ family_names: Vec<FamilyName>,
+ location: SourceLocation,
+ ) -> Self {
+ let mut rule = FontFeatureValuesRule::new(family_names, location);
+ let mut parser = FontFeatureValuesRuleParser {
+ context,
+ rule: &mut rule,
+ };
+ let mut iter = RuleBodyParser::new(input, &mut parser);
+ while let Some(result) = iter.next() {
+ if let Err((error, slice)) = result {
+ let location = error.location;
+ let error = ContextualParseError::UnsupportedRule(slice, error);
+ context.log_css_error(location, error);
+ }
+ }
+ rule
+ }
+
+ /// Prints inside of `@font-feature-values` block.
+ pub fn value_to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ $(
+ if self.$ident.len() > 0 {
+ dest.write_str(concat!("@", $name, " {\n"))?;
+ let iter = self.$ident.iter();
+ for val in iter {
+ val.to_css(dest)?;
+ dest.write_str("\n")?
+ }
+ dest.write_str("}\n")?
+ }
+ )*
+ Ok(())
+ }
+
+ /// Returns length of all at-rules.
+ pub fn len(&self) -> usize {
+ let mut len = 0;
+ $(
+ len += self.$ident.len();
+ )*
+ len
+ }
+
+ /// Convert to Gecko gfxFontFeatureValueSet.
+ #[cfg(feature = "gecko")]
+ pub fn set_at_rules(&self, dest: *mut gfxFontFeatureValueSet) {
+ for ref family in self.family_names.iter() {
+ let family = family.name.to_ascii_lowercase();
+ $(
+ if self.$ident.len() > 0 {
+ for val in self.$ident.iter() {
+ let array = unsafe {
+ Gecko_AppendFeatureValueHashEntry(
+ dest,
+ family.as_ptr(),
+ structs::$gecko_enum,
+ val.name.as_ptr()
+ )
+ };
+ unsafe {
+ val.value.to_gecko_font_feature_values(&mut *array);
+ }
+ }
+ }
+ )*
+ }
+ }
+ }
+
+ impl ToCssWithGuard for FontFeatureValuesRule {
+ fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@font-feature-values ")?;
+ self.family_names.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str(" {\n")?;
+ self.value_to_css(&mut CssWriter::new(dest))?;
+ dest.write_char('}')
+ }
+ }
+
+ /// Updates with new value if same `ident` exists, otherwise pushes to the vector.
+ fn update_or_push<T>(vec: &mut Vec<FFVDeclaration<T>>, element: FFVDeclaration<T>) {
+ if let Some(item) = vec.iter_mut().find(|item| item.name == element.name) {
+ item.value = element.value;
+ } else {
+ vec.push(element);
+ }
+ }
+
+ /// Keeps the information about block type like @swash, @styleset etc.
+ enum BlockType {
+ $(
+ $ident_camel,
+ )*
+ }
+
+ /// Parser for `FontFeatureValuesRule`. Parses all blocks
+ /// <feature-type> {
+ /// <feature-value-declaration-list>
+ /// }
+ /// <feature-type> = @stylistic | @historical-forms | @styleset |
+ /// @character-variant | @swash | @ornaments | @annotation
+ struct FontFeatureValuesRuleParser<'a> {
+ context: &'a ParserContext<'a>,
+ rule: &'a mut FontFeatureValuesRule,
+ }
+
+ /// Default methods reject all qualified rules.
+ impl<'a, 'i> QualifiedRuleParser<'i> for FontFeatureValuesRuleParser<'a> {
+ type Prelude = ();
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+ }
+
+ impl<'a, 'i> AtRuleParser<'i> for FontFeatureValuesRuleParser<'a> {
+ type Prelude = BlockType;
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_prelude<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<BlockType, ParseError<'i>> {
+ match_ignore_ascii_case! { &*name,
+ $(
+ $name => Ok(BlockType::$ident_camel),
+ )*
+ _ => Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)),
+ }
+ }
+
+ fn parse_block<'t>(
+ &mut self,
+ prelude: BlockType,
+ _: &ParserState,
+ input: &mut Parser<'i, 't>
+ ) -> Result<Self::AtRule, ParseError<'i>> {
+ debug_assert!(self.context.rule_types().contains(CssRuleType::FontFeatureValues));
+ match prelude {
+ $(
+ BlockType::$ident_camel => {
+ let mut parser = FFVDeclarationsParser {
+ context: &self.context,
+ declarations: &mut self.rule.$ident,
+ };
+
+ let mut iter = RuleBodyParser::new(input, &mut parser);
+ while let Some(declaration) = iter.next() {
+ if let Err((error, slice)) = declaration {
+ let location = error.location;
+ let error = ContextualParseError::UnsupportedKeyframePropertyDeclaration(
+ slice, error
+ );
+ self.context.log_css_error(location, error);
+ }
+ }
+ },
+ )*
+ }
+
+ Ok(())
+ }
+ }
+
+ impl<'a, 'i> DeclarationParser<'i> for FontFeatureValuesRuleParser<'a> {
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+ }
+
+ impl<'a, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> for FontFeatureValuesRuleParser<'a> {
+ fn parse_declarations(&self) -> bool { false }
+ fn parse_qualified(&self) -> bool { true }
+ }
+ }
+}
+
+font_feature_values_blocks! {
+ blocks = [
+ #[doc = "A @swash blocksck. \
+ Specifies a feature name that will work with the swash() \
+ functional notation of font-variant-alternates."]
+ "swash" swash / Swash / NS_FONT_VARIANT_ALTERNATES_SWASH: SingleValue,
+
+ #[doc = "A @stylistic block. \
+ Specifies a feature name that will work with the annotation() \
+ functional notation of font-variant-alternates."]
+ "stylistic" stylistic / Stylistic / NS_FONT_VARIANT_ALTERNATES_STYLISTIC: SingleValue,
+
+ #[doc = "A @ornaments block. \
+ Specifies a feature name that will work with the ornaments() ] \
+ functional notation of font-variant-alternates."]
+ "ornaments" ornaments / Ornaments / NS_FONT_VARIANT_ALTERNATES_ORNAMENTS: SingleValue,
+
+ #[doc = "A @annotation block. \
+ Specifies a feature name that will work with the stylistic() \
+ functional notation of font-variant-alternates."]
+ "annotation" annotation / Annotation / NS_FONT_VARIANT_ALTERNATES_ANNOTATION: SingleValue,
+
+ #[doc = "A @character-variant block. \
+ Specifies a feature name that will work with the styleset() \
+ functional notation of font-variant-alternates. The value can be a pair."]
+ "character-variant" character_variant / CharacterVariant / NS_FONT_VARIANT_ALTERNATES_CHARACTER_VARIANT:
+ PairValues,
+
+ #[doc = "A @styleset block. \
+ Specifies a feature name that will work with the character-variant() \
+ functional notation of font-variant-alternates. The value can be a list."]
+ "styleset" styleset / Styleset / NS_FONT_VARIANT_ALTERNATES_STYLESET: VectorValues,
+ ]
+}
diff --git a/servo/components/style/stylesheets/font_palette_values_rule.rs b/servo/components/style/stylesheets/font_palette_values_rule.rs
new file mode 100644
index 0000000000..400d348215
--- /dev/null
+++ b/servo/components/style/stylesheets/font_palette_values_rule.rs
@@ -0,0 +1,264 @@
+/* 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/. */
+
+//! The [`@font-palette-values`][font-palette-values] at-rule.
+//!
+//! [font-palette-values]: https://drafts.csswg.org/css-fonts/#font-palette-values
+
+use crate::error_reporting::ContextualParseError;
+use crate::gecko_bindings::bindings::Gecko_AppendPaletteValueHashEntry;
+use crate::gecko_bindings::bindings::{Gecko_SetFontPaletteBase, Gecko_SetFontPaletteOverride};
+use crate::gecko_bindings::structs::gfx::FontPaletteValueSet;
+use crate::gecko_bindings::structs::gfx::FontPaletteValueSet_PaletteValues_kDark;
+use crate::gecko_bindings::structs::gfx::FontPaletteValueSet_PaletteValues_kLight;
+use crate::parser::{Parse, ParserContext};
+use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::stylesheets::font_feature_values_rule::parse_family_name_list;
+use crate::values::computed::font::FamilyName;
+use crate::values::specified::Color as SpecifiedColor;
+use crate::values::specified::NonNegativeInteger;
+use crate::values::DashedIdent;
+use cssparser::{
+ AtRuleParser, CowRcStr, DeclarationParser, Parser, QualifiedRuleParser, RuleBodyItemParser,
+ RuleBodyParser, SourceLocation,
+};
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Write};
+use style_traits::{Comma, OneOrMoreSeparated};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+#[allow(missing_docs)]
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct FontPaletteOverrideColor {
+ index: NonNegativeInteger,
+ color: SpecifiedColor,
+}
+
+impl Parse for FontPaletteOverrideColor {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<FontPaletteOverrideColor, ParseError<'i>> {
+ let index = NonNegativeInteger::parse(context, input)?;
+ let location = input.current_source_location();
+ let color = SpecifiedColor::parse(context, input)?;
+ // Only absolute colors are accepted here.
+ if let SpecifiedColor::Absolute { .. } = color {
+ Ok(FontPaletteOverrideColor { index, color })
+ } else {
+ Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+impl ToCss for FontPaletteOverrideColor {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.index.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.color.to_css(dest)
+ }
+}
+
+impl OneOrMoreSeparated for FontPaletteOverrideColor {
+ type S = Comma;
+}
+
+impl OneOrMoreSeparated for FamilyName {
+ type S = Comma;
+}
+
+#[allow(missing_docs)]
+#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
+pub enum FontPaletteBase {
+ Light,
+ Dark,
+ Index(NonNegativeInteger),
+}
+
+/// The [`@font-palette-values`][font-palette-values] at-rule.
+///
+/// [font-palette-values]: https://drafts.csswg.org/css-fonts/#font-palette-values
+#[derive(Clone, Debug, PartialEq, ToShmem)]
+pub struct FontPaletteValuesRule {
+ /// Palette name.
+ pub name: DashedIdent,
+ /// Font family list for @font-palette-values rule.
+ /// Family names cannot contain generic families. FamilyName
+ /// also accepts only non-generic names.
+ pub family_names: Vec<FamilyName>,
+ /// The base palette.
+ pub base_palette: Option<FontPaletteBase>,
+ /// The list of override colors.
+ pub override_colors: Vec<FontPaletteOverrideColor>,
+ /// The line and column of the rule's source code.
+ pub source_location: SourceLocation,
+}
+
+impl FontPaletteValuesRule {
+ /// Creates an empty FontPaletteValuesRule with given location and name.
+ fn new(name: DashedIdent, location: SourceLocation) -> Self {
+ FontPaletteValuesRule {
+ name,
+ family_names: vec![],
+ base_palette: None,
+ override_colors: vec![],
+ source_location: location,
+ }
+ }
+
+ /// Parses a `FontPaletteValuesRule`.
+ pub fn parse(
+ context: &ParserContext,
+ input: &mut Parser,
+ name: DashedIdent,
+ location: SourceLocation,
+ ) -> Self {
+ let mut rule = FontPaletteValuesRule::new(name, location);
+ let mut parser = FontPaletteValuesDeclarationParser {
+ context,
+ rule: &mut rule,
+ };
+ let mut iter = RuleBodyParser::new(input, &mut parser);
+ while let Some(declaration) = iter.next() {
+ if let Err((error, slice)) = declaration {
+ let location = error.location;
+ let error =
+ ContextualParseError::UnsupportedFontPaletteValuesDescriptor(slice, error);
+ context.log_css_error(location, error);
+ }
+ }
+ rule
+ }
+
+ /// Prints inside of `@font-palette-values` block.
+ fn value_to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if !self.family_names.is_empty() {
+ dest.write_str("font-family: ")?;
+ self.family_names.to_css(dest)?;
+ dest.write_str("; ")?;
+ }
+ if let Some(base) = &self.base_palette {
+ dest.write_str("base-palette: ")?;
+ base.to_css(dest)?;
+ dest.write_str("; ")?;
+ }
+ if !self.override_colors.is_empty() {
+ dest.write_str("override-colors: ")?;
+ self.override_colors.to_css(dest)?;
+ dest.write_str("; ")?;
+ }
+ Ok(())
+ }
+
+ /// Convert to Gecko FontPaletteValueSet.
+ pub fn to_gecko_palette_value_set(&self, dest: *mut FontPaletteValueSet) {
+ for ref family in self.family_names.iter() {
+ let family = family.name.to_ascii_lowercase();
+ let palette_values = unsafe {
+ Gecko_AppendPaletteValueHashEntry(dest, family.as_ptr(), self.name.0.as_ptr())
+ };
+ if let Some(base_palette) = &self.base_palette {
+ unsafe {
+ Gecko_SetFontPaletteBase(
+ palette_values,
+ match &base_palette {
+ FontPaletteBase::Light => FontPaletteValueSet_PaletteValues_kLight,
+ FontPaletteBase::Dark => FontPaletteValueSet_PaletteValues_kDark,
+ FontPaletteBase::Index(i) => i.0.value() as i32,
+ },
+ );
+ }
+ }
+ for c in &self.override_colors {
+ if let SpecifiedColor::Absolute(ref absolute) = c.color {
+ unsafe {
+ Gecko_SetFontPaletteOverride(
+ palette_values,
+ c.index.0.value(),
+ (&absolute.color) as *const _ as *mut _,
+ );
+ }
+ }
+ }
+ }
+ }
+}
+
+impl ToCssWithGuard for FontPaletteValuesRule {
+ fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@font-palette-values ")?;
+ self.name.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str(" { ")?;
+ self.value_to_css(&mut CssWriter::new(dest))?;
+ dest.write_char('}')
+ }
+}
+
+/// Parser for declarations in `FontPaletteValuesRule`.
+struct FontPaletteValuesDeclarationParser<'a> {
+ context: &'a ParserContext<'a>,
+ rule: &'a mut FontPaletteValuesRule,
+}
+
+impl<'a, 'i> AtRuleParser<'i> for FontPaletteValuesDeclarationParser<'a> {
+ type Prelude = ();
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'i> QualifiedRuleParser<'i> for FontPaletteValuesDeclarationParser<'a> {
+ type Prelude = ();
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+fn parse_override_colors<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<Vec<FontPaletteOverrideColor>, ParseError<'i>> {
+ input.parse_comma_separated(|i| FontPaletteOverrideColor::parse(context, i))
+}
+
+impl<'a, 'b, 'i> DeclarationParser<'i> for FontPaletteValuesDeclarationParser<'a> {
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_value<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ match_ignore_ascii_case! { &*name,
+ "font-family" => {
+ self.rule.family_names = parse_family_name_list(self.context, input)?
+ },
+ "base-palette" => {
+ self.rule.base_palette = Some(input.parse_entirely(|i| FontPaletteBase::parse(self.context, i))?)
+ },
+ "override-colors" => {
+ self.rule.override_colors = parse_override_colors(self.context, input)?
+ },
+ _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))),
+ }
+ Ok(())
+ }
+}
+
+impl<'a, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>>
+ for FontPaletteValuesDeclarationParser<'a>
+{
+ fn parse_declarations(&self) -> bool {
+ true
+ }
+ fn parse_qualified(&self) -> bool {
+ false
+ }
+}
diff --git a/servo/components/style/stylesheets/import_rule.rs b/servo/components/style/stylesheets/import_rule.rs
new file mode 100644
index 0000000000..e96134b436
--- /dev/null
+++ b/servo/components/style/stylesheets/import_rule.rs
@@ -0,0 +1,301 @@
+/* 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/. */
+
+//! The [`@import`][import] at-rule.
+//!
+//! [import]: https://drafts.csswg.org/css-cascade-3/#at-import
+
+use crate::media_queries::MediaList;
+use crate::parser::{Parse, ParserContext};
+use crate::shared_lock::{
+ DeepCloneParams, DeepCloneWithLock, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard,
+};
+use crate::str::CssStringWriter;
+use crate::stylesheets::{
+ layer_rule::LayerName, supports_rule::SupportsCondition, CssRule, CssRuleType,
+ StylesheetInDocument,
+};
+use crate::values::CssUrl;
+use cssparser::{Parser, SourceLocation};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+use to_shmem::{self, SharedMemoryBuilder, ToShmem};
+
+/// A sheet that is held from an import rule.
+#[cfg(feature = "gecko")]
+#[derive(Debug)]
+pub enum ImportSheet {
+ /// A bonafide stylesheet.
+ Sheet(crate::gecko::data::GeckoStyleSheet),
+
+ /// An @import created while parsing off-main-thread, whose Gecko sheet has
+ /// yet to be created and attached.
+ Pending,
+
+ /// An @import created with a false <supports-condition>, so will never be fetched.
+ Refused,
+}
+
+#[cfg(feature = "gecko")]
+impl ImportSheet {
+ /// Creates a new ImportSheet from a GeckoStyleSheet.
+ pub fn new(sheet: crate::gecko::data::GeckoStyleSheet) -> Self {
+ ImportSheet::Sheet(sheet)
+ }
+
+ /// Creates a pending ImportSheet for a load that has not started yet.
+ pub fn new_pending() -> Self {
+ ImportSheet::Pending
+ }
+
+ /// Creates a refused ImportSheet for a load that will not happen.
+ pub fn new_refused() -> Self {
+ ImportSheet::Refused
+ }
+
+ /// Returns a reference to the GeckoStyleSheet in this ImportSheet, if it
+ /// exists.
+ pub fn as_sheet(&self) -> Option<&crate::gecko::data::GeckoStyleSheet> {
+ match *self {
+ ImportSheet::Sheet(ref s) => {
+ debug_assert!(!s.hack_is_null());
+ if s.hack_is_null() {
+ return None;
+ }
+ Some(s)
+ },
+ ImportSheet::Refused | ImportSheet::Pending => None,
+ }
+ }
+
+ /// Returns the media list for this import rule.
+ pub fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
+ self.as_sheet().and_then(|s| s.media(guard))
+ }
+
+ /// Returns the rule list for this import rule.
+ pub fn rules<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a [CssRule] {
+ match self.as_sheet() {
+ Some(s) => s.rules(guard),
+ None => &[],
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl DeepCloneWithLock for ImportSheet {
+ fn deep_clone_with_lock(
+ &self,
+ _lock: &SharedRwLock,
+ _guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ use crate::gecko::data::GeckoStyleSheet;
+ use crate::gecko_bindings::bindings;
+ match *self {
+ ImportSheet::Sheet(ref s) => {
+ let clone = unsafe {
+ bindings::Gecko_StyleSheet_Clone(s.raw() as *const _, params.reference_sheet)
+ };
+ ImportSheet::Sheet(unsafe { GeckoStyleSheet::from_addrefed(clone) })
+ },
+ ImportSheet::Pending => ImportSheet::Pending,
+ ImportSheet::Refused => ImportSheet::Refused,
+ }
+ }
+}
+
+/// A sheet that is held from an import rule.
+#[cfg(feature = "servo")]
+#[derive(Debug)]
+pub struct ImportSheet(pub ::servo_arc::Arc<crate::stylesheets::Stylesheet>);
+
+#[cfg(feature = "servo")]
+impl ImportSheet {
+ /// Returns the media list for this import rule.
+ pub fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
+ self.0.media(guard)
+ }
+
+ /// Returns the rules for this import rule.
+ pub fn rules<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a [CssRule] {
+ self.0.rules()
+ }
+}
+
+#[cfg(feature = "servo")]
+impl DeepCloneWithLock for ImportSheet {
+ fn deep_clone_with_lock(
+ &self,
+ _lock: &SharedRwLock,
+ _guard: &SharedRwLockReadGuard,
+ _params: &DeepCloneParams,
+ ) -> Self {
+ use servo_arc::Arc;
+
+ ImportSheet(Arc::new((&*self.0).clone()))
+ }
+}
+
+/// The layer specified in an import rule (can be none, anonymous, or named).
+#[derive(Debug, Clone)]
+pub enum ImportLayer {
+ /// No layer specified
+ None,
+
+ /// Anonymous layer (`layer`)
+ Anonymous,
+
+ /// Named layer (`layer(name)`)
+ Named(LayerName),
+}
+
+/// The supports condition in an import rule.
+#[derive(Debug, Clone)]
+pub struct ImportSupportsCondition {
+ /// The supports condition.
+ pub condition: SupportsCondition,
+
+ /// If the import is enabled, from the result of the import condition.
+ pub enabled: bool,
+}
+
+impl ToCss for ImportLayer {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ ImportLayer::None => Ok(()),
+ ImportLayer::Anonymous => dest.write_str("layer"),
+ ImportLayer::Named(ref name) => {
+ dest.write_str("layer(")?;
+ name.to_css(dest)?;
+ dest.write_char(')')
+ },
+ }
+ }
+}
+
+/// The [`@import`][import] at-rule.
+///
+/// [import]: https://drafts.csswg.org/css-cascade-3/#at-import
+#[derive(Debug)]
+pub struct ImportRule {
+ /// The `<url>` this `@import` rule is loading.
+ pub url: CssUrl,
+
+ /// The stylesheet is always present. However, in the case of gecko async
+ /// parsing, we don't actually have a Gecko sheet at first, and so the
+ /// ImportSheet just has stub behavior until it appears.
+ pub stylesheet: ImportSheet,
+
+ /// A <supports-condition> for the rule.
+ pub supports: Option<ImportSupportsCondition>,
+
+ /// A `layer()` function name.
+ pub layer: ImportLayer,
+
+ /// The line and column of the rule's source code.
+ pub source_location: SourceLocation,
+}
+
+impl ImportRule {
+ /// Parses the layer() / layer / supports() part of the import header, as per
+ /// https://drafts.csswg.org/css-cascade-5/#at-import:
+ ///
+ /// [ layer | layer(<layer-name>) ]?
+ /// [ supports([ <supports-condition> | <declaration> ]) ]?
+ ///
+ /// We do this here so that the import preloader can look at this without having to parse the
+ /// whole import rule or parse the media query list or what not.
+ pub fn parse_layer_and_supports<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ context: &mut ParserContext,
+ ) -> (ImportLayer, Option<ImportSupportsCondition>) {
+ let layer = if input
+ .try_parse(|input| input.expect_ident_matching("layer"))
+ .is_ok()
+ {
+ ImportLayer::Anonymous
+ } else {
+ input
+ .try_parse(|input| {
+ input.expect_function_matching("layer")?;
+ input
+ .parse_nested_block(|input| LayerName::parse(context, input))
+ .map(|name| ImportLayer::Named(name))
+ })
+ .ok()
+ .unwrap_or(ImportLayer::None)
+ };
+
+ let supports = if !static_prefs::pref!("layout.css.import-supports.enabled") {
+ None
+ } else {
+ input
+ .try_parse(SupportsCondition::parse_for_import)
+ .map(|condition| {
+ let enabled = context
+ .nest_for_rule(CssRuleType::Style, |context| condition.eval(context));
+ ImportSupportsCondition { condition, enabled }
+ })
+ .ok()
+ };
+
+ (layer, supports)
+ }
+}
+
+impl ToShmem for ImportRule {
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ Err(String::from(
+ "ToShmem failed for ImportRule: cannot handle imported style sheets",
+ ))
+ }
+}
+
+impl DeepCloneWithLock for ImportRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ ImportRule {
+ url: self.url.clone(),
+ stylesheet: self.stylesheet.deep_clone_with_lock(lock, guard, params),
+ supports: self.supports.clone(),
+ layer: self.layer.clone(),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+impl ToCssWithGuard for ImportRule {
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@import ")?;
+ self.url.to_css(&mut CssWriter::new(dest))?;
+
+ if !matches!(self.layer, ImportLayer::None) {
+ dest.write_char(' ')?;
+ self.layer.to_css(&mut CssWriter::new(dest))?;
+ }
+
+ if let Some(ref supports) = self.supports {
+ dest.write_str(" supports(")?;
+ supports.condition.to_css(&mut CssWriter::new(dest))?;
+ dest.write_char(')')?;
+ }
+
+ if let Some(media) = self.stylesheet.media(guard) {
+ if !media.is_empty() {
+ dest.write_char(' ')?;
+ media.to_css(&mut CssWriter::new(dest))?;
+ }
+ }
+
+ dest.write_char(';')
+ }
+}
diff --git a/servo/components/style/stylesheets/keyframes_rule.rs b/servo/components/style/stylesheets/keyframes_rule.rs
new file mode 100644
index 0000000000..96e916b553
--- /dev/null
+++ b/servo/components/style/stylesheets/keyframes_rule.rs
@@ -0,0 +1,690 @@
+/* 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/. */
+
+//! Keyframes: https://drafts.csswg.org/css-animations/#keyframes
+
+use crate::error_reporting::ContextualParseError;
+use crate::parser::ParserContext;
+use crate::properties::{
+ longhands::{
+ animation_composition::single_value::SpecifiedValue as SpecifiedComposition,
+ transition_timing_function::single_value::SpecifiedValue as SpecifiedTimingFunction,
+ },
+ Importance, LonghandId, PropertyDeclaration, PropertyDeclarationBlock, PropertyDeclarationId,
+ PropertyDeclarationIdSet, PropertyId, SourcePropertyDeclaration,
+};
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, SharedRwLock, SharedRwLockReadGuard};
+use crate::shared_lock::{Locked, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::stylesheets::rule_parser::VendorPrefix;
+use crate::stylesheets::{CssRuleType, StylesheetContents};
+use crate::values::{serialize_percentage, KeyframesName};
+use cssparser::{
+ parse_one_rule, AtRuleParser, CowRcStr, DeclarationParser, Parser, ParserInput, ParserState,
+ QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, Token,
+};
+use servo_arc::Arc;
+use std::borrow::Cow;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ParsingMode, StyleParseErrorKind, ToCss};
+
+/// A [`@keyframes`][keyframes] rule.
+///
+/// [keyframes]: https://drafts.csswg.org/css-animations/#keyframes
+#[derive(Debug, ToShmem)]
+pub struct KeyframesRule {
+ /// The name of the current animation.
+ pub name: KeyframesName,
+ /// The keyframes specified for this CSS rule.
+ pub keyframes: Vec<Arc<Locked<Keyframe>>>,
+ /// Vendor prefix type the @keyframes has.
+ pub vendor_prefix: Option<VendorPrefix>,
+ /// The line and column of the rule's source code.
+ pub source_location: SourceLocation,
+}
+
+impl ToCssWithGuard for KeyframesRule {
+ // Serialization of KeyframesRule is not specced.
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@keyframes ")?;
+ self.name.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str(" {")?;
+ let iter = self.keyframes.iter();
+ for lock in iter {
+ dest.write_str("\n")?;
+ let keyframe = lock.read_with(&guard);
+ keyframe.to_css(guard, dest)?;
+ }
+ dest.write_str("\n}")
+ }
+}
+
+impl KeyframesRule {
+ /// Returns the index of the last keyframe that matches the given selector.
+ /// If the selector is not valid, or no keyframe is found, returns None.
+ ///
+ /// Related spec:
+ /// <https://drafts.csswg.org/css-animations-1/#interface-csskeyframesrule-findrule>
+ pub fn find_rule(&self, guard: &SharedRwLockReadGuard, selector: &str) -> Option<usize> {
+ let mut input = ParserInput::new(selector);
+ if let Ok(selector) = Parser::new(&mut input).parse_entirely(KeyframeSelector::parse) {
+ for (i, keyframe) in self.keyframes.iter().enumerate().rev() {
+ if keyframe.read_with(guard).selector == selector {
+ return Some(i);
+ }
+ }
+ }
+ None
+ }
+}
+
+impl DeepCloneWithLock for KeyframesRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ KeyframesRule {
+ name: self.name.clone(),
+ keyframes: self
+ .keyframes
+ .iter()
+ .map(|x| {
+ Arc::new(
+ lock.wrap(x.read_with(guard).deep_clone_with_lock(lock, guard, params)),
+ )
+ })
+ .collect(),
+ vendor_prefix: self.vendor_prefix.clone(),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+/// A number from 0 to 1, indicating the percentage of the animation when this
+/// keyframe should run.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)]
+pub struct KeyframePercentage(pub f32);
+
+impl ::std::cmp::Ord for KeyframePercentage {
+ #[inline]
+ fn cmp(&self, other: &Self) -> ::std::cmp::Ordering {
+ // We know we have a number from 0 to 1, so unwrap() here is safe.
+ self.0.partial_cmp(&other.0).unwrap()
+ }
+}
+
+impl ::std::cmp::Eq for KeyframePercentage {}
+
+impl ToCss for KeyframePercentage {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_percentage(self.0, dest)
+ }
+}
+
+impl KeyframePercentage {
+ /// Trivially constructs a new `KeyframePercentage`.
+ #[inline]
+ pub fn new(value: f32) -> KeyframePercentage {
+ debug_assert!(value >= 0. && value <= 1.);
+ KeyframePercentage(value)
+ }
+
+ fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<KeyframePercentage, ParseError<'i>> {
+ let token = input.next()?.clone();
+ match token {
+ Token::Ident(ref identifier) if identifier.as_ref().eq_ignore_ascii_case("from") => {
+ Ok(KeyframePercentage::new(0.))
+ },
+ Token::Ident(ref identifier) if identifier.as_ref().eq_ignore_ascii_case("to") => {
+ Ok(KeyframePercentage::new(1.))
+ },
+ Token::Percentage {
+ unit_value: percentage,
+ ..
+ } if percentage >= 0. && percentage <= 1. => Ok(KeyframePercentage::new(percentage)),
+ _ => Err(input.new_unexpected_token_error(token)),
+ }
+ }
+}
+
+/// A keyframes selector is a list of percentages or from/to symbols, which are
+/// converted at parse time to percentages.
+#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)]
+#[css(comma)]
+pub struct KeyframeSelector(#[css(iterable)] Vec<KeyframePercentage>);
+
+impl KeyframeSelector {
+ /// Return the list of percentages this selector contains.
+ #[inline]
+ pub fn percentages(&self) -> &[KeyframePercentage] {
+ &self.0
+ }
+
+ /// A dummy public function so we can write a unit test for this.
+ pub fn new_for_unit_testing(percentages: Vec<KeyframePercentage>) -> KeyframeSelector {
+ KeyframeSelector(percentages)
+ }
+
+ /// Parse a keyframe selector from CSS input.
+ pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ input
+ .parse_comma_separated(KeyframePercentage::parse)
+ .map(KeyframeSelector)
+ }
+}
+
+/// A keyframe.
+#[derive(Debug, ToShmem)]
+pub struct Keyframe {
+ /// The selector this keyframe was specified from.
+ pub selector: KeyframeSelector,
+
+ /// The declaration block that was declared inside this keyframe.
+ ///
+ /// Note that `!important` rules in keyframes don't apply, but we keep this
+ /// `Arc` just for convenience.
+ pub block: Arc<Locked<PropertyDeclarationBlock>>,
+
+ /// The line and column of the rule's source code.
+ pub source_location: SourceLocation,
+}
+
+impl ToCssWithGuard for Keyframe {
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ self.selector.to_css(&mut CssWriter::new(dest))?;
+ dest.write_str(" { ")?;
+ self.block.read_with(guard).to_css(dest)?;
+ dest.write_str(" }")?;
+ Ok(())
+ }
+}
+
+impl Keyframe {
+ /// Parse a CSS keyframe.
+ pub fn parse<'i>(
+ css: &'i str,
+ parent_stylesheet_contents: &StylesheetContents,
+ lock: &SharedRwLock,
+ ) -> Result<Arc<Locked<Self>>, ParseError<'i>> {
+ let url_data = parent_stylesheet_contents.url_data.read();
+ let namespaces = parent_stylesheet_contents.namespaces.read();
+ let mut context = ParserContext::new(
+ parent_stylesheet_contents.origin,
+ &url_data,
+ Some(CssRuleType::Keyframe),
+ ParsingMode::DEFAULT,
+ parent_stylesheet_contents.quirks_mode,
+ Cow::Borrowed(&*namespaces),
+ None,
+ None,
+ );
+ let mut input = ParserInput::new(css);
+ let mut input = Parser::new(&mut input);
+
+ let mut declarations = SourcePropertyDeclaration::default();
+ let mut rule_parser = KeyframeListParser {
+ context: &mut context,
+ shared_lock: &lock,
+ declarations: &mut declarations,
+ };
+ parse_one_rule(&mut input, &mut rule_parser)
+ }
+}
+
+impl DeepCloneWithLock for Keyframe {
+ /// Deep clones this Keyframe.
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ _params: &DeepCloneParams,
+ ) -> Keyframe {
+ Keyframe {
+ selector: self.selector.clone(),
+ block: Arc::new(lock.wrap(self.block.read_with(guard).clone())),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+/// A keyframes step value. This can be a synthetised keyframes animation, that
+/// is, one autogenerated from the current computed values, or a list of
+/// declarations to apply.
+///
+/// TODO: Find a better name for this?
+#[derive(Clone, Debug, MallocSizeOf)]
+pub enum KeyframesStepValue {
+ /// A step formed by a declaration block specified by the CSS.
+ Declarations {
+ /// The declaration block per se.
+ #[cfg_attr(
+ feature = "gecko",
+ ignore_malloc_size_of = "XXX: Primary ref, measure if DMD says it's worthwhile"
+ )]
+ #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")]
+ block: Arc<Locked<PropertyDeclarationBlock>>,
+ },
+ /// A synthetic step computed from the current computed values at the time
+ /// of the animation.
+ ComputedValues,
+}
+
+/// A single step from a keyframe animation.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct KeyframesStep {
+ /// The percentage of the animation duration when this step starts.
+ pub start_percentage: KeyframePercentage,
+ /// Declarations that will determine the final style during the step, or
+ /// `ComputedValues` if this is an autogenerated step.
+ pub value: KeyframesStepValue,
+ /// Whether an animation-timing-function declaration exists in the list of
+ /// declarations.
+ ///
+ /// This is used to know when to override the keyframe animation style.
+ pub declared_timing_function: bool,
+ /// Whether an animation-composition declaration exists in the list of
+ /// declarations.
+ ///
+ /// This is used to know when to override the keyframe animation style.
+ pub declared_composition: bool,
+}
+
+impl KeyframesStep {
+ #[inline]
+ fn new(
+ start_percentage: KeyframePercentage,
+ value: KeyframesStepValue,
+ guard: &SharedRwLockReadGuard,
+ ) -> Self {
+ let mut declared_timing_function = false;
+ let mut declared_composition = false;
+ if let KeyframesStepValue::Declarations { ref block } = value {
+ for prop_decl in block.read_with(guard).declarations().iter() {
+ match *prop_decl {
+ PropertyDeclaration::AnimationTimingFunction(..) => {
+ declared_timing_function = true;
+ },
+ PropertyDeclaration::AnimationComposition(..) => {
+ declared_composition = true;
+ },
+ _ => continue,
+ }
+ // Don't need to continue the loop if both are found.
+ if declared_timing_function && declared_composition {
+ break;
+ }
+ }
+ }
+
+ KeyframesStep {
+ start_percentage,
+ value,
+ declared_timing_function,
+ declared_composition,
+ }
+ }
+
+ /// Return specified PropertyDeclaration.
+ #[inline]
+ fn get_declared_property<'a>(
+ &'a self,
+ guard: &'a SharedRwLockReadGuard,
+ property: LonghandId,
+ ) -> Option<&'a PropertyDeclaration> {
+ match self.value {
+ KeyframesStepValue::Declarations { ref block } => {
+ let guard = block.read_with(guard);
+ let (declaration, _) = guard
+ .get(PropertyDeclarationId::Longhand(property))
+ .unwrap();
+ match *declaration {
+ PropertyDeclaration::CSSWideKeyword(..) => None,
+ // FIXME: Bug 1710735: Support css variable in @keyframes rule.
+ PropertyDeclaration::WithVariables(..) => None,
+ _ => Some(declaration),
+ }
+ },
+ KeyframesStepValue::ComputedValues => {
+ panic!("Shouldn't happen to set this property in missing keyframes")
+ },
+ }
+ }
+
+ /// Return specified TransitionTimingFunction if this KeyframesSteps has
+ /// 'animation-timing-function'.
+ pub fn get_animation_timing_function(
+ &self,
+ guard: &SharedRwLockReadGuard,
+ ) -> Option<SpecifiedTimingFunction> {
+ if !self.declared_timing_function {
+ return None;
+ }
+
+ self.get_declared_property(guard, LonghandId::AnimationTimingFunction)
+ .map(|decl| {
+ match *decl {
+ PropertyDeclaration::AnimationTimingFunction(ref value) => {
+ // Use the first value
+ value.0[0].clone()
+ },
+ _ => unreachable!("Unexpected PropertyDeclaration"),
+ }
+ })
+ }
+
+ /// Return CompositeOperation if this KeyframesSteps has 'animation-composition'.
+ pub fn get_animation_composition(
+ &self,
+ guard: &SharedRwLockReadGuard,
+ ) -> Option<SpecifiedComposition> {
+ if !self.declared_composition {
+ return None;
+ }
+
+ self.get_declared_property(guard, LonghandId::AnimationComposition)
+ .map(|decl| {
+ match *decl {
+ PropertyDeclaration::AnimationComposition(ref value) => {
+ // Use the first value
+ value.0[0].clone()
+ },
+ _ => unreachable!("Unexpected PropertyDeclaration"),
+ }
+ })
+ }
+}
+
+/// This structure represents a list of animation steps computed from the list
+/// of keyframes, in order.
+///
+/// It only takes into account animable properties.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct KeyframesAnimation {
+ /// The difference steps of the animation.
+ pub steps: Vec<KeyframesStep>,
+ /// The properties that change in this animation.
+ pub properties_changed: PropertyDeclarationIdSet,
+ /// Vendor prefix type the @keyframes has.
+ pub vendor_prefix: Option<VendorPrefix>,
+}
+
+/// Get all the animated properties in a keyframes animation.
+fn get_animated_properties(
+ keyframes: &[Arc<Locked<Keyframe>>],
+ guard: &SharedRwLockReadGuard,
+) -> PropertyDeclarationIdSet {
+ let mut ret = PropertyDeclarationIdSet::default();
+ // NB: declarations are already deduplicated, so we don't have to check for
+ // it here.
+ for keyframe in keyframes {
+ let keyframe = keyframe.read_with(&guard);
+ let block = keyframe.block.read_with(guard);
+ // CSS Animations spec clearly defines that properties with !important
+ // in keyframe rules are invalid and ignored, but it's still ambiguous
+ // whether we should drop the !important properties or retain the
+ // properties when they are set via CSSOM. So we assume there might
+ // be properties with !important in keyframe rules here.
+ // See the spec issue https://github.com/w3c/csswg-drafts/issues/1824
+ for declaration in block.normal_declaration_iter() {
+ let declaration_id = declaration.id();
+
+ if declaration_id == PropertyDeclarationId::Longhand(LonghandId::Display) {
+ continue;
+ }
+
+ if !declaration_id.is_animatable() {
+ continue;
+ }
+
+ ret.insert(declaration_id);
+ }
+ }
+
+ ret
+}
+
+impl KeyframesAnimation {
+ /// Create a keyframes animation from a given list of keyframes.
+ ///
+ /// This will return a keyframe animation with empty steps and
+ /// properties_changed if the list of keyframes is empty, or there are no
+ /// animated properties obtained from the keyframes.
+ ///
+ /// Otherwise, this will compute and sort the steps used for the animation,
+ /// and return the animation object.
+ pub fn from_keyframes(
+ keyframes: &[Arc<Locked<Keyframe>>],
+ vendor_prefix: Option<VendorPrefix>,
+ guard: &SharedRwLockReadGuard,
+ ) -> Self {
+ let mut result = KeyframesAnimation {
+ steps: vec![],
+ properties_changed: PropertyDeclarationIdSet::default(),
+ vendor_prefix,
+ };
+
+ if keyframes.is_empty() {
+ return result;
+ }
+
+ result.properties_changed = get_animated_properties(keyframes, guard);
+ if result.properties_changed.is_empty() {
+ return result;
+ }
+
+ for keyframe in keyframes {
+ let keyframe = keyframe.read_with(&guard);
+ for percentage in keyframe.selector.0.iter() {
+ result.steps.push(KeyframesStep::new(
+ *percentage,
+ KeyframesStepValue::Declarations {
+ block: keyframe.block.clone(),
+ },
+ guard,
+ ));
+ }
+ }
+
+ // Sort by the start percentage, so we can easily find a frame.
+ result.steps.sort_by_key(|step| step.start_percentage);
+
+ // Prepend autogenerated keyframes if appropriate.
+ if result.steps[0].start_percentage.0 != 0. {
+ result.steps.insert(
+ 0,
+ KeyframesStep::new(
+ KeyframePercentage::new(0.),
+ KeyframesStepValue::ComputedValues,
+ guard,
+ ),
+ );
+ }
+
+ if result.steps.last().unwrap().start_percentage.0 != 1. {
+ result.steps.push(KeyframesStep::new(
+ KeyframePercentage::new(1.),
+ KeyframesStepValue::ComputedValues,
+ guard,
+ ));
+ }
+
+ result
+ }
+}
+
+/// Parses a keyframes list, like:
+/// 0%, 50% {
+/// width: 50%;
+/// }
+///
+/// 40%, 60%, 100% {
+/// width: 100%;
+/// }
+struct KeyframeListParser<'a, 'b> {
+ context: &'a mut ParserContext<'b>,
+ shared_lock: &'a SharedRwLock,
+ declarations: &'a mut SourcePropertyDeclaration,
+}
+
+/// Parses a keyframe list from CSS input.
+pub fn parse_keyframe_list<'a>(
+ context: &mut ParserContext<'a>,
+ input: &mut Parser,
+ shared_lock: &SharedRwLock,
+) -> Vec<Arc<Locked<Keyframe>>> {
+ let mut declarations = SourcePropertyDeclaration::default();
+ let mut parser = KeyframeListParser {
+ context,
+ shared_lock,
+ declarations: &mut declarations,
+ };
+ RuleBodyParser::new(input, &mut parser)
+ .filter_map(Result::ok)
+ .collect()
+}
+
+impl<'a, 'b, 'i> AtRuleParser<'i> for KeyframeListParser<'a, 'b> {
+ type Prelude = ();
+ type AtRule = Arc<Locked<Keyframe>>;
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> DeclarationParser<'i> for KeyframeListParser<'a, 'b> {
+ type Declaration = Arc<Locked<Keyframe>>;
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> QualifiedRuleParser<'i> for KeyframeListParser<'a, 'b> {
+ type Prelude = KeyframeSelector;
+ type QualifiedRule = Arc<Locked<Keyframe>>;
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_prelude<'t>(
+ &mut self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self::Prelude, ParseError<'i>> {
+ let start_position = input.position();
+ KeyframeSelector::parse(input).map_err(|e| {
+ let location = e.location;
+ let error = ContextualParseError::InvalidKeyframeRule(
+ input.slice_from(start_position),
+ e.clone(),
+ );
+ self.context.log_css_error(location, error);
+ e
+ })
+ }
+
+ fn parse_block<'t>(
+ &mut self,
+ selector: Self::Prelude,
+ start: &ParserState,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self::QualifiedRule, ParseError<'i>> {
+ let mut block = PropertyDeclarationBlock::new();
+ let declarations = &mut self.declarations;
+ self.context
+ .nest_for_rule(CssRuleType::Keyframe, |context| {
+ let mut parser = KeyframeDeclarationParser {
+ context: &context,
+ declarations,
+ };
+ let mut iter = RuleBodyParser::new(input, &mut parser);
+ while let Some(declaration) = iter.next() {
+ match declaration {
+ Ok(()) => {
+ block.extend(iter.parser.declarations.drain(), Importance::Normal);
+ },
+ Err((error, slice)) => {
+ iter.parser.declarations.clear();
+ let location = error.location;
+ let error =
+ ContextualParseError::UnsupportedKeyframePropertyDeclaration(
+ slice, error,
+ );
+ context.log_css_error(location, error);
+ },
+ }
+ // `parse_important` is not called here, `!important` is not allowed in keyframe blocks.
+ }
+ });
+ Ok(Arc::new(self.shared_lock.wrap(Keyframe {
+ selector,
+ block: Arc::new(self.shared_lock.wrap(block)),
+ source_location: start.source_location(),
+ })))
+ }
+}
+
+impl<'a, 'b, 'i> RuleBodyItemParser<'i, Arc<Locked<Keyframe>>, StyleParseErrorKind<'i>>
+ for KeyframeListParser<'a, 'b>
+{
+ fn parse_qualified(&self) -> bool {
+ true
+ }
+ fn parse_declarations(&self) -> bool {
+ false
+ }
+}
+
+struct KeyframeDeclarationParser<'a, 'b: 'a> {
+ context: &'a ParserContext<'b>,
+ declarations: &'a mut SourcePropertyDeclaration,
+}
+
+/// Default methods reject all at rules.
+impl<'a, 'b, 'i> AtRuleParser<'i> for KeyframeDeclarationParser<'a, 'b> {
+ type Prelude = ();
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> QualifiedRuleParser<'i> for KeyframeDeclarationParser<'a, 'b> {
+ type Prelude = ();
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+}
+
+impl<'a, 'b, 'i> DeclarationParser<'i> for KeyframeDeclarationParser<'a, 'b> {
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_value<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ let id = match PropertyId::parse(&name, self.context) {
+ Ok(id) => id,
+ Err(()) => {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnknownProperty(name)));
+ },
+ };
+
+ // TODO(emilio): Shouldn't this use parse_entirely?
+ PropertyDeclaration::parse_into(self.declarations, id, self.context, input)?;
+
+ // In case there is still unparsed text in the declaration, we should
+ // roll back.
+ input.expect_exhausted()?;
+
+ Ok(())
+ }
+}
+
+impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>>
+ for KeyframeDeclarationParser<'a, 'b>
+{
+ fn parse_qualified(&self) -> bool {
+ false
+ }
+ fn parse_declarations(&self) -> bool {
+ true
+ }
+}
diff --git a/servo/components/style/stylesheets/layer_rule.rs b/servo/components/style/stylesheets/layer_rule.rs
new file mode 100644
index 0000000000..3ebe6bb34f
--- /dev/null
+++ b/servo/components/style/stylesheets/layer_rule.rs
@@ -0,0 +1,228 @@
+/* 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/. */
+
+//! A [`@layer`][layer] rule.
+//!
+//! [layer]: https://drafts.csswg.org/css-cascade-5/#layering
+
+use crate::parser::{Parse, ParserContext};
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
+use crate::values::AtomIdent;
+
+use super::CssRules;
+
+use cssparser::{Parser, SourceLocation, Token};
+use servo_arc::Arc;
+use smallvec::SmallVec;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// The order of a given layer. We use 16 bits so that we can pack LayerOrder
+/// and CascadeLevel in a single 32-bit struct. If we need more bits we can go
+/// back to packing CascadeLevel in a single byte as we did before.
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)]
+pub struct LayerOrder(u16);
+
+impl LayerOrder {
+ /// The order of the root layer.
+ pub const fn root() -> Self {
+ Self(std::u16::MAX - 1)
+ }
+
+ /// The order of the style attribute layer.
+ pub const fn style_attribute() -> Self {
+ Self(std::u16::MAX)
+ }
+
+ /// Returns whether this layer is for the style attribute, which behaves
+ /// differently in terms of !important, see
+ /// https://github.com/w3c/csswg-drafts/issues/6872
+ ///
+ /// (This is a bit silly, mind-you, but it's needed so that revert-layer
+ /// behaves correctly).
+ #[inline]
+ pub fn is_style_attribute_layer(&self) -> bool {
+ *self == Self::style_attribute()
+ }
+
+ /// The first cascade layer order.
+ pub const fn first() -> Self {
+ Self(0)
+ }
+
+ /// Increment the cascade layer order.
+ #[inline]
+ pub fn inc(&mut self) {
+ if self.0 != std::u16::MAX - 1 {
+ self.0 += 1;
+ }
+ }
+}
+
+/// A `<layer-name>`: https://drafts.csswg.org/css-cascade-5/#typedef-layer-name
+#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)]
+pub struct LayerName(pub SmallVec<[AtomIdent; 1]>);
+
+impl LayerName {
+ /// Returns an empty layer name (which isn't a valid final state, so caller
+ /// is responsible to fill up the name before use).
+ pub fn new_empty() -> Self {
+ Self(Default::default())
+ }
+
+ /// Returns a synthesized name for an anonymous layer.
+ pub fn new_anonymous() -> Self {
+ use std::sync::atomic::{AtomicUsize, Ordering};
+ static NEXT_ANONYMOUS_LAYER_NAME: AtomicUsize = AtomicUsize::new(0);
+
+ let mut name = SmallVec::new();
+ let next_id = NEXT_ANONYMOUS_LAYER_NAME.fetch_add(1, Ordering::Relaxed);
+ // The parens don't _technically_ prevent conflicts with authors, as
+ // authors could write escaped parens as part of the identifier, I
+ // think, but highly reduces the possibility.
+ name.push(AtomIdent::from(&*format!("-moz-anon-layer({})", next_id)));
+
+ LayerName(name)
+ }
+
+ /// Returns the names of the layers. That is, for a layer like `foo.bar`,
+ /// it'd return [foo, bar].
+ pub fn layer_names(&self) -> &[AtomIdent] {
+ &self.0
+ }
+}
+
+impl Parse for LayerName {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut result = SmallVec::new();
+ result.push(AtomIdent::from(&**input.expect_ident()?));
+ loop {
+ let next_name = input.try_parse(|input| -> Result<AtomIdent, ParseError<'i>> {
+ match input.next_including_whitespace()? {
+ Token::Delim('.') => {},
+ other => {
+ let t = other.clone();
+ return Err(input.new_unexpected_token_error(t));
+ },
+ }
+
+ let name = match input.next_including_whitespace()? {
+ Token::Ident(ref ident) => ident,
+ other => {
+ let t = other.clone();
+ return Err(input.new_unexpected_token_error(t));
+ },
+ };
+
+ Ok(AtomIdent::from(&**name))
+ });
+
+ match next_name {
+ Ok(name) => result.push(name),
+ Err(..) => break,
+ }
+ }
+ Ok(LayerName(result))
+ }
+}
+
+impl ToCss for LayerName {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let mut first = true;
+ for name in self.0.iter() {
+ if !first {
+ dest.write_char('.')?;
+ }
+ first = false;
+ name.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(Debug, ToShmem)]
+/// A block `@layer <name>? { ... }`
+/// https://drafts.csswg.org/css-cascade-5/#layer-block
+pub struct LayerBlockRule {
+ /// The layer name, or `None` if anonymous.
+ pub name: Option<LayerName>,
+ /// The nested rules.
+ pub rules: Arc<Locked<CssRules>>,
+ /// The source position where this rule was found.
+ pub source_location: SourceLocation,
+}
+
+impl ToCssWithGuard for LayerBlockRule {
+ fn to_css(
+ &self,
+ guard: &SharedRwLockReadGuard,
+ dest: &mut crate::str::CssStringWriter,
+ ) -> fmt::Result {
+ dest.write_str("@layer")?;
+ if let Some(ref name) = self.name {
+ dest.write_char(' ')?;
+ name.to_css(&mut CssWriter::new(dest))?;
+ }
+ self.rules.read_with(guard).to_css_block(guard, dest)
+ }
+}
+
+impl DeepCloneWithLock for LayerBlockRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ Self {
+ name: self.name.clone(),
+ rules: Arc::new(
+ lock.wrap(
+ self.rules
+ .read_with(guard)
+ .deep_clone_with_lock(lock, guard, params),
+ ),
+ ),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+/// A statement `@layer <name>, <name>, <name>;`
+///
+/// https://drafts.csswg.org/css-cascade-5/#layer-empty
+#[derive(Clone, Debug, ToShmem)]
+pub struct LayerStatementRule {
+ /// The list of layers to sort.
+ pub names: Vec<LayerName>,
+ /// The source position where this rule was found.
+ pub source_location: SourceLocation,
+}
+
+impl ToCssWithGuard for LayerStatementRule {
+ fn to_css(
+ &self,
+ _: &SharedRwLockReadGuard,
+ dest: &mut crate::str::CssStringWriter,
+ ) -> fmt::Result {
+ let mut writer = CssWriter::new(dest);
+ writer.write_str("@layer ")?;
+ let mut first = true;
+ for name in &*self.names {
+ if !first {
+ writer.write_str(", ")?;
+ }
+ first = false;
+ name.to_css(&mut writer)?;
+ }
+ writer.write_char(';')
+ }
+}
diff --git a/servo/components/style/stylesheets/loader.rs b/servo/components/style/stylesheets/loader.rs
new file mode 100644
index 0000000000..f987cf9597
--- /dev/null
+++ b/servo/components/style/stylesheets/loader.rs
@@ -0,0 +1,31 @@
+/* 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/. */
+
+//! The stylesheet loader is the abstraction used to trigger network requests
+//! for `@import` rules.
+
+use crate::media_queries::MediaList;
+use crate::parser::ParserContext;
+use crate::shared_lock::{Locked, SharedRwLock};
+use crate::stylesheets::import_rule::{ImportLayer, ImportRule, ImportSupportsCondition};
+use crate::values::CssUrl;
+use cssparser::SourceLocation;
+use servo_arc::Arc;
+
+/// The stylesheet loader is the abstraction used to trigger network requests
+/// for `@import` rules.
+pub trait StylesheetLoader {
+ /// Request a stylesheet after parsing a given `@import` rule, and return
+ /// the constructed `@import` rule.
+ fn request_stylesheet(
+ &self,
+ url: CssUrl,
+ location: SourceLocation,
+ context: &ParserContext,
+ lock: &SharedRwLock,
+ media: Arc<Locked<MediaList>>,
+ supports: Option<ImportSupportsCondition>,
+ layer: ImportLayer,
+ ) -> Arc<Locked<ImportRule>>;
+}
diff --git a/servo/components/style/stylesheets/margin_rule.rs b/servo/components/style/stylesheets/margin_rule.rs
new file mode 100644
index 0000000000..ab46283151
--- /dev/null
+++ b/servo/components/style/stylesheets/margin_rule.rs
@@ -0,0 +1,167 @@
+/* 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/. */
+
+//! A [`@margin`][margin] rule.
+//!
+//! [margin]: https://drafts.csswg.org/css-page-3/#margin-boxes
+
+use crate::properties::PropertyDeclarationBlock;
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use cssparser::SourceLocation;
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+
+macro_rules! margin_rule_types {
+ ($($(#[$($meta:tt)+])* $id:ident => $val:literal,)+) => {
+ /// [`@margin`][margin] rule names.
+ ///
+ /// https://drafts.csswg.org/css-page-3/#margin-at-rules
+ #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+ #[repr(u8)]
+ pub enum MarginRuleType {
+ $($(#[$($meta)+])* $id,)+
+ }
+
+ impl MarginRuleType {
+ #[inline]
+ fn to_str(&self) -> &'static str {
+ match *self {
+ $(MarginRuleType::$id => concat!('@', $val),)+
+ }
+ }
+ /// Matches the rule type for this name. This does not expect a
+ /// leading '@'.
+ pub fn match_name(name: &str) -> Option<Self> {
+ Some(match_ignore_ascii_case! { name,
+ $( $val => MarginRuleType::$id, )+
+ _ => return None,
+ })
+ }
+ }
+ }
+}
+
+margin_rule_types! {
+ /// [`@top-left-corner`][top-left-corner] margin rule
+ ///
+ /// [top-left-corner] https://drafts.csswg.org/css-page-3/#top-left-corner-box-def
+ TopLeftCorner => "top-left-corner",
+ /// [`@top-left`][top-left] margin rule
+ ///
+ /// [top-left] https://drafts.csswg.org/css-page-3/#top-left-box-def
+ TopLeft => "top-left",
+ /// [`@top-center`][top-center] margin rule
+ ///
+ /// [top-center] https://drafts.csswg.org/css-page-3/#top-center-box-def
+ TopCenter => "top-center",
+ /// [`@top-right`][top-right] margin rule
+ ///
+ /// [top-right] https://drafts.csswg.org/css-page-3/#top-right-box-def
+ TopRight => "top-right",
+ /// [`@top-right-corner`][top-right-corner] margin rule
+ ///
+ /// [top-right-corner] https://drafts.csswg.org/css-page-3/#top-right-corner-box-def
+ TopRightCorner => "top-right-corner",
+ /// [`@bottom-left-corner`][bottom-left-corner] margin rule
+ ///
+ /// [bottom-left-corner] https://drafts.csswg.org/css-page-3/#bottom-left-corner-box-def
+ BottomLeftCorner => "bottom-left-corner",
+ /// [`@bottom-left`][bottom-left] margin rule
+ ///
+ /// [bottom-left] https://drafts.csswg.org/css-page-3/#bottom-left-box-def
+ BottomLeft => "bottom-left",
+ /// [`@bottom-center`][bottom-center] margin rule
+ ///
+ /// [bottom-center] https://drafts.csswg.org/css-page-3/#bottom-center-box-def
+ BottomCenter => "bottom-center",
+ /// [`@bottom-right`][bottom-right] margin rule
+ ///
+ /// [bottom-right] https://drafts.csswg.org/css-page-3/#bottom-right-box-def
+ BottomRight => "bottom-right",
+ /// [`@bottom-right-corner`][bottom-right-corner] margin rule
+ ///
+ /// [bottom-right-corner] https://drafts.csswg.org/css-page-3/#bottom-right-corner-box-def
+ BottomRightCorner => "bottom-right-corner",
+ /// [`@left-top`][left-top] margin rule
+ ///
+ /// [left-top] https://drafts.csswg.org/css-page-3/#left-top-box-def
+ LeftTop => "left-top",
+ /// [`@left-middle`][left-middle] margin rule
+ ///
+ /// [left-middle] https://drafts.csswg.org/css-page-3/#left-middle-box-def
+ LeftMiddle => "left-middle",
+ /// [`@left-bottom`][left-bottom] margin rule
+ ///
+ /// [left-bottom] https://drafts.csswg.org/css-page-3/#left-bottom-box-def
+ LeftBottom => "left-bottom",
+ /// [`@right-top`][right-top] margin rule
+ ///
+ /// [right-top] https://drafts.csswg.org/css-page-3/#right-top-box-def
+ RightTop => "right-top",
+ /// [`@right-middle`][right-middle] margin rule
+ ///
+ /// [right-middle] https://drafts.csswg.org/css-page-3/#right-middle-box-def
+ RightMiddle => "right-middle",
+ /// [`@right-bottom`][right-bottom] margin rule
+ ///
+ /// [right-bottom] https://drafts.csswg.org/css-page-3/#right-bottom-box-def
+ RightBottom => "right-bottom",
+}
+
+/// A [`@margin`][margin] rule.
+///
+/// [margin]: https://drafts.csswg.org/css-page-3/#margin-at-rules
+#[derive(Clone, Debug, ToShmem)]
+pub struct MarginRule {
+ /// Type of this margin rule.
+ pub rule_type: MarginRuleType,
+ /// The declaration block this margin rule contains.
+ pub block: Arc<Locked<PropertyDeclarationBlock>>,
+ /// The source position this rule was found at.
+ pub source_location: SourceLocation,
+}
+
+impl MarginRule {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ // Measurement of other fields may be added later.
+ self.block.unconditional_shallow_size_of(ops) +
+ self.block.read_with(guard).size_of(ops)
+ }
+}
+
+impl ToCssWithGuard for MarginRule {
+ /// Serialization of a margin-rule is not specced, this is adapted from how
+ /// page-rules and style-rules are serialized.
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str(self.rule_type.to_str())?;
+ dest.write_str(" { ")?;
+ let declaration_block = self.block.read_with(guard);
+ declaration_block.to_css(dest)?;
+ if !declaration_block.declarations().is_empty() {
+ dest.write_char(' ')?;
+ }
+ dest.write_char('}')
+ }
+}
+
+impl DeepCloneWithLock for MarginRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ _params: &DeepCloneParams,
+ ) -> Self {
+ MarginRule {
+ rule_type: self.rule_type,
+ block: Arc::new(lock.wrap(self.block.read_with(&guard).clone())),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
diff --git a/servo/components/style/stylesheets/media_rule.rs b/servo/components/style/stylesheets/media_rule.rs
new file mode 100644
index 0000000000..cde60a16bf
--- /dev/null
+++ b/servo/components/style/stylesheets/media_rule.rs
@@ -0,0 +1,71 @@
+/* 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/. */
+
+//! An [`@media`][media] rule.
+//!
+//! [media]: https://drafts.csswg.org/css-conditional/#at-ruledef-media
+
+use crate::media_queries::MediaList;
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::stylesheets::CssRules;
+use cssparser::SourceLocation;
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// An [`@media`][media] rule.
+///
+/// [media]: https://drafts.csswg.org/css-conditional/#at-ruledef-media
+#[derive(Debug, ToShmem)]
+pub struct MediaRule {
+ /// The list of media queries used by this media rule.
+ pub media_queries: Arc<Locked<MediaList>>,
+ /// The nested rules to this media rule.
+ pub rules: Arc<Locked<CssRules>>,
+ /// The source position where this media rule was found.
+ pub source_location: SourceLocation,
+}
+
+impl MediaRule {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ // Measurement of other fields may be added later.
+ self.rules.unconditional_shallow_size_of(ops) +
+ self.rules.read_with(guard).size_of(guard, ops)
+ }
+}
+
+impl ToCssWithGuard for MediaRule {
+ // Serialization of MediaRule is not specced.
+ // https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSMediaRule
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@media ")?;
+ self.media_queries
+ .read_with(guard)
+ .to_css(&mut CssWriter::new(dest))?;
+ self.rules.read_with(guard).to_css_block(guard, dest)
+ }
+}
+
+impl DeepCloneWithLock for MediaRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ let media_queries = self.media_queries.read_with(guard);
+ let rules = self.rules.read_with(guard);
+ MediaRule {
+ media_queries: Arc::new(lock.wrap(media_queries.clone())),
+ rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
diff --git a/servo/components/style/stylesheets/mod.rs b/servo/components/style/stylesheets/mod.rs
new file mode 100644
index 0000000000..2bf75565de
--- /dev/null
+++ b/servo/components/style/stylesheets/mod.rs
@@ -0,0 +1,597 @@
+/* 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/. */
+
+//! Style sheets and their CSS rules.
+
+pub mod container_rule;
+mod counter_style_rule;
+mod document_rule;
+mod font_face_rule;
+pub mod font_feature_values_rule;
+pub mod font_palette_values_rule;
+pub mod import_rule;
+pub mod keyframes_rule;
+pub mod layer_rule;
+mod loader;
+mod media_rule;
+mod namespace_rule;
+pub mod origin;
+mod page_rule;
+mod margin_rule;
+mod property_rule;
+mod rule_list;
+mod rule_parser;
+mod rules_iterator;
+mod style_rule;
+mod stylesheet;
+pub mod supports_rule;
+
+#[cfg(feature = "gecko")]
+use crate::gecko_bindings::sugar::refptr::RefCounted;
+#[cfg(feature = "gecko")]
+use crate::gecko_bindings::{bindings, structs};
+use crate::parser::ParserContext;
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use cssparser::{parse_one_rule, Parser, ParserInput};
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use servo_arc::Arc;
+use std::borrow::Cow;
+use std::fmt;
+#[cfg(feature = "gecko")]
+use std::mem::{self, ManuallyDrop};
+use style_traits::ParsingMode;
+#[cfg(feature = "gecko")]
+use to_shmem::{self, SharedMemoryBuilder, ToShmem};
+
+pub use self::container_rule::ContainerRule;
+pub use self::counter_style_rule::CounterStyleRule;
+pub use self::document_rule::DocumentRule;
+pub use self::font_face_rule::FontFaceRule;
+pub use self::font_feature_values_rule::FontFeatureValuesRule;
+pub use self::font_palette_values_rule::FontPaletteValuesRule;
+pub use self::import_rule::ImportRule;
+pub use self::keyframes_rule::KeyframesRule;
+pub use self::layer_rule::{LayerBlockRule, LayerStatementRule};
+pub use self::loader::StylesheetLoader;
+pub use self::margin_rule::{MarginRule, MarginRuleType};
+pub use self::media_rule::MediaRule;
+pub use self::namespace_rule::NamespaceRule;
+pub use self::origin::{Origin, OriginSet, OriginSetIterator, PerOrigin, PerOriginIter};
+pub use self::page_rule::{PagePseudoClassFlags, PageRule, PageSelector, PageSelectors};
+pub use self::property_rule::PropertyRule;
+pub use self::rule_list::{CssRules, CssRulesHelpers};
+pub use self::rule_parser::{InsertRuleContext, State, TopLevelRuleParser};
+pub use self::rules_iterator::{AllRules, EffectiveRules};
+pub use self::rules_iterator::{
+ EffectiveRulesIterator, NestedRuleIterationCondition, RulesIterator,
+};
+pub use self::style_rule::StyleRule;
+pub use self::stylesheet::{AllowImportRules, SanitizationData, SanitizationKind};
+pub use self::stylesheet::{DocumentStyleSheet, Namespaces, Stylesheet};
+pub use self::stylesheet::{StylesheetContents, StylesheetInDocument, UserAgentStylesheets};
+pub use self::supports_rule::SupportsRule;
+
+/// The CORS mode used for a CSS load.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)]
+pub enum CorsMode {
+ /// No CORS mode, so cross-origin loads can be done.
+ None,
+ /// Anonymous CORS request.
+ Anonymous,
+}
+
+/// Extra data that the backend may need to resolve url values.
+///
+/// If the usize's lowest bit is 0, then this is a strong reference to a
+/// structs::URLExtraData object.
+///
+/// Otherwise, shifting the usize's bits the right by one gives the
+/// UserAgentStyleSheetID value corresponding to the style sheet whose
+/// URLExtraData this is, which is stored in URLExtraData_sShared. We don't
+/// hold a strong reference to that object from here, but we rely on that
+/// array's objects being held alive until shutdown.
+///
+/// We use this packed representation rather than an enum so that
+/// `from_ptr_ref` can work.
+#[cfg(feature = "gecko")]
+// Although deriving MallocSizeOf means it always returns 0, that is fine because UrlExtraData
+// objects are reference-counted.
+#[derive(MallocSizeOf, PartialEq)]
+#[repr(C)]
+pub struct UrlExtraData(usize);
+
+/// Extra data that the backend may need to resolve url values.
+#[cfg(not(feature = "gecko"))]
+pub type UrlExtraData = ::servo_url::ServoUrl;
+
+#[cfg(feature = "gecko")]
+impl Clone for UrlExtraData {
+ fn clone(&self) -> UrlExtraData {
+ UrlExtraData::new(self.ptr())
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl Drop for UrlExtraData {
+ fn drop(&mut self) {
+ // No need to release when we have an index into URLExtraData_sShared.
+ if self.0 & 1 == 0 {
+ unsafe {
+ self.as_ref().release();
+ }
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl ToShmem for UrlExtraData {
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ if self.0 & 1 == 0 {
+ let shared_extra_datas = unsafe { &structs::URLExtraData_sShared };
+ let self_ptr = self.as_ref() as *const _ as *mut _;
+ let sheet_id = shared_extra_datas
+ .iter()
+ .position(|r| r.mRawPtr == self_ptr);
+ let sheet_id = match sheet_id {
+ Some(id) => id,
+ None => {
+ return Err(String::from(
+ "ToShmem failed for UrlExtraData: expected sheet's URLExtraData to be in \
+ URLExtraData::sShared",
+ ));
+ },
+ };
+ Ok(ManuallyDrop::new(UrlExtraData((sheet_id << 1) | 1)))
+ } else {
+ Ok(ManuallyDrop::new(UrlExtraData(self.0)))
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl UrlExtraData {
+ /// Create a new UrlExtraData wrapping a pointer to the specified Gecko
+ /// URLExtraData object.
+ pub fn new(ptr: *mut structs::URLExtraData) -> UrlExtraData {
+ unsafe {
+ (*ptr).addref();
+ }
+ UrlExtraData(ptr as usize)
+ }
+
+ /// True if this URL scheme is chrome.
+ #[inline]
+ pub fn chrome_rules_enabled(&self) -> bool {
+ self.as_ref().mChromeRulesEnabled
+ }
+
+ /// Create a reference to this `UrlExtraData` from a reference to pointer.
+ ///
+ /// The pointer must be valid and non null.
+ ///
+ /// This method doesn't touch refcount.
+ #[inline]
+ pub unsafe fn from_ptr_ref(ptr: &*mut structs::URLExtraData) -> &Self {
+ mem::transmute(ptr)
+ }
+
+ /// Returns a pointer to the Gecko URLExtraData object.
+ pub fn ptr(&self) -> *mut structs::URLExtraData {
+ if self.0 & 1 == 0 {
+ self.0 as *mut structs::URLExtraData
+ } else {
+ unsafe {
+ let sheet_id = self.0 >> 1;
+ structs::URLExtraData_sShared[sheet_id].mRawPtr
+ }
+ }
+ }
+
+ fn as_ref(&self) -> &structs::URLExtraData {
+ unsafe { &*(self.ptr() as *const structs::URLExtraData) }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl fmt::Debug for UrlExtraData {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ macro_rules! define_debug_struct {
+ ($struct_name:ident, $gecko_class:ident, $debug_fn:ident) => {
+ struct $struct_name(*mut structs::$gecko_class);
+ impl fmt::Debug for $struct_name {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ use nsstring::nsCString;
+ let mut spec = nsCString::new();
+ unsafe {
+ bindings::$debug_fn(self.0, &mut spec);
+ }
+ spec.fmt(formatter)
+ }
+ }
+ };
+ }
+
+ define_debug_struct!(DebugURI, nsIURI, Gecko_nsIURI_Debug);
+ define_debug_struct!(
+ DebugReferrerInfo,
+ nsIReferrerInfo,
+ Gecko_nsIReferrerInfo_Debug
+ );
+
+ formatter
+ .debug_struct("URLExtraData")
+ .field("chrome_rules_enabled", &self.chrome_rules_enabled())
+ .field("base", &DebugURI(self.as_ref().mBaseURI.raw()))
+ .field(
+ "referrer",
+ &DebugReferrerInfo(self.as_ref().mReferrerInfo.raw()),
+ )
+ .finish()
+ }
+}
+
+// XXX We probably need to figure out whether we should mark Eq here.
+// It is currently marked so because properties::UnparsedValue wants Eq.
+#[cfg(feature = "gecko")]
+impl Eq for UrlExtraData {}
+
+/// A CSS rule.
+///
+/// TODO(emilio): Lots of spec links should be around.
+#[derive(Clone, Debug, ToShmem)]
+#[allow(missing_docs)]
+pub enum CssRule {
+ Style(Arc<Locked<StyleRule>>),
+ // No Charset here, CSSCharsetRule has been removed from CSSOM
+ // https://drafts.csswg.org/cssom/#changes-from-5-december-2013
+ Namespace(Arc<NamespaceRule>),
+ Import(Arc<Locked<ImportRule>>),
+ Media(Arc<MediaRule>),
+ Container(Arc<ContainerRule>),
+ FontFace(Arc<Locked<FontFaceRule>>),
+ FontFeatureValues(Arc<FontFeatureValuesRule>),
+ FontPaletteValues(Arc<FontPaletteValuesRule>),
+ CounterStyle(Arc<Locked<CounterStyleRule>>),
+ Keyframes(Arc<Locked<KeyframesRule>>),
+ Margin(Arc<MarginRule>),
+ Supports(Arc<SupportsRule>),
+ Page(Arc<Locked<PageRule>>),
+ Property(Arc<PropertyRule>),
+ Document(Arc<DocumentRule>),
+ LayerBlock(Arc<LayerBlockRule>),
+ LayerStatement(Arc<LayerStatementRule>),
+}
+
+impl CssRule {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ match *self {
+ // Not all fields are currently fully measured. Extra measurement
+ // may be added later.
+ CssRule::Namespace(_) => 0,
+
+ // We don't need to measure ImportRule::stylesheet because we measure
+ // it on the C++ side in the child list of the ServoStyleSheet.
+ CssRule::Import(_) => 0,
+
+ CssRule::Style(ref lock) => {
+ lock.unconditional_shallow_size_of(ops) + lock.read_with(guard).size_of(guard, ops)
+ },
+ CssRule::Media(ref arc) => {
+ arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops)
+ },
+ CssRule::Container(ref arc) => {
+ arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops)
+ },
+ CssRule::FontFace(_) => 0,
+ CssRule::FontFeatureValues(_) => 0,
+ CssRule::FontPaletteValues(_) => 0,
+ CssRule::CounterStyle(_) => 0,
+ CssRule::Keyframes(_) => 0,
+ CssRule::Margin(ref arc) => {
+ arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops)
+ },
+ CssRule::Supports(ref arc) => {
+ arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops)
+ },
+ CssRule::Page(ref lock) => {
+ lock.unconditional_shallow_size_of(ops) + lock.read_with(guard).size_of(guard, ops)
+ },
+ CssRule::Property(ref rule) => {
+ rule.unconditional_shallow_size_of(ops) + rule.size_of(guard, ops)
+ },
+ CssRule::Document(ref arc) => {
+ arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops)
+ },
+ // TODO(emilio): Add memory reporting for these rules.
+ CssRule::LayerBlock(_) | CssRule::LayerStatement(_) => 0,
+ }
+ }
+}
+
+/// https://drafts.csswg.org/cssom-1/#dom-cssrule-type
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, Eq, FromPrimitive, PartialEq)]
+#[repr(u8)]
+pub enum CssRuleType {
+ // https://drafts.csswg.org/cssom/#the-cssrule-interface
+ Style = 1,
+ // Charset = 2, // Historical
+ Import = 3,
+ Media = 4,
+ FontFace = 5,
+ Page = 6,
+ // https://drafts.csswg.org/css-animations-1/#interface-cssrule-idl
+ Keyframes = 7,
+ Keyframe = 8,
+ // https://drafts.csswg.org/cssom/#the-cssrule-interface
+ Margin = 9,
+ Namespace = 10,
+ // https://drafts.csswg.org/css-counter-styles-3/#extentions-to-cssrule-interface
+ CounterStyle = 11,
+ // https://drafts.csswg.org/css-conditional-3/#extentions-to-cssrule-interface
+ Supports = 12,
+ // https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#extentions-to-cssrule-interface
+ Document = 13,
+ // https://drafts.csswg.org/css-fonts/#om-fontfeaturevalues
+ FontFeatureValues = 14,
+ // After viewport, all rules should return 0 from the API, but we still need
+ // a constant somewhere.
+ LayerBlock = 16,
+ LayerStatement = 17,
+ Container = 18,
+ FontPaletteValues = 19,
+ // 20 is an arbitrary number to use for Property.
+ Property = 20,
+}
+
+impl CssRuleType {
+ /// Returns a bit that identifies this rule type.
+ #[inline]
+ pub const fn bit(self) -> u32 {
+ 1 << self as u32
+ }
+}
+
+/// Set of rule types.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub struct CssRuleTypes(u32);
+
+impl From<CssRuleType> for CssRuleTypes {
+ fn from(ty: CssRuleType) -> Self {
+ Self(ty.bit())
+ }
+}
+
+impl CssRuleTypes {
+ /// Returns whether the rule is in the current set.
+ #[inline]
+ pub fn contains(self, ty: CssRuleType) -> bool {
+ self.0 & ty.bit() != 0
+ }
+
+ /// Returns all the rules specified in the set.
+ #[inline]
+ pub fn bits(self) -> u32 {
+ self.0
+ }
+
+ /// Creates a raw CssRuleTypes bitfield.
+ #[inline]
+ pub fn from_bits(bits: u32) -> Self {
+ Self(bits)
+ }
+
+ /// Returns whether the rule set is empty.
+ #[inline]
+ pub fn is_empty(self) -> bool {
+ self.0 == 0
+ }
+
+ /// Inserts a rule type into the set.
+ #[inline]
+ pub fn insert(&mut self, ty: CssRuleType) {
+ self.0 |= ty.bit()
+ }
+
+ /// Returns whether any of the types intersect.
+ #[inline]
+ pub fn intersects(self, other: Self) -> bool {
+ self.0 & other.0 != 0
+ }
+}
+
+#[allow(missing_docs)]
+pub enum RulesMutateError {
+ Syntax,
+ IndexSize,
+ HierarchyRequest,
+ InvalidState,
+}
+
+impl CssRule {
+ /// Returns the CSSOM rule type of this rule.
+ pub fn rule_type(&self) -> CssRuleType {
+ match *self {
+ CssRule::Style(_) => CssRuleType::Style,
+ CssRule::Import(_) => CssRuleType::Import,
+ CssRule::Media(_) => CssRuleType::Media,
+ CssRule::FontFace(_) => CssRuleType::FontFace,
+ CssRule::FontFeatureValues(_) => CssRuleType::FontFeatureValues,
+ CssRule::FontPaletteValues(_) => CssRuleType::FontPaletteValues,
+ CssRule::CounterStyle(_) => CssRuleType::CounterStyle,
+ CssRule::Keyframes(_) => CssRuleType::Keyframes,
+ CssRule::Margin(_) => CssRuleType::Margin,
+ CssRule::Namespace(_) => CssRuleType::Namespace,
+ CssRule::Supports(_) => CssRuleType::Supports,
+ CssRule::Page(_) => CssRuleType::Page,
+ CssRule::Property(_) => CssRuleType::Property,
+ CssRule::Document(_) => CssRuleType::Document,
+ CssRule::LayerBlock(_) => CssRuleType::LayerBlock,
+ CssRule::LayerStatement(_) => CssRuleType::LayerStatement,
+ CssRule::Container(_) => CssRuleType::Container,
+ }
+ }
+
+ /// Parse a CSS rule.
+ ///
+ /// Returns a parsed CSS rule and the final state of the parser.
+ ///
+ /// Input state is None for a nested rule
+ pub fn parse(
+ css: &str,
+ insert_rule_context: InsertRuleContext,
+ parent_stylesheet_contents: &StylesheetContents,
+ shared_lock: &SharedRwLock,
+ loader: Option<&dyn StylesheetLoader>,
+ allow_import_rules: AllowImportRules,
+ ) -> Result<Self, RulesMutateError> {
+ let url_data = parent_stylesheet_contents.url_data.read();
+ let namespaces = parent_stylesheet_contents.namespaces.read();
+ let mut context = ParserContext::new(
+ parent_stylesheet_contents.origin,
+ &url_data,
+ None,
+ ParsingMode::DEFAULT,
+ parent_stylesheet_contents.quirks_mode,
+ Cow::Borrowed(&*namespaces),
+ None,
+ None,
+ );
+ context.rule_types = insert_rule_context.containing_rule_types;
+
+ let state = if !insert_rule_context.containing_rule_types.is_empty() {
+ State::Body
+ } else if insert_rule_context.index == 0 {
+ State::Start
+ } else {
+ let index = insert_rule_context.index;
+ insert_rule_context.max_rule_state_at_index(index - 1)
+ };
+
+ let mut input = ParserInput::new(css);
+ let mut input = Parser::new(&mut input);
+
+ // nested rules are in the body state
+ let mut rule_parser = TopLevelRuleParser {
+ context,
+ shared_lock: &shared_lock,
+ loader,
+ state,
+ dom_error: None,
+ insert_rule_context: Some(insert_rule_context),
+ allow_import_rules,
+ declaration_parser_state: Default::default(),
+ error_reporting_state: Default::default(),
+ rules: Default::default(),
+ };
+
+ match parse_one_rule(&mut input, &mut rule_parser) {
+ Ok(_) => Ok(rule_parser.rules.pop().unwrap()),
+ Err(_) => Err(rule_parser.dom_error.unwrap_or(RulesMutateError::Syntax)),
+ }
+ }
+}
+
+impl DeepCloneWithLock for CssRule {
+ /// Deep clones this CssRule.
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> CssRule {
+ match *self {
+ CssRule::Namespace(ref arc) => CssRule::Namespace(arc.clone()),
+ CssRule::Import(ref arc) => {
+ let rule = arc
+ .read_with(guard)
+ .deep_clone_with_lock(lock, guard, params);
+ CssRule::Import(Arc::new(lock.wrap(rule)))
+ },
+ CssRule::Style(ref arc) => {
+ let rule = arc.read_with(guard);
+ CssRule::Style(Arc::new(
+ lock.wrap(rule.deep_clone_with_lock(lock, guard, params)),
+ ))
+ },
+ CssRule::Container(ref arc) => {
+ CssRule::Container(Arc::new(arc.deep_clone_with_lock(lock, guard, params)))
+ },
+ CssRule::Media(ref arc) => {
+ CssRule::Media(Arc::new(arc.deep_clone_with_lock(lock, guard, params)))
+ },
+ CssRule::FontFace(ref arc) => {
+ let rule = arc.read_with(guard);
+ CssRule::FontFace(Arc::new(lock.wrap(rule.clone())))
+ },
+ CssRule::FontFeatureValues(ref arc) => CssRule::FontFeatureValues(arc.clone()),
+ CssRule::FontPaletteValues(ref arc) => CssRule::FontPaletteValues(arc.clone()),
+ CssRule::CounterStyle(ref arc) => {
+ let rule = arc.read_with(guard);
+ CssRule::CounterStyle(Arc::new(lock.wrap(rule.clone())))
+ },
+ CssRule::Keyframes(ref arc) => {
+ let rule = arc.read_with(guard);
+ CssRule::Keyframes(Arc::new(
+ lock.wrap(rule.deep_clone_with_lock(lock, guard, params)),
+ ))
+ },
+ CssRule::Margin(ref arc) => {
+ CssRule::Margin(Arc::new(arc.deep_clone_with_lock(lock, guard, params)))
+ },
+ CssRule::Supports(ref arc) => {
+ CssRule::Supports(Arc::new(arc.deep_clone_with_lock(lock, guard, params)))
+ },
+ CssRule::Page(ref arc) => {
+ let rule = arc.read_with(guard);
+ CssRule::Page(Arc::new(
+ lock.wrap(rule.deep_clone_with_lock(lock, guard, params)),
+ ))
+ },
+ CssRule::Property(ref arc) => {
+ // @property rules are immutable, so we don't need any of the `Locked`
+ // shenanigans, actually, and can just share the rule.
+ CssRule::Property(arc.clone())
+ },
+ CssRule::Document(ref arc) => {
+ CssRule::Document(Arc::new(arc.deep_clone_with_lock(lock, guard, params)))
+ },
+ CssRule::LayerStatement(ref arc) => CssRule::LayerStatement(arc.clone()),
+ CssRule::LayerBlock(ref arc) => {
+ CssRule::LayerBlock(Arc::new(arc.deep_clone_with_lock(lock, guard, params)))
+ },
+ }
+ }
+}
+
+impl ToCssWithGuard for CssRule {
+ // https://drafts.csswg.org/cssom/#serialize-a-css-rule
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ match *self {
+ CssRule::Namespace(ref rule) => rule.to_css(guard, dest),
+ CssRule::Import(ref lock) => lock.read_with(guard).to_css(guard, dest),
+ CssRule::Style(ref lock) => lock.read_with(guard).to_css(guard, dest),
+ CssRule::FontFace(ref lock) => lock.read_with(guard).to_css(guard, dest),
+ CssRule::FontFeatureValues(ref rule) => rule.to_css(guard, dest),
+ CssRule::FontPaletteValues(ref rule) => rule.to_css(guard, dest),
+ CssRule::CounterStyle(ref lock) => lock.read_with(guard).to_css(guard, dest),
+ CssRule::Keyframes(ref lock) => lock.read_with(guard).to_css(guard, dest),
+ CssRule::Margin(ref rule) => rule.to_css(guard, dest),
+ CssRule::Media(ref rule) => rule.to_css(guard, dest),
+ CssRule::Supports(ref rule) => rule.to_css(guard, dest),
+ CssRule::Page(ref lock) => lock.read_with(guard).to_css(guard, dest),
+ CssRule::Property(ref rule) => rule.to_css(guard, dest),
+ CssRule::Document(ref rule) => rule.to_css(guard, dest),
+ CssRule::LayerBlock(ref rule) => rule.to_css(guard, dest),
+ CssRule::LayerStatement(ref rule) => rule.to_css(guard, dest),
+ CssRule::Container(ref rule) => rule.to_css(guard, dest),
+ }
+ }
+}
diff --git a/servo/components/style/stylesheets/namespace_rule.rs b/servo/components/style/stylesheets/namespace_rule.rs
new file mode 100644
index 0000000000..ad980b70a8
--- /dev/null
+++ b/servo/components/style/stylesheets/namespace_rule.rs
@@ -0,0 +1,43 @@
+/* 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/. */
+
+//! The `@namespace` at-rule.
+
+use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::{Namespace, Prefix};
+use cssparser::SourceLocation;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A `@namespace` rule.
+#[derive(Clone, Debug, PartialEq, ToShmem)]
+#[allow(missing_docs)]
+pub struct NamespaceRule {
+ /// The namespace prefix, and `None` if it's the default Namespace
+ pub prefix: Option<Prefix>,
+ /// The actual namespace url.
+ pub url: Namespace,
+ /// The source location this rule was found at.
+ pub source_location: SourceLocation,
+}
+
+impl ToCssWithGuard for NamespaceRule {
+ // https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSNamespaceRule
+ fn to_css(
+ &self,
+ _guard: &SharedRwLockReadGuard,
+ dest_str: &mut CssStringWriter,
+ ) -> fmt::Result {
+ let mut dest = CssWriter::new(dest_str);
+ dest.write_str("@namespace ")?;
+ if let Some(ref prefix) = self.prefix {
+ prefix.to_css(&mut dest)?;
+ dest.write_char(' ')?;
+ }
+ dest.write_str("url(")?;
+ self.url.to_string().to_css(&mut dest)?;
+ dest.write_str(");")
+ }
+}
diff --git a/servo/components/style/stylesheets/origin.rs b/servo/components/style/stylesheets/origin.rs
new file mode 100644
index 0000000000..76167f6d5c
--- /dev/null
+++ b/servo/components/style/stylesheets/origin.rs
@@ -0,0 +1,248 @@
+/* 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/. */
+
+//! [CSS cascade origins](https://drafts.csswg.org/css-cascade/#cascading-origins).
+
+use std::marker::PhantomData;
+use std::ops::BitOrAssign;
+
+/// Each style rule has an origin, which determines where it enters the cascade.
+///
+/// <https://drafts.csswg.org/css-cascade/#cascading-origins>
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem, PartialOrd, Ord)]
+#[repr(u8)]
+pub enum Origin {
+ /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user-agent>
+ UserAgent = 0x1,
+
+ /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user>
+ User = 0x2,
+
+ /// <https://drafts.csswg.org/css-cascade/#cascade-origin-author>
+ Author = 0x4,
+}
+
+impl Origin {
+ /// Returns an origin that goes in order for `index`.
+ ///
+ /// This is used for iterating across origins.
+ fn from_index(index: i8) -> Option<Self> {
+ Some(match index {
+ 0 => Origin::Author,
+ 1 => Origin::User,
+ 2 => Origin::UserAgent,
+ _ => return None,
+ })
+ }
+
+ fn to_index(self) -> i8 {
+ match self {
+ Origin::Author => 0,
+ Origin::User => 1,
+ Origin::UserAgent => 2,
+ }
+ }
+
+ /// Returns an iterator from this origin, towards all the less specific
+ /// origins. So for `UserAgent`, it'd iterate through all origins.
+ #[inline]
+ pub fn following_including(self) -> OriginSetIterator {
+ OriginSetIterator {
+ set: OriginSet::ORIGIN_USER | OriginSet::ORIGIN_AUTHOR | OriginSet::ORIGIN_USER_AGENT,
+ cur: self.to_index(),
+ rev: true,
+ }
+ }
+}
+
+/// A set of origins. This is equivalent to Gecko's OriginFlags.
+#[derive(Clone, Copy, PartialEq, MallocSizeOf)]
+pub struct OriginSet(u8);
+bitflags! {
+ impl OriginSet: u8 {
+ /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user-agent>
+ const ORIGIN_USER_AGENT = Origin::UserAgent as u8;
+ /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user>
+ const ORIGIN_USER = Origin::User as u8;
+ /// <https://drafts.csswg.org/css-cascade/#cascade-origin-author>
+ const ORIGIN_AUTHOR = Origin::Author as u8;
+ }
+}
+
+impl OriginSet {
+ /// Returns an iterator over the origins present in this `OriginSet`.
+ ///
+ /// See the `OriginSet` documentation for information about the order
+ /// origins are iterated.
+ pub fn iter_origins(&self) -> OriginSetIterator {
+ OriginSetIterator {
+ set: *self,
+ cur: 0,
+ rev: false,
+ }
+ }
+}
+
+impl From<Origin> for OriginSet {
+ fn from(origin: Origin) -> Self {
+ Self::from_bits_retain(origin as u8)
+ }
+}
+
+impl BitOrAssign<Origin> for OriginSet {
+ fn bitor_assign(&mut self, origin: Origin) {
+ *self |= OriginSet::from(origin);
+ }
+}
+
+/// Iterates over the origins present in an `OriginSet`, in order from
+/// highest priority (author) to lower (user agent).
+#[derive(Clone)]
+pub struct OriginSetIterator {
+ set: OriginSet,
+ cur: i8,
+ rev: bool,
+}
+
+impl Iterator for OriginSetIterator {
+ type Item = Origin;
+
+ fn next(&mut self) -> Option<Origin> {
+ loop {
+ let origin = Origin::from_index(self.cur)?;
+
+ if self.rev {
+ self.cur -= 1;
+ } else {
+ self.cur += 1;
+ }
+
+ if self.set.contains(origin.into()) {
+ return Some(origin);
+ }
+ }
+ }
+}
+
+/// An object that stores a `T` for each origin of the CSS cascade.
+#[derive(Debug, Default, MallocSizeOf)]
+pub struct PerOrigin<T> {
+ /// Data for `Origin::UserAgent`.
+ pub user_agent: T,
+
+ /// Data for `Origin::User`.
+ pub user: T,
+
+ /// Data for `Origin::Author`.
+ pub author: T,
+}
+
+impl<T> PerOrigin<T> {
+ /// Returns a reference to the per-origin data for the specified origin.
+ #[inline]
+ pub fn borrow_for_origin(&self, origin: &Origin) -> &T {
+ match *origin {
+ Origin::UserAgent => &self.user_agent,
+ Origin::User => &self.user,
+ Origin::Author => &self.author,
+ }
+ }
+
+ /// Returns a mutable reference to the per-origin data for the specified
+ /// origin.
+ #[inline]
+ pub fn borrow_mut_for_origin(&mut self, origin: &Origin) -> &mut T {
+ match *origin {
+ Origin::UserAgent => &mut self.user_agent,
+ Origin::User => &mut self.user,
+ Origin::Author => &mut self.author,
+ }
+ }
+
+ /// Iterates over references to per-origin extra style data, from highest
+ /// level (author) to lowest (user agent).
+ pub fn iter_origins(&self) -> PerOriginIter<T> {
+ PerOriginIter {
+ data: &self,
+ cur: 0,
+ rev: false,
+ }
+ }
+
+ /// Iterates over references to per-origin extra style data, from lowest
+ /// level (user agent) to highest (author).
+ pub fn iter_origins_rev(&self) -> PerOriginIter<T> {
+ PerOriginIter {
+ data: &self,
+ cur: 2,
+ rev: true,
+ }
+ }
+
+ /// Iterates over mutable references to per-origin extra style data, from
+ /// highest level (author) to lowest (user agent).
+ pub fn iter_mut_origins(&mut self) -> PerOriginIterMut<T> {
+ PerOriginIterMut {
+ data: self,
+ cur: 0,
+ _marker: PhantomData,
+ }
+ }
+}
+
+/// Iterator over `PerOrigin<T>`, from highest level (author) to lowest
+/// (user agent).
+///
+/// We rely on this specific order for correctly looking up @font-face,
+/// @counter-style and @keyframes rules.
+pub struct PerOriginIter<'a, T: 'a> {
+ data: &'a PerOrigin<T>,
+ cur: i8,
+ rev: bool,
+}
+
+impl<'a, T> Iterator for PerOriginIter<'a, T>
+where
+ T: 'a,
+{
+ type Item = (&'a T, Origin);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let origin = Origin::from_index(self.cur)?;
+
+ self.cur += if self.rev { -1 } else { 1 };
+
+ Some((self.data.borrow_for_origin(&origin), origin))
+ }
+}
+
+/// Like `PerOriginIter<T>`, but iterates over mutable references to the
+/// per-origin data.
+///
+/// We must use unsafe code here since it's not possible for the borrow
+/// checker to know that we are safely returning a different reference
+/// each time from `next()`.
+pub struct PerOriginIterMut<'a, T: 'a> {
+ data: *mut PerOrigin<T>,
+ cur: i8,
+ _marker: PhantomData<&'a mut PerOrigin<T>>,
+}
+
+impl<'a, T> Iterator for PerOriginIterMut<'a, T>
+where
+ T: 'a,
+{
+ type Item = (&'a mut T, Origin);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let origin = Origin::from_index(self.cur)?;
+
+ self.cur += 1;
+
+ Some((
+ unsafe { (*self.data).borrow_mut_for_origin(&origin) },
+ origin,
+ ))
+ }
+}
diff --git a/servo/components/style/stylesheets/page_rule.rs b/servo/components/style/stylesheets/page_rule.rs
new file mode 100644
index 0000000000..a1618309a3
--- /dev/null
+++ b/servo/components/style/stylesheets/page_rule.rs
@@ -0,0 +1,366 @@
+/* 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/. */
+
+//! A [`@page`][page] rule.
+//!
+//! [page]: https://drafts.csswg.org/css2/page.html#page-box
+
+use crate::parser::{Parse, ParserContext};
+use crate::properties::PropertyDeclarationBlock;
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
+use crate::stylesheets::CssRules;
+use crate::str::CssStringWriter;
+use crate::values::{AtomIdent, CustomIdent};
+use cssparser::{Parser, SourceLocation, Token};
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use servo_arc::Arc;
+use smallvec::SmallVec;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+macro_rules! page_pseudo_classes {
+ ($($(#[$($meta:tt)+])* $id:ident => $val:literal,)+) => {
+ /// [`@page`][page] rule pseudo-classes.
+ ///
+ /// https://drafts.csswg.org/css-page-3/#page-selectors
+ #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+ #[repr(u8)]
+ pub enum PagePseudoClass {
+ $($(#[$($meta)+])* $id,)+
+ }
+ impl PagePseudoClass {
+ fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let loc = input.current_source_location();
+ let colon = input.next_including_whitespace()?;
+ if *colon != Token::Colon {
+ return Err(loc.new_unexpected_token_error(colon.clone()));
+ }
+
+ let ident = input.next_including_whitespace()?;
+ if let Token::Ident(s) = ident {
+ return match_ignore_ascii_case! { &**s,
+ $($val => Ok(PagePseudoClass::$id),)+
+ _ => Err(loc.new_unexpected_token_error(Token::Ident(s.clone()))),
+ };
+ }
+ Err(loc.new_unexpected_token_error(ident.clone()))
+ }
+ #[inline]
+ fn to_str(&self) -> &'static str {
+ match *self {
+ $(PagePseudoClass::$id => concat!(':', $val),)+
+ }
+ }
+ }
+ }
+}
+
+page_pseudo_classes! {
+ /// [`:first`][first] pseudo-class
+ ///
+ /// [first] https://drafts.csswg.org/css-page-3/#first-pseudo
+ First => "first",
+ /// [`:blank`][blank] pseudo-class
+ ///
+ /// [blank] https://drafts.csswg.org/css-page-3/#blank-pseudo
+ Blank => "blank",
+ /// [`:left`][left] pseudo-class
+ ///
+ /// [left]: https://drafts.csswg.org/css-page-3/#spread-pseudos
+ Left => "left",
+ /// [`:right`][right] pseudo-class
+ ///
+ /// [right]: https://drafts.csswg.org/css-page-3/#spread-pseudos
+ Right => "right",
+}
+
+bitflags! {
+ /// Bit-flags for pseudo-class. This should only be used for querying if a
+ /// page-rule applies.
+ ///
+ /// https://drafts.csswg.org/css-page-3/#page-selectors
+ #[derive(Clone, Copy)]
+ #[repr(C)]
+ pub struct PagePseudoClassFlags : u8 {
+ /// No pseudo-classes
+ const NONE = 0;
+ /// Flag for PagePseudoClass::First
+ const FIRST = 1 << 0;
+ /// Flag for PagePseudoClass::Blank
+ const BLANK = 1 << 1;
+ /// Flag for PagePseudoClass::Left
+ const LEFT = 1 << 2;
+ /// Flag for PagePseudoClass::Right
+ const RIGHT = 1 << 3;
+ }
+}
+
+impl PagePseudoClassFlags {
+ /// Creates a pseudo-class flags object with a single pseudo-class.
+ #[inline]
+ pub fn new(other: &PagePseudoClass) -> Self {
+ match *other {
+ PagePseudoClass::First => PagePseudoClassFlags::FIRST,
+ PagePseudoClass::Blank => PagePseudoClassFlags::BLANK,
+ PagePseudoClass::Left => PagePseudoClassFlags::LEFT,
+ PagePseudoClass::Right => PagePseudoClassFlags::RIGHT,
+ }
+ }
+ /// Checks if the given pseudo class applies to this set of flags.
+ #[inline]
+ pub fn contains_class(self, other: &PagePseudoClass) -> bool {
+ self.intersects(PagePseudoClassFlags::new(other))
+ }
+}
+
+type PagePseudoClasses = SmallVec<[PagePseudoClass; 4]>;
+
+/// Type of a single [`@page`][page selector]
+///
+/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors
+#[derive(Clone, Debug, MallocSizeOf, ToShmem)]
+pub struct PageSelector {
+ /// Page name
+ ///
+ /// https://drafts.csswg.org/css-page-3/#page-type-selector
+ pub name: AtomIdent,
+ /// Pseudo-classes for [`@page`][page-selectors]
+ ///
+ /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors
+ pub pseudos: PagePseudoClasses,
+}
+
+impl PageSelector {
+ /// Checks if the ident matches a page-name's ident.
+ ///
+ /// This does not take pseudo selectors into account.
+ #[inline]
+ pub fn ident_matches(&self, other: &CustomIdent) -> bool {
+ self.name.0 == other.0
+ }
+
+ /// Checks that this selector matches the ident and all pseudo classes are
+ /// present in the provided flags.
+ #[inline]
+ pub fn matches(&self, name: &CustomIdent, flags: PagePseudoClassFlags) -> bool {
+ self.ident_matches(name) && self.flags_match(flags)
+ }
+
+ /// Checks that all pseudo classes in this selector are present in the
+ /// provided flags.
+ ///
+ /// Equivalent to, but may be more efficient than:
+ ///
+ /// ```
+ /// match_specificity(flags).is_some()
+ /// ```
+ pub fn flags_match(&self, flags: PagePseudoClassFlags) -> bool {
+ self.pseudos.iter().all(|pc| flags.contains_class(pc))
+ }
+
+ /// Implements specificity calculation for a page selector given a set of
+ /// page pseudo-classes to match with.
+ /// If this selector includes any pseudo-classes that are not in the flags,
+ /// then this will return None.
+ ///
+ /// To fit the specificity calculation into a 32-bit value, this limits the
+ /// maximum count of :first and :blank to 32767, and the maximum count of
+ /// :left and :right to 65535.
+ ///
+ /// https://drafts.csswg.org/css-page-3/#cascading-and-page-context
+ pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option<u32> {
+ let mut g: usize = 0;
+ let mut h: usize = 0;
+ for pc in self.pseudos.iter() {
+ if !flags.contains_class(pc) {
+ return None;
+ }
+ match pc {
+ PagePseudoClass::First | PagePseudoClass::Blank => g += 1,
+ PagePseudoClass::Left | PagePseudoClass::Right => h += 1,
+ }
+ }
+ let h = h.min(0xFFFF) as u32;
+ let g = (g.min(0x7FFF) as u32) << 16;
+ let f = if self.name.0.is_empty() {
+ 0
+ } else {
+ 0x80000000
+ };
+ Some(h + g + f)
+ }
+}
+
+impl ToCss for PageSelector {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.name.to_css(dest)?;
+ for pc in self.pseudos.iter() {
+ dest.write_str(pc.to_str())?;
+ }
+ Ok(())
+ }
+}
+
+fn parse_page_name<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AtomIdent, ParseError<'i>> {
+ let s = input.expect_ident()?;
+ Ok(AtomIdent::from(&**s))
+}
+
+impl Parse for PageSelector {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let name = input
+ .try_parse(parse_page_name)
+ .unwrap_or(AtomIdent(atom!("")));
+ let mut pseudos = PagePseudoClasses::default();
+ while let Ok(pc) = input.try_parse(PagePseudoClass::parse) {
+ pseudos.push(pc);
+ }
+ Ok(PageSelector { name, pseudos })
+ }
+}
+
+/// A list of [`@page`][page selectors]
+///
+/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors
+#[derive(Clone, Debug, Default, MallocSizeOf, ToCss, ToShmem)]
+#[css(comma)]
+pub struct PageSelectors(#[css(iterable)] pub Box<[PageSelector]>);
+
+impl PageSelectors {
+ /// Creates a new PageSelectors from a Vec, as from parse_comma_separated
+ #[inline]
+ pub fn new(s: Vec<PageSelector>) -> Self {
+ PageSelectors(s.into())
+ }
+ /// Returns true iff there are any page selectors
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.as_slice().is_empty()
+ }
+ /// Get the underlying PageSelector data as a slice
+ #[inline]
+ pub fn as_slice(&self) -> &[PageSelector] {
+ &*self.0
+ }
+}
+
+impl Parse for PageSelectors {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(PageSelectors::new(input.parse_comma_separated(|i| {
+ PageSelector::parse(context, i)
+ })?))
+ }
+}
+
+/// A [`@page`][page] rule.
+///
+/// This implements only a limited subset of the CSS
+/// 2.2 syntax.
+///
+/// [page]: https://drafts.csswg.org/css2/page.html#page-box
+/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors
+#[derive(Clone, Debug, ToShmem)]
+pub struct PageRule {
+ /// Selectors of the page-rule
+ pub selectors: PageSelectors,
+ /// Nested rules.
+ pub rules: Arc<Locked<CssRules>>,
+ /// The declaration block this page rule contains.
+ pub block: Arc<Locked<PropertyDeclarationBlock>>,
+ /// The source position this rule was found at.
+ pub source_location: SourceLocation,
+}
+
+impl PageRule {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ // Measurement of other fields may be added later.
+ self.rules.unconditional_shallow_size_of(ops) +
+ self.rules.read_with(guard).size_of(guard, ops) +
+ self.block.unconditional_shallow_size_of(ops) +
+ self.block.read_with(guard).size_of(ops) +
+ self.selectors.size_of(ops)
+ }
+ /// Computes the specificity of this page rule when matched with flags.
+ ///
+ /// Computing this value has linear-complexity with the size of the
+ /// selectors, so the caller should usually call this once and cache the
+ /// result.
+ ///
+ /// Returns None if the flags do not match this page rule.
+ ///
+ /// The return type is ordered by page-rule specificity.
+ pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option<u32> {
+ let mut specificity = None;
+ for s in self.selectors.0.iter().map(|s| s.match_specificity(flags)) {
+ specificity = s.max(specificity);
+ }
+ specificity
+ }
+}
+
+impl ToCssWithGuard for PageRule {
+ /// Serialization of PageRule is not specced, adapted from steps for
+ /// StyleRule.
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ // https://drafts.csswg.org/cssom/#serialize-a-css-rule
+ dest.write_str("@page ")?;
+ if !self.selectors.is_empty() {
+ self.selectors.to_css(&mut CssWriter::new(dest))?;
+ dest.write_char(' ')?;
+ }
+ dest.write_char('{')?;
+
+ // TODO: share more/most of this with style rules
+ // https://bugzilla.mozilla.org/1867164
+ let declaration_block = self.block.read_with(guard);
+ let has_declarations = !declaration_block.declarations().is_empty();
+
+ let rules = self.rules.read_with(guard);
+ if !rules.is_empty() {
+ if has_declarations {
+ dest.write_str("\n ")?;
+ declaration_block.to_css(dest)?;
+ }
+ return rules.to_css_block_without_opening(guard, dest);
+ }
+
+ if has_declarations {
+ dest.write_char(' ')?;
+ declaration_block.to_css(dest)?;
+ }
+ dest.write_str(" }")
+ }
+}
+
+impl DeepCloneWithLock for PageRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ let rules = self.rules.read_with(&guard);
+ PageRule {
+ selectors: self.selectors.clone(),
+ block: Arc::new(lock.wrap(self.block.read_with(&guard).clone())),
+ rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
diff --git a/servo/components/style/stylesheets/property_rule.rs b/servo/components/style/stylesheets/property_rule.rs
new file mode 100644
index 0000000000..abe32050bf
--- /dev/null
+++ b/servo/components/style/stylesheets/property_rule.rs
@@ -0,0 +1,5 @@
+/* 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/. */
+
+pub use crate::properties_and_values::registry::PropertyRegistration as PropertyRule;
diff --git a/servo/components/style/stylesheets/rule_list.rs b/servo/components/style/stylesheets/rule_list.rs
new file mode 100644
index 0000000000..1b9f330185
--- /dev/null
+++ b/servo/components/style/stylesheets/rule_list.rs
@@ -0,0 +1,189 @@
+/* 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/. */
+
+//! A list of CSS rules.
+
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::stylesheets::loader::StylesheetLoader;
+use crate::stylesheets::rule_parser::InsertRuleContext;
+use crate::stylesheets::stylesheet::StylesheetContents;
+use crate::stylesheets::{AllowImportRules, CssRule, CssRuleTypes, RulesMutateError};
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocShallowSizeOf, MallocSizeOfOps};
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+
+/// A list of CSS rules.
+#[derive(Debug, ToShmem)]
+pub struct CssRules(pub Vec<CssRule>);
+
+impl CssRules {
+ /// Whether this CSS rules is empty.
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+impl DeepCloneWithLock for CssRules {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ CssRules(
+ self.0
+ .iter()
+ .map(|x| x.deep_clone_with_lock(lock, guard, params))
+ .collect(),
+ )
+ }
+}
+
+impl CssRules {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = self.0.shallow_size_of(ops);
+ for rule in self.0.iter() {
+ n += rule.size_of(guard, ops);
+ }
+ n
+ }
+
+ /// Trivially construct a new set of CSS rules.
+ pub fn new(rules: Vec<CssRule>, shared_lock: &SharedRwLock) -> Arc<Locked<CssRules>> {
+ Arc::new(shared_lock.wrap(CssRules(rules)))
+ }
+
+ /// Returns whether all the rules in this list are namespace or import
+ /// rules.
+ fn only_ns_or_import(&self) -> bool {
+ self.0.iter().all(|r| match *r {
+ CssRule::Namespace(..) | CssRule::Import(..) => true,
+ _ => false,
+ })
+ }
+
+ /// <https://drafts.csswg.org/cssom/#remove-a-css-rule>
+ pub fn remove_rule(&mut self, index: usize) -> Result<(), RulesMutateError> {
+ // Step 1, 2
+ if index >= self.0.len() {
+ return Err(RulesMutateError::IndexSize);
+ }
+
+ {
+ // Step 3
+ let ref rule = self.0[index];
+
+ // Step 4
+ if let CssRule::Namespace(..) = *rule {
+ if !self.only_ns_or_import() {
+ return Err(RulesMutateError::InvalidState);
+ }
+ }
+ }
+
+ // Step 5, 6
+ self.0.remove(index);
+ Ok(())
+ }
+
+ /// Serializes this CSSRules to CSS text as a block of rules.
+ ///
+ /// This should be speced into CSSOM spec at some point. See
+ /// <https://github.com/w3c/csswg-drafts/issues/1985>
+ pub fn to_css_block(
+ &self,
+ guard: &SharedRwLockReadGuard,
+ dest: &mut CssStringWriter,
+ ) -> fmt::Result {
+ dest.write_str(" {")?;
+ self.to_css_block_without_opening(guard, dest)
+ }
+
+ /// As above, but without the opening curly bracket. That's needed for nesting.
+ pub fn to_css_block_without_opening(
+ &self,
+ guard: &SharedRwLockReadGuard,
+ dest: &mut CssStringWriter,
+ ) -> fmt::Result {
+ for rule in self.0.iter() {
+ dest.write_str("\n ")?;
+ rule.to_css(guard, dest)?;
+ }
+ dest.write_str("\n}")
+ }
+}
+
+/// A trait to implement helpers for `Arc<Locked<CssRules>>`.
+pub trait CssRulesHelpers {
+ /// <https://drafts.csswg.org/cssom/#insert-a-css-rule>
+ ///
+ /// Written in this funky way because parsing an @import rule may cause us
+ /// to clone a stylesheet from the same document due to caching in the CSS
+ /// loader.
+ ///
+ /// TODO(emilio): We could also pass the write guard down into the loader
+ /// instead, but that seems overkill.
+ fn insert_rule(
+ &self,
+ lock: &SharedRwLock,
+ rule: &str,
+ parent_stylesheet_contents: &StylesheetContents,
+ index: usize,
+ nested: CssRuleTypes,
+ loader: Option<&dyn StylesheetLoader>,
+ allow_import_rules: AllowImportRules,
+ ) -> Result<CssRule, RulesMutateError>;
+}
+
+impl CssRulesHelpers for Locked<CssRules> {
+ fn insert_rule(
+ &self,
+ lock: &SharedRwLock,
+ rule: &str,
+ parent_stylesheet_contents: &StylesheetContents,
+ index: usize,
+ containing_rule_types: CssRuleTypes,
+ loader: Option<&dyn StylesheetLoader>,
+ allow_import_rules: AllowImportRules,
+ ) -> Result<CssRule, RulesMutateError> {
+ let new_rule = {
+ let read_guard = lock.read();
+ let rules = self.read_with(&read_guard);
+
+ // Step 1, 2
+ if index > rules.0.len() {
+ return Err(RulesMutateError::IndexSize);
+ }
+
+ let insert_rule_context = InsertRuleContext {
+ rule_list: &rules.0,
+ index,
+ containing_rule_types,
+ };
+
+ // Steps 3, 4, 5, 6
+ CssRule::parse(
+ &rule,
+ insert_rule_context,
+ parent_stylesheet_contents,
+ lock,
+ loader,
+ allow_import_rules,
+ )?
+ };
+
+ {
+ let mut write_guard = lock.write();
+ let rules = self.write_with(&mut write_guard);
+ rules.0.insert(index, new_rule.clone());
+ }
+
+ Ok(new_rule)
+ }
+}
diff --git a/servo/components/style/stylesheets/rule_parser.rs b/servo/components/style/stylesheets/rule_parser.rs
new file mode 100644
index 0000000000..742ad5d250
--- /dev/null
+++ b/servo/components/style/stylesheets/rule_parser.rs
@@ -0,0 +1,982 @@
+/* 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/. */
+
+//! Parsing of the stylesheet contents.
+
+use crate::counter_style::{parse_counter_style_body, parse_counter_style_name_definition};
+use crate::custom_properties::parse_name as parse_custom_property_name;
+use crate::error_reporting::ContextualParseError;
+use crate::font_face::parse_font_face_block;
+use crate::media_queries::MediaList;
+use crate::parser::{Parse, ParserContext};
+use crate::properties::declaration_block::{
+ parse_property_declaration_list, DeclarationParserState, PropertyDeclarationBlock,
+};
+use crate::properties_and_values::rule::{parse_property_block, PropertyRuleName};
+use crate::selector_parser::{SelectorImpl, SelectorParser};
+use crate::shared_lock::{Locked, SharedRwLock};
+use crate::str::starts_with_ignore_ascii_case;
+use crate::stylesheets::container_rule::{ContainerCondition, ContainerRule};
+use crate::stylesheets::document_rule::DocumentCondition;
+use crate::stylesheets::font_feature_values_rule::parse_family_name_list;
+use crate::stylesheets::import_rule::{ImportLayer, ImportRule, ImportSupportsCondition};
+use crate::stylesheets::keyframes_rule::parse_keyframe_list;
+use crate::stylesheets::layer_rule::{LayerBlockRule, LayerName, LayerStatementRule};
+use crate::stylesheets::supports_rule::SupportsCondition;
+use crate::stylesheets::{
+ AllowImportRules, CorsMode, CssRule, CssRuleType, CssRuleTypes, CssRules, DocumentRule,
+ FontFeatureValuesRule, FontPaletteValuesRule, KeyframesRule, MarginRule, MarginRuleType,
+ MediaRule, NamespaceRule, PageRule, PageSelectors, RulesMutateError, StyleRule,
+ StylesheetLoader, SupportsRule,
+};
+use crate::values::computed::font::FamilyName;
+use crate::values::{CssUrl, CustomIdent, DashedIdent, KeyframesName};
+use crate::{Atom, Namespace, Prefix};
+use cssparser::{
+ AtRuleParser, BasicParseError, BasicParseErrorKind, CowRcStr, DeclarationParser, Parser,
+ ParserState, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation,
+ SourcePosition,
+};
+use selectors::parser::{ParseRelative, SelectorList};
+use servo_arc::Arc;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// The information we need particularly to do CSSOM insertRule stuff.
+pub struct InsertRuleContext<'a> {
+ /// The rule list we're about to insert into.
+ pub rule_list: &'a [CssRule],
+ /// The index we're about to get inserted at.
+ pub index: usize,
+ /// The containing rule types of our ancestors.
+ pub containing_rule_types: CssRuleTypes,
+}
+
+impl<'a> InsertRuleContext<'a> {
+ /// Returns the max rule state allowable for insertion at a given index in
+ /// the rule list.
+ pub fn max_rule_state_at_index(&self, index: usize) -> State {
+ let rule = match self.rule_list.get(index) {
+ Some(rule) => rule,
+ None => return State::Body,
+ };
+ match rule {
+ CssRule::Import(..) => State::Imports,
+ CssRule::Namespace(..) => State::Namespaces,
+ CssRule::LayerStatement(..) => {
+ // If there are @import / @namespace after this layer, then
+ // we're in the early-layers phase, otherwise we're in the body
+ // and everything is fair game.
+ let next_non_layer_statement_rule = self.rule_list[index + 1..]
+ .iter()
+ .find(|r| !matches!(*r, CssRule::LayerStatement(..)));
+ if let Some(non_layer) = next_non_layer_statement_rule {
+ if matches!(*non_layer, CssRule::Import(..) | CssRule::Namespace(..)) {
+ return State::EarlyLayers;
+ }
+ }
+ State::Body
+ },
+ _ => State::Body,
+ }
+ }
+}
+
+/// The parser for the top-level rules in a stylesheet.
+pub struct TopLevelRuleParser<'a, 'i> {
+ /// A reference to the lock we need to use to create rules.
+ pub shared_lock: &'a SharedRwLock,
+ /// A reference to a stylesheet loader if applicable, for `@import` rules.
+ pub loader: Option<&'a dyn StylesheetLoader>,
+ /// The top-level parser context.
+ pub context: ParserContext<'a>,
+ /// The current state of the parser.
+ pub state: State,
+ /// Whether we have tried to parse was invalid due to being in the wrong
+ /// place (e.g. an @import rule was found while in the `Body` state). Reset
+ /// to `false` when `take_had_hierarchy_error` is called.
+ pub dom_error: Option<RulesMutateError>,
+ /// The info we need insert a rule in a list.
+ pub insert_rule_context: Option<InsertRuleContext<'a>>,
+ /// Whether @import rules will be allowed.
+ pub allow_import_rules: AllowImportRules,
+ /// Parser state for declaration blocks in either nested rules or style rules.
+ pub declaration_parser_state: DeclarationParserState<'i>,
+ /// State we keep around only for error reporting purposes. Right now that contains just the
+ /// selectors stack for nesting, if any.
+ ///
+ /// TODO(emilio): This isn't populated properly for `insertRule()` but...
+ pub error_reporting_state: Vec<SelectorList<SelectorImpl>>,
+ /// The rules we've parsed so far.
+ pub rules: Vec<CssRule>,
+}
+
+impl<'a, 'i> TopLevelRuleParser<'a, 'i> {
+ #[inline]
+ fn nested(&mut self) -> &mut NestedRuleParser<'a, 'i> {
+ // SAFETY: NestedRuleParser is just a repr(transparent) wrapper over TopLevelRuleParser
+ const_assert!(
+ std::mem::size_of::<TopLevelRuleParser<'static, 'static>>() ==
+ std::mem::size_of::<NestedRuleParser<'static, 'static>>()
+ );
+ const_assert!(
+ std::mem::align_of::<TopLevelRuleParser<'static, 'static>>() ==
+ std::mem::align_of::<NestedRuleParser<'static, 'static>>()
+ );
+ unsafe { &mut *(self as *mut _ as *mut NestedRuleParser<'a, 'i>) }
+ }
+
+ /// Returns the current state of the parser.
+ #[inline]
+ pub fn state(&self) -> State {
+ self.state
+ }
+
+ /// Checks whether we can parse a rule that would transition us to
+ /// `new_state`.
+ ///
+ /// This is usually a simple branch, but we may need more bookkeeping if
+ /// doing `insertRule` from CSSOM.
+ fn check_state(&mut self, new_state: State) -> bool {
+ if self.state > new_state {
+ self.dom_error = Some(RulesMutateError::HierarchyRequest);
+ return false;
+ }
+
+ let ctx = match self.insert_rule_context {
+ Some(ref ctx) => ctx,
+ None => return true,
+ };
+
+ let max_rule_state = ctx.max_rule_state_at_index(ctx.index);
+ if new_state > max_rule_state {
+ self.dom_error = Some(RulesMutateError::HierarchyRequest);
+ return false;
+ }
+
+ // If there's anything that isn't a namespace rule (or import rule, but
+ // we checked that already at the beginning), reject with a
+ // StateError.
+ if new_state == State::Namespaces &&
+ ctx.rule_list[ctx.index..]
+ .iter()
+ .any(|r| !matches!(*r, CssRule::Namespace(..)))
+ {
+ self.dom_error = Some(RulesMutateError::InvalidState);
+ return false;
+ }
+
+ true
+ }
+}
+
+/// The current state of the parser.
+#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
+pub enum State {
+ /// We haven't started parsing rules.
+ Start = 1,
+ /// We're parsing early `@layer` statement rules.
+ EarlyLayers = 2,
+ /// We're parsing `@import` and early `@layer` statement rules.
+ Imports = 3,
+ /// We're parsing `@namespace` rules.
+ Namespaces = 4,
+ /// We're parsing the main body of the stylesheet.
+ Body = 5,
+}
+
+#[derive(Clone, Debug, MallocSizeOf, ToShmem)]
+/// Vendor prefix.
+pub enum VendorPrefix {
+ /// -moz prefix.
+ Moz,
+ /// -webkit prefix.
+ WebKit,
+}
+
+/// A rule prelude for at-rule with block.
+pub enum AtRulePrelude {
+ /// A @font-face rule prelude.
+ FontFace,
+ /// A @font-feature-values rule prelude, with its FamilyName list.
+ FontFeatureValues(Vec<FamilyName>),
+ /// A @font-palette-values rule prelude, with its identifier.
+ FontPaletteValues(DashedIdent),
+ /// A @counter-style rule prelude, with its counter style name.
+ CounterStyle(CustomIdent),
+ /// A @media rule prelude, with its media queries.
+ Media(Arc<Locked<MediaList>>),
+ /// A @container rule prelude.
+ Container(Arc<ContainerCondition>),
+ /// An @supports rule, with its conditional
+ Supports(SupportsCondition),
+ /// A @keyframes rule, with its animation name and vendor prefix if exists.
+ Keyframes(KeyframesName, Option<VendorPrefix>),
+ /// A @page rule prelude, with its page name if it exists.
+ Page(PageSelectors),
+ /// A @property rule prelude.
+ Property(PropertyRuleName),
+ /// A @document rule, with its conditional.
+ Document(DocumentCondition),
+ /// A @import rule prelude.
+ Import(
+ CssUrl,
+ Arc<Locked<MediaList>>,
+ Option<ImportSupportsCondition>,
+ ImportLayer,
+ ),
+ /// A @margin rule prelude.
+ Margin(MarginRuleType),
+ /// A @namespace rule prelude.
+ Namespace(Option<Prefix>, Namespace),
+ /// A @layer rule prelude.
+ Layer(Vec<LayerName>),
+}
+
+impl AtRulePrelude {
+ fn name(&self) -> &'static str {
+ match *self {
+ Self::FontFace => "font-face",
+ Self::FontFeatureValues(..) => "font-feature-values",
+ Self::FontPaletteValues(..) => "font-palette-values",
+ Self::CounterStyle(..) => "counter-style",
+ Self::Media(..) => "media",
+ Self::Container(..) => "container",
+ Self::Supports(..) => "supports",
+ Self::Keyframes(..) => "keyframes",
+ Self::Page(..) => "page",
+ Self::Property(..) => "property",
+ Self::Document(..) => "-moz-document",
+ Self::Import(..) => "import",
+ Self::Margin(..) => "margin",
+ Self::Namespace(..) => "namespace",
+ Self::Layer(..) => "layer",
+ }
+ }
+}
+
+impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a, 'i> {
+ type Prelude = AtRulePrelude;
+ type AtRule = SourcePosition;
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_prelude<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<AtRulePrelude, ParseError<'i>> {
+ match_ignore_ascii_case! { &*name,
+ "import" => {
+ if !self.check_state(State::Imports) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedImportRule))
+ }
+
+ if let AllowImportRules::No = self.allow_import_rules {
+ return Err(input.new_custom_error(StyleParseErrorKind::DisallowedImportRule))
+ }
+
+ // FIXME(emilio): We should always be able to have a loader
+ // around! See bug 1533783.
+ if self.loader.is_none() {
+ error!("Saw @import rule, but no way to trigger the load");
+ return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedImportRule))
+ }
+
+ let url_string = input.expect_url_or_string()?.as_ref().to_owned();
+ let url = CssUrl::parse_from_string(url_string, &self.context, CorsMode::None);
+
+ let (layer, supports) = ImportRule::parse_layer_and_supports(input, &mut self.context);
+
+ let media = MediaList::parse(&self.context, input);
+ let media = Arc::new(self.shared_lock.wrap(media));
+
+ return Ok(AtRulePrelude::Import(url, media, supports, layer));
+ },
+ "namespace" => {
+ if !self.check_state(State::Namespaces) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedNamespaceRule))
+ }
+
+ let prefix = input.try_parse(|i| i.expect_ident_cloned())
+ .map(|s| Prefix::from(s.as_ref())).ok();
+ let maybe_namespace = match input.expect_url_or_string() {
+ Ok(url_or_string) => url_or_string,
+ Err(BasicParseError { kind: BasicParseErrorKind::UnexpectedToken(t), location }) => {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnexpectedTokenWithinNamespace(t)))
+ }
+ Err(e) => return Err(e.into()),
+ };
+ let url = Namespace::from(maybe_namespace.as_ref());
+ return Ok(AtRulePrelude::Namespace(prefix, url));
+ },
+ // @charset is removed by rust-cssparser if it’s the first rule in the stylesheet
+ // anything left is invalid.
+ "charset" => {
+ self.dom_error = Some(RulesMutateError::HierarchyRequest);
+ return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedCharsetRule))
+ },
+ "layer" => {
+ let state_to_check = if self.state <= State::EarlyLayers {
+ // The real state depends on whether there's a block or not.
+ // We don't know that yet, but the parse_block check deals
+ // with that.
+ State::EarlyLayers
+ } else {
+ State::Body
+ };
+ if !self.check_state(state_to_check) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ },
+ _ => {
+ // All other rules have blocks, so we do this check early in
+ // parse_block instead.
+ }
+ }
+
+ AtRuleParser::parse_prelude(self.nested(), name, input)
+ }
+
+ #[inline]
+ fn parse_block<'t>(
+ &mut self,
+ prelude: AtRulePrelude,
+ start: &ParserState,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self::AtRule, ParseError<'i>> {
+ if !self.check_state(State::Body) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ AtRuleParser::parse_block(self.nested(), prelude, start, input)?;
+ self.state = State::Body;
+ Ok(start.position())
+ }
+
+ #[inline]
+ fn rule_without_block(
+ &mut self,
+ prelude: AtRulePrelude,
+ start: &ParserState,
+ ) -> Result<Self::AtRule, ()> {
+ match prelude {
+ AtRulePrelude::Import(url, media, supports, layer) => {
+ let loader = self
+ .loader
+ .expect("Expected a stylesheet loader for @import");
+
+ let import_rule = loader.request_stylesheet(
+ url,
+ start.source_location(),
+ &self.context,
+ &self.shared_lock,
+ media,
+ supports,
+ layer,
+ );
+
+ self.state = State::Imports;
+ self.rules.push(CssRule::Import(import_rule))
+ },
+ AtRulePrelude::Namespace(prefix, url) => {
+ let namespaces = self.context.namespaces.to_mut();
+ let prefix = if let Some(prefix) = prefix {
+ namespaces.prefixes.insert(prefix.clone(), url.clone());
+ Some(prefix)
+ } else {
+ namespaces.default = Some(url.clone());
+ None
+ };
+
+ self.state = State::Namespaces;
+ self.rules.push(CssRule::Namespace(Arc::new(NamespaceRule {
+ prefix,
+ url,
+ source_location: start.source_location(),
+ })));
+ },
+ AtRulePrelude::Layer(..) => {
+ AtRuleParser::rule_without_block(self.nested(), prelude, start)?;
+ if self.state <= State::EarlyLayers {
+ self.state = State::EarlyLayers;
+ } else {
+ self.state = State::Body;
+ }
+ },
+ _ => AtRuleParser::rule_without_block(self.nested(), prelude, start)?,
+ };
+
+ Ok(start.position())
+ }
+}
+
+impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'i> {
+ type Prelude = SelectorList<SelectorImpl>;
+ type QualifiedRule = SourcePosition;
+ type Error = StyleParseErrorKind<'i>;
+
+ #[inline]
+ fn parse_prelude<'t>(
+ &mut self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self::Prelude, ParseError<'i>> {
+ if !self.check_state(State::Body) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ QualifiedRuleParser::parse_prelude(self.nested(), input)
+ }
+
+ #[inline]
+ fn parse_block<'t>(
+ &mut self,
+ prelude: Self::Prelude,
+ start: &ParserState,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self::QualifiedRule, ParseError<'i>> {
+ QualifiedRuleParser::parse_block(self.nested(), prelude, start, input)?;
+ self.state = State::Body;
+ Ok(start.position())
+ }
+}
+
+#[repr(transparent)]
+#[derive(Deref, DerefMut)]
+struct NestedRuleParser<'a, 'i>(TopLevelRuleParser<'a, 'i>);
+
+struct NestedParseResult {
+ rules: Vec<CssRule>,
+ declarations: PropertyDeclarationBlock,
+}
+
+impl NestedParseResult {
+ fn into_rules(
+ mut self,
+ shared_lock: &SharedRwLock,
+ source_location: SourceLocation,
+ ) -> Arc<Locked<CssRules>> {
+ lazy_static! {
+ static ref AMPERSAND: SelectorList<SelectorImpl> = {
+ let list = SelectorList::ampersand();
+ list.slice()
+ .iter()
+ .for_each(|selector| selector.mark_as_intentionally_leaked());
+ list
+ };
+ };
+
+ if !self.declarations.is_empty() {
+ self.rules.insert(
+ 0,
+ CssRule::Style(Arc::new(shared_lock.wrap(StyleRule {
+ selectors: AMPERSAND.clone(),
+ block: Arc::new(shared_lock.wrap(self.declarations)),
+ rules: None,
+ source_location,
+ }))),
+ )
+ }
+
+ CssRules::new(self.rules, shared_lock)
+ }
+}
+
+impl<'a, 'i> NestedRuleParser<'a, 'i> {
+ #[inline]
+ fn in_style_rule(&self) -> bool {
+ self.context.rule_types.contains(CssRuleType::Style)
+ }
+
+ #[inline]
+ fn in_page_rule(&self) -> bool {
+ self.context.rule_types.contains(CssRuleType::Page)
+ }
+
+ #[inline]
+ fn in_style_or_page_rule(&self) -> bool {
+ let types = CssRuleTypes::from_bits(CssRuleType::Style.bit() | CssRuleType::Page.bit());
+ self.context.rule_types.intersects(types)
+ }
+
+ // https://drafts.csswg.org/css-nesting/#conditionals
+ // In addition to nested style rules, this specification allows nested group rules inside
+ // of style rules: any at-rule whose body contains style rules can be nested inside of a
+ // style rule as well.
+ fn at_rule_allowed(&self, prelude: &AtRulePrelude) -> bool {
+ match prelude {
+ AtRulePrelude::Media(..) |
+ AtRulePrelude::Supports(..) |
+ AtRulePrelude::Container(..) |
+ AtRulePrelude::Document(..) |
+ AtRulePrelude::Layer(..) => true,
+
+ AtRulePrelude::Namespace(..) |
+ AtRulePrelude::FontFace |
+ AtRulePrelude::FontFeatureValues(..) |
+ AtRulePrelude::FontPaletteValues(..) |
+ AtRulePrelude::CounterStyle(..) |
+ AtRulePrelude::Keyframes(..) |
+ AtRulePrelude::Page(..) |
+ AtRulePrelude::Property(..) |
+ AtRulePrelude::Import(..) => !self.in_style_or_page_rule(),
+ AtRulePrelude::Margin(..) => self.in_page_rule(),
+ }
+ }
+
+ fn nest_for_rule<R>(&mut self, rule_type: CssRuleType, cb: impl FnOnce(&mut Self) -> R) -> R {
+ let old_rule_types = self.context.rule_types;
+ self.context.rule_types.insert(rule_type);
+ let r = cb(self);
+ self.context.rule_types = old_rule_types;
+ r
+ }
+
+ fn parse_nested(
+ &mut self,
+ input: &mut Parser<'i, '_>,
+ rule_type: CssRuleType,
+ ) -> NestedParseResult {
+ self.nest_for_rule(rule_type, |parser| {
+ let parse_declarations = parser.parse_declarations();
+ let mut old_declaration_state = std::mem::take(&mut parser.declaration_parser_state);
+ let mut rules = std::mem::take(&mut parser.rules);
+ let mut iter = RuleBodyParser::new(input, parser);
+ while let Some(result) = iter.next() {
+ match result {
+ Ok(()) => {},
+ Err((error, slice)) => {
+ if parse_declarations {
+ let top = &mut **iter.parser;
+ top.declaration_parser_state
+ .did_error(&top.context, error, slice);
+ } else {
+ let location = error.location;
+ let error = ContextualParseError::InvalidRule(slice, error);
+ iter.parser.context.log_css_error(location, error);
+ }
+ },
+ }
+ }
+ let declarations = if parse_declarations {
+ let top = &mut **parser;
+ top.declaration_parser_state
+ .report_errors_if_needed(&top.context, &top.error_reporting_state);
+ parser.declaration_parser_state.take_declarations()
+ } else {
+ PropertyDeclarationBlock::default()
+ };
+ debug_assert!(
+ !parser.declaration_parser_state.has_parsed_declarations(),
+ "Parsed but didn't consume declarations"
+ );
+ std::mem::swap(
+ &mut parser.declaration_parser_state,
+ &mut old_declaration_state,
+ );
+ std::mem::swap(&mut parser.rules, &mut rules);
+ NestedParseResult {
+ rules,
+ declarations,
+ }
+ })
+ }
+
+ #[inline(never)]
+ fn handle_error_reporting_selectors_pre(
+ &mut self,
+ start: &ParserState,
+ selectors: &SelectorList<SelectorImpl>,
+ ) {
+ use cssparser::ToCss;
+ debug_assert!(self.context.error_reporting_enabled());
+ self.error_reporting_state.push(selectors.clone());
+ 'selector_loop: for selector in selectors.slice().iter() {
+ let mut current = selector.iter();
+ loop {
+ let mut found_host = false;
+ let mut found_non_host = false;
+ for component in &mut current {
+ if component.is_host() {
+ found_host = true;
+ } else {
+ found_non_host = true;
+ }
+ if found_host && found_non_host {
+ self.context.log_css_error(
+ start.source_location(),
+ ContextualParseError::NeverMatchingHostSelector(
+ selector.to_css_string(),
+ ),
+ );
+ continue 'selector_loop;
+ }
+ }
+ if current.next_sequence().is_none() {
+ break;
+ }
+ }
+ }
+ }
+
+ fn handle_error_reporting_selectors_post(&mut self) {
+ self.error_reporting_state.pop();
+ }
+}
+
+impl<'a, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'i> {
+ type Prelude = AtRulePrelude;
+ type AtRule = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_prelude<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self::Prelude, ParseError<'i>> {
+ Ok(match_ignore_ascii_case! { &*name,
+ "media" => {
+ let media_queries = MediaList::parse(&self.context, input);
+ let arc = Arc::new(self.shared_lock.wrap(media_queries));
+ AtRulePrelude::Media(arc)
+ },
+ "supports" => {
+ let cond = SupportsCondition::parse(input)?;
+ AtRulePrelude::Supports(cond)
+ },
+ "font-face" => {
+ AtRulePrelude::FontFace
+ },
+ "container" if static_prefs::pref!("layout.css.container-queries.enabled") => {
+ let condition = Arc::new(ContainerCondition::parse(&self.context, input)?);
+ AtRulePrelude::Container(condition)
+ },
+ "layer" => {
+ let names = input.try_parse(|input| {
+ input.parse_comma_separated(|input| {
+ LayerName::parse(&self.context, input)
+ })
+ }).unwrap_or_default();
+ AtRulePrelude::Layer(names)
+ },
+ "font-feature-values" if cfg!(feature = "gecko") => {
+ let family_names = parse_family_name_list(&self.context, input)?;
+ AtRulePrelude::FontFeatureValues(family_names)
+ },
+ "font-palette-values" if static_prefs::pref!("layout.css.font-palette.enabled") => {
+ let name = DashedIdent::parse(&self.context, input)?;
+ AtRulePrelude::FontPaletteValues(name)
+ },
+ "counter-style" if cfg!(feature = "gecko") => {
+ let name = parse_counter_style_name_definition(input)?;
+ AtRulePrelude::CounterStyle(name)
+ },
+ "keyframes" | "-webkit-keyframes" | "-moz-keyframes" => {
+ let prefix = if starts_with_ignore_ascii_case(&*name, "-webkit-") {
+ Some(VendorPrefix::WebKit)
+ } else if starts_with_ignore_ascii_case(&*name, "-moz-") {
+ Some(VendorPrefix::Moz)
+ } else {
+ None
+ };
+ if cfg!(feature = "servo") &&
+ prefix.as_ref().map_or(false, |p| matches!(*p, VendorPrefix::Moz)) {
+ // Servo should not support @-moz-keyframes.
+ return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone())))
+ }
+ let name = KeyframesName::parse(&self.context, input)?;
+ AtRulePrelude::Keyframes(name, prefix)
+ },
+ "page" if cfg!(feature = "gecko") => {
+ AtRulePrelude::Page(
+ input.try_parse(|i| PageSelectors::parse(&self.context, i)).unwrap_or_default()
+ )
+ },
+ "property" if static_prefs::pref!("layout.css.properties-and-values.enabled") => {
+ let name = input.expect_ident_cloned()?;
+ let name = parse_custom_property_name(&name).map_err(|_| {
+ input.new_custom_error(StyleParseErrorKind::UnexpectedIdent(name.clone()))
+ })?;
+ AtRulePrelude::Property(PropertyRuleName(Atom::from(name)))
+ },
+ "-moz-document" if cfg!(feature = "gecko") => {
+ let cond = DocumentCondition::parse(&self.context, input)?;
+ AtRulePrelude::Document(cond)
+ },
+ _ => {
+ if static_prefs::pref!("layout.css.margin-rules.enabled") {
+ if let Some(margin_rule_type) = MarginRuleType::match_name(&name) {
+ return Ok(AtRulePrelude::Margin(margin_rule_type));
+ }
+ }
+ return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone())))
+ },
+ })
+ }
+
+ fn parse_block<'t>(
+ &mut self,
+ prelude: AtRulePrelude,
+ start: &ParserState,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ if !self.at_rule_allowed(&prelude) {
+ self.dom_error = Some(RulesMutateError::HierarchyRequest);
+ return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(prelude.name().into())));
+ }
+ let rule = match prelude {
+ AtRulePrelude::FontFace => self.nest_for_rule(CssRuleType::FontFace, |p| {
+ CssRule::FontFace(Arc::new(p.shared_lock.wrap(
+ parse_font_face_block(&p.context, input, start.source_location()).into(),
+ )))
+ }),
+ AtRulePrelude::FontFeatureValues(family_names) => {
+ self.nest_for_rule(CssRuleType::FontFeatureValues, |p| {
+ CssRule::FontFeatureValues(Arc::new(FontFeatureValuesRule::parse(
+ &p.context,
+ input,
+ family_names,
+ start.source_location(),
+ )))
+ })
+ },
+ AtRulePrelude::FontPaletteValues(name) => {
+ self.nest_for_rule(CssRuleType::FontPaletteValues, |p| {
+ CssRule::FontPaletteValues(Arc::new(FontPaletteValuesRule::parse(
+ &p.context,
+ input,
+ name,
+ start.source_location(),
+ )))
+ })
+ },
+ AtRulePrelude::CounterStyle(name) => {
+ let body = self.nest_for_rule(CssRuleType::CounterStyle, |p| {
+ parse_counter_style_body(name, &p.context, input, start.source_location())
+ })?;
+ CssRule::CounterStyle(Arc::new(self.shared_lock.wrap(body)))
+ },
+ AtRulePrelude::Media(media_queries) => {
+ let source_location = start.source_location();
+ CssRule::Media(Arc::new(MediaRule {
+ media_queries,
+ rules: self
+ .parse_nested(input, CssRuleType::Media)
+ .into_rules(self.shared_lock, source_location),
+ source_location,
+ }))
+ },
+ AtRulePrelude::Supports(condition) => {
+ let enabled =
+ self.nest_for_rule(CssRuleType::Style, |p| condition.eval(&p.context));
+ let source_location = start.source_location();
+ CssRule::Supports(Arc::new(SupportsRule {
+ condition,
+ rules: self
+ .parse_nested(input, CssRuleType::Supports)
+ .into_rules(self.shared_lock, source_location),
+ enabled,
+ source_location,
+ }))
+ },
+ AtRulePrelude::Keyframes(name, vendor_prefix) => {
+ self.nest_for_rule(CssRuleType::Keyframe, |p| {
+ let top = &mut **p;
+ CssRule::Keyframes(Arc::new(top.shared_lock.wrap(KeyframesRule {
+ name,
+ keyframes: parse_keyframe_list(&mut top.context, input, top.shared_lock),
+ vendor_prefix,
+ source_location: start.source_location(),
+ })))
+ })
+ },
+ AtRulePrelude::Page(selectors) => {
+ let source_location = start.source_location();
+ let page_rule = if !static_prefs::pref!("layout.css.margin-rules.enabled") {
+ let declarations = self.nest_for_rule(CssRuleType::Page, |p| {
+ parse_property_declaration_list(&p.context, input, &[])
+ });
+ PageRule {
+ selectors,
+ rules: CssRules::new(vec![], self.shared_lock),
+ block: Arc::new(self.shared_lock.wrap(declarations)),
+ source_location,
+ }
+ } else {
+ let result = self.parse_nested(input, CssRuleType::Page);
+ PageRule {
+ selectors,
+ rules: CssRules::new(result.rules, self.shared_lock),
+ block: Arc::new(self.shared_lock.wrap(result.declarations)),
+ source_location,
+ }
+ };
+ CssRule::Page(Arc::new(self.shared_lock.wrap(page_rule)))
+ },
+ AtRulePrelude::Property(name) => self.nest_for_rule(CssRuleType::Property, |p| {
+ let rule_data =
+ parse_property_block(&p.context, input, name, start.source_location())?;
+ Ok::<CssRule, ParseError<'i>>(CssRule::Property(Arc::new(rule_data)))
+ })?,
+ AtRulePrelude::Document(condition) => {
+ if !cfg!(feature = "gecko") {
+ unreachable!()
+ }
+ let source_location = start.source_location();
+ CssRule::Document(Arc::new(DocumentRule {
+ condition,
+ rules: self
+ .parse_nested(input, CssRuleType::Document)
+ .into_rules(self.shared_lock, source_location),
+ source_location,
+ }))
+ },
+ AtRulePrelude::Container(condition) => {
+ let source_location = start.source_location();
+ CssRule::Container(Arc::new(ContainerRule {
+ condition,
+ rules: self
+ .parse_nested(input, CssRuleType::Container)
+ .into_rules(self.shared_lock, source_location),
+ source_location,
+ }))
+ },
+ AtRulePrelude::Layer(names) => {
+ let name = match names.len() {
+ 0 | 1 => names.into_iter().next(),
+ _ => return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)),
+ };
+ let source_location = start.source_location();
+ CssRule::LayerBlock(Arc::new(LayerBlockRule {
+ name,
+ rules: self
+ .parse_nested(input, CssRuleType::LayerBlock)
+ .into_rules(self.shared_lock, source_location),
+ source_location,
+ }))
+ },
+ AtRulePrelude::Margin(rule_type) => {
+ let declarations = self.nest_for_rule(CssRuleType::Margin, |p| {
+ parse_property_declaration_list(&p.context, input, &[])
+ });
+ CssRule::Margin(Arc::new(MarginRule {
+ rule_type,
+ block: Arc::new(self.shared_lock.wrap(declarations)),
+ source_location: start.source_location(),
+ }))
+ }
+ AtRulePrelude::Import(..) | AtRulePrelude::Namespace(..) => {
+ // These rules don't have blocks.
+ return Err(input.new_unexpected_token_error(cssparser::Token::CurlyBracketBlock));
+ },
+ };
+ self.rules.push(rule);
+ Ok(())
+ }
+
+ #[inline]
+ fn rule_without_block(
+ &mut self,
+ prelude: AtRulePrelude,
+ start: &ParserState,
+ ) -> Result<(), ()> {
+ if self.in_style_rule() {
+ return Err(());
+ }
+ let rule = match prelude {
+ AtRulePrelude::Layer(names) => {
+ if names.is_empty() {
+ return Err(());
+ }
+ CssRule::LayerStatement(Arc::new(LayerStatementRule {
+ names,
+ source_location: start.source_location(),
+ }))
+ },
+ _ => return Err(()),
+ };
+ self.rules.push(rule);
+ Ok(())
+ }
+}
+
+impl<'a, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'i> {
+ type Prelude = SelectorList<SelectorImpl>;
+ type QualifiedRule = ();
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_prelude<'t>(
+ &mut self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self::Prelude, ParseError<'i>> {
+ let selector_parser = SelectorParser {
+ stylesheet_origin: self.context.stylesheet_origin,
+ namespaces: &self.context.namespaces,
+ url_data: self.context.url_data,
+ for_supports_rule: false,
+ };
+ let parse_relative = if self.in_style_rule() {
+ ParseRelative::ForNesting
+ } else {
+ ParseRelative::No
+ };
+ SelectorList::parse(&selector_parser, input, parse_relative)
+ }
+
+ fn parse_block<'t>(
+ &mut self,
+ selectors: Self::Prelude,
+ start: &ParserState,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ let reporting_errors = self.context.error_reporting_enabled();
+ if reporting_errors {
+ self.handle_error_reporting_selectors_pre(start, &selectors);
+ }
+ let result = self.parse_nested(input, CssRuleType::Style);
+ if reporting_errors {
+ self.handle_error_reporting_selectors_post();
+ }
+ let block = Arc::new(self.shared_lock.wrap(result.declarations));
+ let top = &mut **self;
+ top.rules
+ .push(CssRule::Style(Arc::new(top.shared_lock.wrap(StyleRule {
+ selectors,
+ block,
+ rules: if result.rules.is_empty() {
+ None
+ } else {
+ Some(CssRules::new(result.rules, top.shared_lock))
+ },
+ source_location: start.source_location(),
+ }))));
+ Ok(())
+ }
+}
+
+impl<'a, 'i> DeclarationParser<'i> for NestedRuleParser<'a, 'i> {
+ type Declaration = ();
+ type Error = StyleParseErrorKind<'i>;
+ fn parse_value<'t>(
+ &mut self,
+ name: CowRcStr<'i>,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(), ParseError<'i>> {
+ let top = &mut **self;
+ top.declaration_parser_state
+ .parse_value(&top.context, name, input)
+ }
+}
+
+impl<'a, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> for NestedRuleParser<'a, 'i> {
+ fn parse_qualified(&self) -> bool {
+ true
+ }
+
+ /// If nesting is disabled, we can't get there for a non-style-rule. If it's enabled, we parse
+ /// raw declarations there.
+ fn parse_declarations(&self) -> bool {
+ // We also have to check for page rules here because we currently don't
+ // have a bespoke parser for page rules, and parse them as though they
+ // are style rules.
+ self.in_style_or_page_rule()
+ }
+}
diff --git a/servo/components/style/stylesheets/rules_iterator.rs b/servo/components/style/stylesheets/rules_iterator.rs
new file mode 100644
index 0000000000..76d41c8184
--- /dev/null
+++ b/servo/components/style/stylesheets/rules_iterator.rs
@@ -0,0 +1,331 @@
+/* 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/. */
+
+//! An iterator over a list of rules.
+
+use crate::context::QuirksMode;
+use crate::media_queries::Device;
+use crate::shared_lock::SharedRwLockReadGuard;
+use crate::stylesheets::{CssRule, DocumentRule, ImportRule, MediaRule, SupportsRule};
+use smallvec::SmallVec;
+use std::slice;
+
+/// An iterator over a list of rules.
+pub struct RulesIterator<'a, 'b, C>
+where
+ 'b: 'a,
+ C: NestedRuleIterationCondition + 'static,
+{
+ device: &'a Device,
+ quirks_mode: QuirksMode,
+ guard: &'a SharedRwLockReadGuard<'b>,
+ stack: SmallVec<[slice::Iter<'a, CssRule>; 3]>,
+ _phantom: ::std::marker::PhantomData<C>,
+}
+
+impl<'a, 'b, C> RulesIterator<'a, 'b, C>
+where
+ 'b: 'a,
+ C: NestedRuleIterationCondition + 'static,
+{
+ /// Creates a new `RulesIterator` to iterate over `rules`.
+ pub fn new(
+ device: &'a Device,
+ quirks_mode: QuirksMode,
+ guard: &'a SharedRwLockReadGuard<'b>,
+ rules: slice::Iter<'a, CssRule>,
+ ) -> Self {
+ let mut stack = SmallVec::new();
+ stack.push(rules);
+ Self {
+ device,
+ quirks_mode,
+ guard,
+ stack,
+ _phantom: ::std::marker::PhantomData,
+ }
+ }
+
+ /// Skips all the remaining children of the last nested rule processed.
+ pub fn skip_children(&mut self) {
+ self.stack.pop();
+ }
+
+ /// Returns the children of `rule`, and whether `rule` is effective.
+ pub fn children(
+ rule: &'a CssRule,
+ device: &'a Device,
+ quirks_mode: QuirksMode,
+ guard: &'a SharedRwLockReadGuard<'_>,
+ effective: &mut bool,
+ ) -> Option<slice::Iter<'a, CssRule>> {
+ *effective = true;
+ match *rule {
+ CssRule::Namespace(_) |
+ CssRule::FontFace(_) |
+ CssRule::CounterStyle(_) |
+ CssRule::Keyframes(_) |
+ CssRule::Margin(_) |
+ CssRule::Property(_) |
+ CssRule::LayerStatement(_) |
+ CssRule::FontFeatureValues(_) |
+ CssRule::FontPaletteValues(_) => None,
+ CssRule::Page(ref page_rule) => {
+ let page_rule = page_rule.read_with(guard);
+ let rules = page_rule.rules.read_with(guard);
+ Some(rules.0.iter())
+ },
+ CssRule::Style(ref style_rule) => {
+ let style_rule = style_rule.read_with(guard);
+ style_rule
+ .rules
+ .as_ref()
+ .map(|r| r.read_with(guard).0.iter())
+ },
+ CssRule::Import(ref import_rule) => {
+ let import_rule = import_rule.read_with(guard);
+ if !C::process_import(guard, device, quirks_mode, import_rule) {
+ *effective = false;
+ return None;
+ }
+ Some(import_rule.stylesheet.rules(guard).iter())
+ },
+ CssRule::Document(ref doc_rule) => {
+ if !C::process_document(guard, device, quirks_mode, doc_rule) {
+ *effective = false;
+ return None;
+ }
+ Some(doc_rule.rules.read_with(guard).0.iter())
+ },
+ CssRule::Container(ref container_rule) => {
+ Some(container_rule.rules.read_with(guard).0.iter())
+ },
+ CssRule::Media(ref media_rule) => {
+ if !C::process_media(guard, device, quirks_mode, media_rule) {
+ *effective = false;
+ return None;
+ }
+ Some(media_rule.rules.read_with(guard).0.iter())
+ },
+ CssRule::Supports(ref supports_rule) => {
+ if !C::process_supports(guard, device, quirks_mode, supports_rule) {
+ *effective = false;
+ return None;
+ }
+ Some(supports_rule.rules.read_with(guard).0.iter())
+ },
+ CssRule::LayerBlock(ref layer_rule) => Some(layer_rule.rules.read_with(guard).0.iter()),
+ }
+ }
+}
+
+impl<'a, 'b, C> Iterator for RulesIterator<'a, 'b, C>
+where
+ 'b: 'a,
+ C: NestedRuleIterationCondition + 'static,
+{
+ type Item = &'a CssRule;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ while !self.stack.is_empty() {
+ let rule = {
+ let nested_iter = self.stack.last_mut().unwrap();
+ match nested_iter.next() {
+ Some(r) => r,
+ None => {
+ self.stack.pop();
+ continue;
+ },
+ }
+ };
+
+ let mut effective = true;
+ let children = Self::children(
+ rule,
+ self.device,
+ self.quirks_mode,
+ self.guard,
+ &mut effective,
+ );
+ if !effective {
+ continue;
+ }
+
+ if let Some(children) = children {
+ // NOTE: It's important that `children` gets pushed even if
+ // empty, so that `skip_children()` works as expected.
+ self.stack.push(children);
+ }
+
+ return Some(rule);
+ }
+
+ None
+ }
+}
+
+/// RulesIterator.
+pub trait NestedRuleIterationCondition {
+ /// Whether we should process the nested rules in a given `@import` rule.
+ fn process_import(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &ImportRule,
+ ) -> bool;
+
+ /// Whether we should process the nested rules in a given `@media` rule.
+ fn process_media(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &MediaRule,
+ ) -> bool;
+
+ /// Whether we should process the nested rules in a given `@-moz-document`
+ /// rule.
+ fn process_document(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &DocumentRule,
+ ) -> bool;
+
+ /// Whether we should process the nested rules in a given `@supports` rule.
+ fn process_supports(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &SupportsRule,
+ ) -> bool;
+}
+
+/// A struct that represents the condition that a rule applies to the document.
+pub struct EffectiveRules;
+
+impl EffectiveRules {
+ /// Returns whether a given rule is effective.
+ pub fn is_effective(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &CssRule,
+ ) -> bool {
+ match *rule {
+ CssRule::Import(ref import_rule) => {
+ let import_rule = import_rule.read_with(guard);
+ Self::process_import(guard, device, quirks_mode, import_rule)
+ },
+ CssRule::Document(ref doc_rule) => {
+ Self::process_document(guard, device, quirks_mode, doc_rule)
+ },
+ CssRule::Media(ref media_rule) => {
+ Self::process_media(guard, device, quirks_mode, media_rule)
+ },
+ CssRule::Supports(ref supports_rule) => {
+ Self::process_supports(guard, device, quirks_mode, supports_rule)
+ },
+ _ => true,
+ }
+ }
+}
+
+impl NestedRuleIterationCondition for EffectiveRules {
+ fn process_import(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &ImportRule,
+ ) -> bool {
+ match rule.stylesheet.media(guard) {
+ Some(m) => m.evaluate(device, quirks_mode),
+ None => true,
+ }
+ }
+
+ fn process_media(
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ rule: &MediaRule,
+ ) -> bool {
+ rule.media_queries
+ .read_with(guard)
+ .evaluate(device, quirks_mode)
+ }
+
+ fn process_document(
+ _: &SharedRwLockReadGuard,
+ device: &Device,
+ _: QuirksMode,
+ rule: &DocumentRule,
+ ) -> bool {
+ rule.condition.evaluate(device)
+ }
+
+ fn process_supports(
+ _: &SharedRwLockReadGuard,
+ _: &Device,
+ _: QuirksMode,
+ rule: &SupportsRule,
+ ) -> bool {
+ rule.enabled
+ }
+}
+
+/// A filter that processes all the rules in a rule list.
+pub struct AllRules;
+
+impl NestedRuleIterationCondition for AllRules {
+ fn process_import(
+ _: &SharedRwLockReadGuard,
+ _: &Device,
+ _: QuirksMode,
+ _: &ImportRule,
+ ) -> bool {
+ true
+ }
+
+ fn process_media(_: &SharedRwLockReadGuard, _: &Device, _: QuirksMode, _: &MediaRule) -> bool {
+ true
+ }
+
+ fn process_document(
+ _: &SharedRwLockReadGuard,
+ _: &Device,
+ _: QuirksMode,
+ _: &DocumentRule,
+ ) -> bool {
+ true
+ }
+
+ fn process_supports(
+ _: &SharedRwLockReadGuard,
+ _: &Device,
+ _: QuirksMode,
+ _: &SupportsRule,
+ ) -> bool {
+ true
+ }
+}
+
+/// An iterator over all the effective rules of a stylesheet.
+///
+/// NOTE: This iterator recurses into `@import` rules.
+pub type EffectiveRulesIterator<'a, 'b> = RulesIterator<'a, 'b, EffectiveRules>;
+
+impl<'a, 'b> EffectiveRulesIterator<'a, 'b> {
+ /// Returns an iterator over the effective children of a rule, even if
+ /// `rule` itself is not effective.
+ pub fn effective_children(
+ device: &'a Device,
+ quirks_mode: QuirksMode,
+ guard: &'a SharedRwLockReadGuard<'b>,
+ rule: &'a CssRule,
+ ) -> Self {
+ let children =
+ RulesIterator::<AllRules>::children(rule, device, quirks_mode, guard, &mut false);
+ EffectiveRulesIterator::new(device, quirks_mode, guard, children.unwrap_or([].iter()))
+ }
+}
diff --git a/servo/components/style/stylesheets/style_rule.rs b/servo/components/style/stylesheets/style_rule.rs
new file mode 100644
index 0000000000..8f8b9f4a13
--- /dev/null
+++ b/servo/components/style/stylesheets/style_rule.rs
@@ -0,0 +1,104 @@
+/* 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/. */
+
+//! A style rule.
+
+use crate::properties::PropertyDeclarationBlock;
+use crate::selector_parser::SelectorImpl;
+use crate::shared_lock::{
+ DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard,
+};
+use crate::str::CssStringWriter;
+use crate::stylesheets::CssRules;
+use cssparser::SourceLocation;
+#[cfg(feature = "gecko")]
+use malloc_size_of::{
+ MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf, MallocUnconditionalSizeOf,
+};
+use selectors::SelectorList;
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+
+/// A style rule, with selectors and declarations.
+#[derive(Debug, ToShmem)]
+pub struct StyleRule {
+ /// The list of selectors in this rule.
+ pub selectors: SelectorList<SelectorImpl>,
+ /// The declaration block with the properties it contains.
+ pub block: Arc<Locked<PropertyDeclarationBlock>>,
+ /// The nested rules to this style rule. Only non-`None` when nesting is enabled.
+ pub rules: Option<Arc<Locked<CssRules>>>,
+ /// The location in the sheet where it was found.
+ pub source_location: SourceLocation,
+}
+
+impl DeepCloneWithLock for StyleRule {
+ /// Deep clones this StyleRule.
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> StyleRule {
+ StyleRule {
+ selectors: self.selectors.clone(),
+ block: Arc::new(lock.wrap(self.block.read_with(guard).clone())),
+ rules: self.rules.as_ref().map(|rules| {
+ let rules = rules.read_with(guard);
+ Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params)))
+ }),
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+impl StyleRule {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = 0;
+ n += self.selectors.unconditional_size_of(ops);
+ n += self.block.unconditional_shallow_size_of(ops) +
+ self.block.read_with(guard).size_of(ops);
+ if let Some(ref rules) = self.rules {
+ n += rules.unconditional_shallow_size_of(ops) +
+ rules.read_with(guard).size_of(guard, ops)
+ }
+ n
+ }
+}
+
+impl ToCssWithGuard for StyleRule {
+ /// https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSStyleRule
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ use cssparser::ToCss;
+ // Step 1
+ self.selectors.to_css(dest)?;
+ dest.write_str(" {")?;
+
+ // Step 2
+ let declaration_block = self.block.read_with(guard);
+ let has_declarations = !declaration_block.declarations().is_empty();
+
+ // Step 3
+ if let Some(ref rules) = self.rules {
+ let rules = rules.read_with(guard);
+ // Step 6 (here because it's more convenient)
+ if !rules.is_empty() {
+ if has_declarations {
+ dest.write_str("\n ")?;
+ declaration_block.to_css(dest)?;
+ }
+ return rules.to_css_block_without_opening(guard, dest);
+ }
+ }
+
+ // Steps 4 & 5
+ if has_declarations {
+ dest.write_char(' ')?;
+ declaration_block.to_css(dest)?;
+ }
+ dest.write_str(" }")
+ }
+}
diff --git a/servo/components/style/stylesheets/stylesheet.rs b/servo/components/style/stylesheets/stylesheet.rs
new file mode 100644
index 0000000000..1604022871
--- /dev/null
+++ b/servo/components/style/stylesheets/stylesheet.rs
@@ -0,0 +1,566 @@
+/* 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/. */
+
+use crate::context::QuirksMode;
+use crate::error_reporting::{ContextualParseError, ParseErrorReporter};
+use crate::media_queries::{Device, MediaList};
+use crate::parser::ParserContext;
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard};
+use crate::stylesheets::loader::StylesheetLoader;
+use crate::stylesheets::rule_parser::{State, TopLevelRuleParser};
+use crate::stylesheets::rules_iterator::{EffectiveRules, EffectiveRulesIterator};
+use crate::stylesheets::rules_iterator::{NestedRuleIterationCondition, RulesIterator};
+use crate::stylesheets::{CssRule, CssRules, Origin, UrlExtraData};
+use crate::use_counters::UseCounters;
+use crate::{Namespace, Prefix};
+use cssparser::{Parser, ParserInput, StyleSheetParser};
+use fxhash::FxHashMap;
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use parking_lot::RwLock;
+use servo_arc::Arc;
+use std::sync::atomic::{AtomicBool, Ordering};
+use style_traits::ParsingMode;
+
+/// This structure holds the user-agent and user stylesheets.
+pub struct UserAgentStylesheets {
+ /// The lock used for user-agent stylesheets.
+ pub shared_lock: SharedRwLock,
+ /// The user or user agent stylesheets.
+ pub user_or_user_agent_stylesheets: Vec<DocumentStyleSheet>,
+ /// The quirks mode stylesheet.
+ pub quirks_mode_stylesheet: DocumentStyleSheet,
+}
+
+/// A set of namespaces applying to a given stylesheet.
+///
+/// The namespace id is used in gecko
+#[derive(Clone, Debug, Default, MallocSizeOf)]
+#[allow(missing_docs)]
+pub struct Namespaces {
+ pub default: Option<Namespace>,
+ pub prefixes: FxHashMap<Prefix, Namespace>,
+}
+
+/// The contents of a given stylesheet. This effectively maps to a
+/// StyleSheetInner in Gecko.
+#[derive(Debug)]
+pub struct StylesheetContents {
+ /// List of rules in the order they were found (important for
+ /// cascading order)
+ pub rules: Arc<Locked<CssRules>>,
+ /// The origin of this stylesheet.
+ pub origin: Origin,
+ /// The url data this stylesheet should use.
+ pub url_data: RwLock<UrlExtraData>,
+ /// The namespaces that apply to this stylesheet.
+ pub namespaces: RwLock<Namespaces>,
+ /// The quirks mode of this stylesheet.
+ pub quirks_mode: QuirksMode,
+ /// This stylesheet's source map URL.
+ pub source_map_url: RwLock<Option<String>>,
+ /// This stylesheet's source URL.
+ pub source_url: RwLock<Option<String>>,
+
+ /// We don't want to allow construction outside of this file, to guarantee
+ /// that all contents are created with Arc<>.
+ _forbid_construction: (),
+}
+
+impl StylesheetContents {
+ /// Parse a given CSS string, with a given url-data, origin, and
+ /// quirks mode.
+ pub fn from_str(
+ css: &str,
+ url_data: UrlExtraData,
+ origin: Origin,
+ shared_lock: &SharedRwLock,
+ stylesheet_loader: Option<&dyn StylesheetLoader>,
+ error_reporter: Option<&dyn ParseErrorReporter>,
+ quirks_mode: QuirksMode,
+ use_counters: Option<&UseCounters>,
+ allow_import_rules: AllowImportRules,
+ sanitization_data: Option<&mut SanitizationData>,
+ ) -> Arc<Self> {
+ let (namespaces, rules, source_map_url, source_url) = Stylesheet::parse_rules(
+ css,
+ &url_data,
+ origin,
+ &shared_lock,
+ stylesheet_loader,
+ error_reporter,
+ quirks_mode,
+ use_counters,
+ allow_import_rules,
+ sanitization_data,
+ );
+
+ Arc::new(Self {
+ rules: CssRules::new(rules, &shared_lock),
+ origin,
+ url_data: RwLock::new(url_data),
+ namespaces: RwLock::new(namespaces),
+ quirks_mode,
+ source_map_url: RwLock::new(source_map_url),
+ source_url: RwLock::new(source_url),
+ _forbid_construction: (),
+ })
+ }
+
+ /// Creates a new StylesheetContents with the specified pre-parsed rules,
+ /// origin, URL data, and quirks mode.
+ ///
+ /// Since the rules have already been parsed, and the intention is that
+ /// this function is used for read only User Agent style sheets, an empty
+ /// namespace map is used, and the source map and source URLs are set to
+ /// None.
+ ///
+ /// An empty namespace map should be fine, as it is only used for parsing,
+ /// not serialization of existing selectors. Since UA sheets are read only,
+ /// we should never need the namespace map.
+ pub fn from_shared_data(
+ rules: Arc<Locked<CssRules>>,
+ origin: Origin,
+ url_data: UrlExtraData,
+ quirks_mode: QuirksMode,
+ ) -> Arc<Self> {
+ debug_assert!(rules.is_static());
+ Arc::new(Self {
+ rules,
+ origin,
+ url_data: RwLock::new(url_data),
+ namespaces: RwLock::new(Namespaces::default()),
+ quirks_mode,
+ source_map_url: RwLock::new(None),
+ source_url: RwLock::new(None),
+ _forbid_construction: (),
+ })
+ }
+
+ /// Returns a reference to the list of rules.
+ #[inline]
+ pub fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] {
+ &self.rules.read_with(guard).0
+ }
+
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ if self.rules.is_static() {
+ return 0;
+ }
+ // Measurement of other fields may be added later.
+ self.rules.unconditional_shallow_size_of(ops) +
+ self.rules.read_with(guard).size_of(guard, ops)
+ }
+}
+
+impl DeepCloneWithLock for StylesheetContents {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ // Make a deep clone of the rules, using the new lock.
+ let rules = self
+ .rules
+ .read_with(guard)
+ .deep_clone_with_lock(lock, guard, params);
+
+ Self {
+ rules: Arc::new(lock.wrap(rules)),
+ quirks_mode: self.quirks_mode,
+ origin: self.origin,
+ url_data: RwLock::new((*self.url_data.read()).clone()),
+ namespaces: RwLock::new((*self.namespaces.read()).clone()),
+ source_map_url: RwLock::new((*self.source_map_url.read()).clone()),
+ source_url: RwLock::new((*self.source_url.read()).clone()),
+ _forbid_construction: (),
+ }
+ }
+}
+
+/// The structure servo uses to represent a stylesheet.
+#[derive(Debug)]
+pub struct Stylesheet {
+ /// The contents of this stylesheet.
+ pub contents: Arc<StylesheetContents>,
+ /// The lock used for objects inside this stylesheet
+ pub shared_lock: SharedRwLock,
+ /// List of media associated with the Stylesheet.
+ pub media: Arc<Locked<MediaList>>,
+ /// Whether this stylesheet should be disabled.
+ pub disabled: AtomicBool,
+}
+
+/// A trait to represent a given stylesheet in a document.
+pub trait StylesheetInDocument: ::std::fmt::Debug {
+ /// Get whether this stylesheet is enabled.
+ fn enabled(&self) -> bool;
+
+ /// Get the media associated with this stylesheet.
+ fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList>;
+
+ /// Returns a reference to the list of rules in this stylesheet.
+ fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] {
+ self.contents().rules(guard)
+ }
+
+ /// Returns a reference to the contents of the stylesheet.
+ fn contents(&self) -> &StylesheetContents;
+
+ /// Return an iterator using the condition `C`.
+ #[inline]
+ fn iter_rules<'a, 'b, C>(
+ &'a self,
+ device: &'a Device,
+ guard: &'a SharedRwLockReadGuard<'b>,
+ ) -> RulesIterator<'a, 'b, C>
+ where
+ C: NestedRuleIterationCondition,
+ {
+ let contents = self.contents();
+ RulesIterator::new(
+ device,
+ contents.quirks_mode,
+ guard,
+ contents.rules(guard).iter(),
+ )
+ }
+
+ /// Returns whether the style-sheet applies for the current device.
+ fn is_effective_for_device(&self, device: &Device, guard: &SharedRwLockReadGuard) -> bool {
+ match self.media(guard) {
+ Some(medialist) => medialist.evaluate(device, self.contents().quirks_mode),
+ None => true,
+ }
+ }
+
+ /// Return an iterator over the effective rules within the style-sheet, as
+ /// according to the supplied `Device`.
+ #[inline]
+ fn effective_rules<'a, 'b>(
+ &'a self,
+ device: &'a Device,
+ guard: &'a SharedRwLockReadGuard<'b>,
+ ) -> EffectiveRulesIterator<'a, 'b> {
+ self.iter_rules::<EffectiveRules>(device, guard)
+ }
+}
+
+impl StylesheetInDocument for Stylesheet {
+ fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
+ Some(self.media.read_with(guard))
+ }
+
+ fn enabled(&self) -> bool {
+ !self.disabled()
+ }
+
+ #[inline]
+ fn contents(&self) -> &StylesheetContents {
+ &self.contents
+ }
+}
+
+/// A simple wrapper over an `Arc<Stylesheet>`, with pointer comparison, and
+/// suitable for its use in a `StylesheetSet`.
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct DocumentStyleSheet(
+ #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] pub Arc<Stylesheet>,
+);
+
+impl PartialEq for DocumentStyleSheet {
+ fn eq(&self, other: &Self) -> bool {
+ Arc::ptr_eq(&self.0, &other.0)
+ }
+}
+
+impl StylesheetInDocument for DocumentStyleSheet {
+ fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
+ self.0.media(guard)
+ }
+
+ fn enabled(&self) -> bool {
+ self.0.enabled()
+ }
+
+ #[inline]
+ fn contents(&self) -> &StylesheetContents {
+ self.0.contents()
+ }
+}
+
+/// The kind of sanitization to use when parsing a stylesheet.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum SanitizationKind {
+ /// Perform no sanitization.
+ None,
+ /// Allow only @font-face, style rules, and @namespace.
+ Standard,
+ /// Allow everything but conditional rules.
+ NoConditionalRules,
+}
+
+/// Whether @import rules are allowed.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum AllowImportRules {
+ /// @import rules will be parsed.
+ Yes,
+ /// @import rules will not be parsed.
+ No,
+}
+
+impl SanitizationKind {
+ fn allows(self, rule: &CssRule) -> bool {
+ debug_assert_ne!(self, SanitizationKind::None);
+ // NOTE(emilio): If this becomes more complex (not filtering just by
+ // top-level rules), we should thread all the data through nested rules
+ // and such. But this doesn't seem necessary at the moment.
+ let is_standard = matches!(self, SanitizationKind::Standard);
+ match *rule {
+ CssRule::Document(..) |
+ CssRule::Media(..) |
+ CssRule::Supports(..) |
+ CssRule::Import(..) |
+ CssRule::Container(..) |
+ // TODO(emilio): Perhaps Layer should not be always sanitized? But
+ // we sanitize @media and co, so this seems safer for now.
+ CssRule::LayerStatement(..) |
+ CssRule::LayerBlock(..) => false,
+
+ CssRule::FontFace(..) | CssRule::Namespace(..) | CssRule::Style(..) => true,
+
+ CssRule::Keyframes(..) |
+ CssRule::Page(..) |
+ CssRule::Margin(..) |
+ CssRule::Property(..) |
+ CssRule::FontFeatureValues(..) |
+ CssRule::FontPaletteValues(..) |
+ CssRule::CounterStyle(..) => !is_standard,
+ }
+ }
+}
+
+/// A struct to hold the data relevant to style sheet sanitization.
+#[derive(Debug)]
+pub struct SanitizationData {
+ kind: SanitizationKind,
+ output: String,
+}
+
+impl SanitizationData {
+ /// Create a new input for sanitization.
+ #[inline]
+ pub fn new(kind: SanitizationKind) -> Option<Self> {
+ if matches!(kind, SanitizationKind::None) {
+ return None;
+ }
+ Some(Self {
+ kind,
+ output: String::new(),
+ })
+ }
+
+ /// Take the sanitized output.
+ #[inline]
+ pub fn take(self) -> String {
+ self.output
+ }
+}
+
+impl Stylesheet {
+ /// Updates an empty stylesheet from a given string of text.
+ pub fn update_from_str(
+ existing: &Stylesheet,
+ css: &str,
+ url_data: UrlExtraData,
+ stylesheet_loader: Option<&dyn StylesheetLoader>,
+ error_reporter: Option<&dyn ParseErrorReporter>,
+ allow_import_rules: AllowImportRules,
+ ) {
+ // FIXME: Consider adding use counters to Servo?
+ let (namespaces, rules, source_map_url, source_url) = Self::parse_rules(
+ css,
+ &url_data,
+ existing.contents.origin,
+ &existing.shared_lock,
+ stylesheet_loader,
+ error_reporter,
+ existing.contents.quirks_mode,
+ /* use_counters = */ None,
+ allow_import_rules,
+ /* sanitization_data = */ None,
+ );
+
+ *existing.contents.url_data.write() = url_data;
+ *existing.contents.namespaces.write() = namespaces;
+
+ // Acquire the lock *after* parsing, to minimize the exclusive section.
+ let mut guard = existing.shared_lock.write();
+ *existing.contents.rules.write_with(&mut guard) = CssRules(rules);
+ *existing.contents.source_map_url.write() = source_map_url;
+ *existing.contents.source_url.write() = source_url;
+ }
+
+ fn parse_rules(
+ css: &str,
+ url_data: &UrlExtraData,
+ origin: Origin,
+ shared_lock: &SharedRwLock,
+ stylesheet_loader: Option<&dyn StylesheetLoader>,
+ error_reporter: Option<&dyn ParseErrorReporter>,
+ quirks_mode: QuirksMode,
+ use_counters: Option<&UseCounters>,
+ allow_import_rules: AllowImportRules,
+ mut sanitization_data: Option<&mut SanitizationData>,
+ ) -> (Namespaces, Vec<CssRule>, Option<String>, Option<String>) {
+ let mut input = ParserInput::new(css);
+ let mut input = Parser::new(&mut input);
+
+ let context = ParserContext::new(
+ origin,
+ url_data,
+ None,
+ ParsingMode::DEFAULT,
+ quirks_mode,
+ /* namespaces = */ Default::default(),
+ error_reporter,
+ use_counters,
+ );
+
+ let mut rule_parser = TopLevelRuleParser {
+ shared_lock,
+ loader: stylesheet_loader,
+ context,
+ state: State::Start,
+ dom_error: None,
+ insert_rule_context: None,
+ allow_import_rules,
+ declaration_parser_state: Default::default(),
+ error_reporting_state: Default::default(),
+ rules: Vec::new(),
+ };
+
+ {
+ let mut iter = StyleSheetParser::new(&mut input, &mut rule_parser);
+ while let Some(result) = iter.next() {
+ match result {
+ Ok(rule_start) => {
+ // TODO(emilio, nesting): sanitize nested CSS rules, probably?
+ if let Some(ref mut data) = sanitization_data {
+ if let Some(ref rule) = iter.parser.rules.last() {
+ if !data.kind.allows(rule) {
+ iter.parser.rules.pop();
+ continue;
+ }
+ }
+ let end = iter.input.position().byte_index();
+ data.output.push_str(&css[rule_start.byte_index()..end]);
+ }
+ },
+ Err((error, slice)) => {
+ let location = error.location;
+ let error = ContextualParseError::InvalidRule(slice, error);
+ iter.parser.context.log_css_error(location, error);
+ },
+ }
+ }
+ }
+
+ let source_map_url = input.current_source_map_url().map(String::from);
+ let source_url = input.current_source_url().map(String::from);
+ (
+ rule_parser.context.namespaces.into_owned(),
+ rule_parser.rules,
+ source_map_url,
+ source_url,
+ )
+ }
+
+ /// Creates an empty stylesheet and parses it with a given base url, origin
+ /// and media.
+ ///
+ /// Effectively creates a new stylesheet and forwards the hard work to
+ /// `Stylesheet::update_from_str`.
+ pub fn from_str(
+ css: &str,
+ url_data: UrlExtraData,
+ origin: Origin,
+ media: Arc<Locked<MediaList>>,
+ shared_lock: SharedRwLock,
+ stylesheet_loader: Option<&dyn StylesheetLoader>,
+ error_reporter: Option<&dyn ParseErrorReporter>,
+ quirks_mode: QuirksMode,
+ allow_import_rules: AllowImportRules,
+ ) -> Self {
+ // FIXME: Consider adding use counters to Servo?
+ let contents = StylesheetContents::from_str(
+ css,
+ url_data,
+ origin,
+ &shared_lock,
+ stylesheet_loader,
+ error_reporter,
+ quirks_mode,
+ /* use_counters = */ None,
+ allow_import_rules,
+ /* sanitized_output = */ None,
+ );
+
+ Stylesheet {
+ contents,
+ shared_lock,
+ media,
+ disabled: AtomicBool::new(false),
+ }
+ }
+
+ /// Returns whether the stylesheet has been explicitly disabled through the
+ /// CSSOM.
+ pub fn disabled(&self) -> bool {
+ self.disabled.load(Ordering::SeqCst)
+ }
+
+ /// Records that the stylesheet has been explicitly disabled through the
+ /// CSSOM.
+ ///
+ /// Returns whether the the call resulted in a change in disabled state.
+ ///
+ /// Disabled stylesheets remain in the document, but their rules are not
+ /// added to the Stylist.
+ pub fn set_disabled(&self, disabled: bool) -> bool {
+ self.disabled.swap(disabled, Ordering::SeqCst) != disabled
+ }
+}
+
+#[cfg(feature = "servo")]
+impl Clone for Stylesheet {
+ fn clone(&self) -> Self {
+ // Create a new lock for our clone.
+ let lock = self.shared_lock.clone();
+ let guard = self.shared_lock.read();
+
+ // Make a deep clone of the media, using the new lock.
+ let media = self.media.read_with(&guard).clone();
+ let media = Arc::new(lock.wrap(media));
+ let contents = Arc::new(self.contents.deep_clone_with_lock(
+ &lock,
+ &guard,
+ &DeepCloneParams,
+ ));
+
+ Stylesheet {
+ contents,
+ media,
+ shared_lock: lock,
+ disabled: AtomicBool::new(self.disabled.load(Ordering::SeqCst)),
+ }
+ }
+}
diff --git a/servo/components/style/stylesheets/supports_rule.rs b/servo/components/style/stylesheets/supports_rule.rs
new file mode 100644
index 0000000000..a3ffe5a2f5
--- /dev/null
+++ b/servo/components/style/stylesheets/supports_rule.rs
@@ -0,0 +1,397 @@
+/* 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/. */
+
+//! [@supports rules](https://drafts.csswg.org/css-conditional-3/#at-supports)
+
+use crate::font_face::{FontFaceSourceFormatKeyword, FontFaceSourceTechFlags};
+use crate::parser::ParserContext;
+use crate::properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
+use crate::selector_parser::{SelectorImpl, SelectorParser};
+use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
+use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
+use crate::str::CssStringWriter;
+use crate::stylesheets::{CssRuleType, CssRules};
+use cssparser::parse_important;
+use cssparser::{Delimiter, Parser, SourceLocation, Token};
+use cssparser::{ParseError as CssParseError, ParserInput};
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use selectors::parser::{Selector, SelectorParseErrorKind};
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+use std::str;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// An [`@supports`][supports] rule.
+///
+/// [supports]: https://drafts.csswg.org/css-conditional-3/#at-supports
+#[derive(Debug, ToShmem)]
+pub struct SupportsRule {
+ /// The parsed condition
+ pub condition: SupportsCondition,
+ /// Child rules
+ pub rules: Arc<Locked<CssRules>>,
+ /// The result of evaluating the condition
+ pub enabled: bool,
+ /// The line and column of the rule's source code.
+ pub source_location: SourceLocation,
+}
+
+impl SupportsRule {
+ /// Measure heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
+ // Measurement of other fields may be added later.
+ self.rules.unconditional_shallow_size_of(ops) +
+ self.rules.read_with(guard).size_of(guard, ops)
+ }
+}
+
+impl ToCssWithGuard for SupportsRule {
+ fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
+ dest.write_str("@supports ")?;
+ self.condition.to_css(&mut CssWriter::new(dest))?;
+ self.rules.read_with(guard).to_css_block(guard, dest)
+ }
+}
+
+impl DeepCloneWithLock for SupportsRule {
+ fn deep_clone_with_lock(
+ &self,
+ lock: &SharedRwLock,
+ guard: &SharedRwLockReadGuard,
+ params: &DeepCloneParams,
+ ) -> Self {
+ let rules = self.rules.read_with(guard);
+ SupportsRule {
+ condition: self.condition.clone(),
+ rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))),
+ enabled: self.enabled,
+ source_location: self.source_location.clone(),
+ }
+ }
+}
+
+/// An @supports condition
+///
+/// <https://drafts.csswg.org/css-conditional-3/#at-supports>
+#[derive(Clone, Debug, ToShmem)]
+pub enum SupportsCondition {
+ /// `not (condition)`
+ Not(Box<SupportsCondition>),
+ /// `(condition)`
+ Parenthesized(Box<SupportsCondition>),
+ /// `(condition) and (condition) and (condition) ..`
+ And(Vec<SupportsCondition>),
+ /// `(condition) or (condition) or (condition) ..`
+ Or(Vec<SupportsCondition>),
+ /// `property-ident: value` (value can be any tokens)
+ Declaration(Declaration),
+ /// A `selector()` function.
+ Selector(RawSelector),
+ /// `font-format(<font-format>)`
+ FontFormat(FontFaceSourceFormatKeyword),
+ /// `font-tech(<font-tech>)`
+ FontTech(FontFaceSourceTechFlags),
+ /// `(any tokens)` or `func(any tokens)`
+ FutureSyntax(String),
+}
+
+impl SupportsCondition {
+ /// Parse a condition
+ ///
+ /// <https://drafts.csswg.org/css-conditional/#supports_condition>
+ pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() {
+ let inner = SupportsCondition::parse_in_parens(input)?;
+ return Ok(SupportsCondition::Not(Box::new(inner)));
+ }
+
+ let in_parens = SupportsCondition::parse_in_parens(input)?;
+
+ let location = input.current_source_location();
+ let (keyword, wrapper) = match input.next() {
+ // End of input
+ Err(..) => return Ok(in_parens),
+ Ok(&Token::Ident(ref ident)) => {
+ match_ignore_ascii_case! { &ident,
+ "and" => ("and", SupportsCondition::And as fn(_) -> _),
+ "or" => ("or", SupportsCondition::Or as fn(_) -> _),
+ _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())))
+ }
+ },
+ Ok(t) => return Err(location.new_unexpected_token_error(t.clone())),
+ };
+
+ let mut conditions = Vec::with_capacity(2);
+ conditions.push(in_parens);
+ loop {
+ conditions.push(SupportsCondition::parse_in_parens(input)?);
+ if input
+ .try_parse(|input| input.expect_ident_matching(keyword))
+ .is_err()
+ {
+ // Did not find the expected keyword.
+ // If we found some other token, it will be rejected by
+ // `Parser::parse_entirely` somewhere up the stack.
+ return Ok(wrapper(conditions));
+ }
+ }
+ }
+
+ /// Parses a functional supports condition.
+ fn parse_functional<'i, 't>(
+ function: &str,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ match_ignore_ascii_case! { function,
+ "selector" => {
+ let pos = input.position();
+ consume_any_value(input)?;
+ Ok(SupportsCondition::Selector(RawSelector(
+ input.slice_from(pos).to_owned()
+ )))
+ },
+ "font-format" if static_prefs::pref!("layout.css.font-tech.enabled") => {
+ let kw = FontFaceSourceFormatKeyword::parse(input)?;
+ Ok(SupportsCondition::FontFormat(kw))
+ },
+ "font-tech" if static_prefs::pref!("layout.css.font-tech.enabled") => {
+ let flag = FontFaceSourceTechFlags::parse_one(input)?;
+ Ok(SupportsCondition::FontTech(flag))
+ },
+ _ => {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ }
+ }
+
+ /// Parses an `@import` condition as per
+ /// https://drafts.csswg.org/css-cascade-5/#typedef-import-conditions
+ pub fn parse_for_import<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("supports")?;
+ input.parse_nested_block(parse_condition_or_declaration)
+ }
+
+ /// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens>
+ fn parse_in_parens<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ // Whitespace is normally taken care of in `Parser::next`, but we want to not include it in
+ // `pos` for the SupportsCondition::FutureSyntax cases.
+ input.skip_whitespace();
+ let pos = input.position();
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::ParenthesisBlock => {
+ let nested = input
+ .try_parse(|input| input.parse_nested_block(parse_condition_or_declaration));
+ if let Ok(nested) = nested {
+ return Ok(Self::Parenthesized(Box::new(nested)));
+ }
+ },
+ Token::Function(ref ident) => {
+ let ident = ident.clone();
+ let nested = input.try_parse(|input| {
+ input.parse_nested_block(|input| {
+ SupportsCondition::parse_functional(&ident, input)
+ })
+ });
+ if nested.is_ok() {
+ return nested;
+ }
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ input.parse_nested_block(consume_any_value)?;
+ Ok(SupportsCondition::FutureSyntax(
+ input.slice_from(pos).to_owned(),
+ ))
+ }
+
+ /// Evaluate a supports condition
+ pub fn eval(&self, cx: &ParserContext) -> bool {
+ match *self {
+ SupportsCondition::Not(ref cond) => !cond.eval(cx),
+ SupportsCondition::Parenthesized(ref cond) => cond.eval(cx),
+ SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx)),
+ SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx)),
+ SupportsCondition::Declaration(ref decl) => decl.eval(cx),
+ SupportsCondition::Selector(ref selector) => selector.eval(cx),
+ SupportsCondition::FontFormat(ref format) => eval_font_format(format),
+ SupportsCondition::FontTech(ref tech) => eval_font_tech(tech),
+ SupportsCondition::FutureSyntax(_) => false,
+ }
+ }
+}
+
+fn eval_font_format(kw: &FontFaceSourceFormatKeyword) -> bool {
+ use crate::gecko_bindings::bindings;
+ unsafe { bindings::Gecko_IsFontFormatSupported(*kw) }
+}
+
+fn eval_font_tech(flag: &FontFaceSourceTechFlags) -> bool {
+ use crate::gecko_bindings::bindings;
+ unsafe { bindings::Gecko_IsFontTechSupported(*flag) }
+}
+
+/// supports_condition | declaration
+/// <https://drafts.csswg.org/css-conditional/#dom-css-supports-conditiontext-conditiontext>
+pub fn parse_condition_or_declaration<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<SupportsCondition, ParseError<'i>> {
+ if let Ok(condition) = input.try_parse(SupportsCondition::parse) {
+ Ok(condition)
+ } else {
+ Declaration::parse(input).map(SupportsCondition::Declaration)
+ }
+}
+
+impl ToCss for SupportsCondition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ SupportsCondition::Not(ref cond) => {
+ dest.write_str("not ")?;
+ cond.to_css(dest)
+ },
+ SupportsCondition::Parenthesized(ref cond) => {
+ dest.write_char('(')?;
+ cond.to_css(dest)?;
+ dest.write_char(')')
+ },
+ SupportsCondition::And(ref vec) => {
+ let mut first = true;
+ for cond in vec {
+ if !first {
+ dest.write_str(" and ")?;
+ }
+ first = false;
+ cond.to_css(dest)?;
+ }
+ Ok(())
+ },
+ SupportsCondition::Or(ref vec) => {
+ let mut first = true;
+ for cond in vec {
+ if !first {
+ dest.write_str(" or ")?;
+ }
+ first = false;
+ cond.to_css(dest)?;
+ }
+ Ok(())
+ },
+ SupportsCondition::Declaration(ref decl) => decl.to_css(dest),
+ SupportsCondition::Selector(ref selector) => {
+ dest.write_str("selector(")?;
+ selector.to_css(dest)?;
+ dest.write_char(')')
+ },
+ SupportsCondition::FontFormat(ref kw) => {
+ dest.write_str("font-format(")?;
+ kw.to_css(dest)?;
+ dest.write_char(')')
+ },
+ SupportsCondition::FontTech(ref flag) => {
+ dest.write_str("font-tech(")?;
+ flag.to_css(dest)?;
+ dest.write_char(')')
+ },
+ SupportsCondition::FutureSyntax(ref s) => dest.write_str(&s),
+ }
+ }
+}
+
+#[derive(Clone, Debug, ToShmem)]
+/// A possibly-invalid CSS selector.
+pub struct RawSelector(pub String);
+
+impl ToCss for RawSelector {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(&self.0)
+ }
+}
+
+impl RawSelector {
+ /// Tries to evaluate a `selector()` function.
+ pub fn eval(&self, context: &ParserContext) -> bool {
+ let mut input = ParserInput::new(&self.0);
+ let mut input = Parser::new(&mut input);
+ input
+ .parse_entirely(|input| -> Result<(), CssParseError<()>> {
+ let parser = SelectorParser {
+ namespaces: &context.namespaces,
+ stylesheet_origin: context.stylesheet_origin,
+ url_data: context.url_data,
+ for_supports_rule: true,
+ };
+
+ Selector::<SelectorImpl>::parse(&parser, input)
+ .map_err(|_| input.new_custom_error(()))?;
+
+ Ok(())
+ })
+ .is_ok()
+ }
+}
+
+#[derive(Clone, Debug, ToShmem)]
+/// A possibly-invalid property declaration
+pub struct Declaration(pub String);
+
+impl ToCss for Declaration {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(&self.0)
+ }
+}
+
+/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value>
+fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> {
+ input.expect_no_error_token().map_err(|err| err.into())
+}
+
+impl Declaration {
+ /// Parse a declaration
+ pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Declaration, ParseError<'i>> {
+ let pos = input.position();
+ input.expect_ident()?;
+ input.expect_colon()?;
+ consume_any_value(input)?;
+ Ok(Declaration(input.slice_from(pos).to_owned()))
+ }
+
+ /// Determine if a declaration parses
+ ///
+ /// <https://drafts.csswg.org/css-conditional-3/#support-definition>
+ pub fn eval(&self, context: &ParserContext) -> bool {
+ debug_assert!(context.rule_types().contains(CssRuleType::Style));
+
+ let mut input = ParserInput::new(&self.0);
+ let mut input = Parser::new(&mut input);
+ input
+ .parse_entirely(|input| -> Result<(), CssParseError<()>> {
+ let prop = input.expect_ident_cloned().unwrap();
+ input.expect_colon().unwrap();
+
+ let id =
+ PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?;
+
+ let mut declarations = SourcePropertyDeclaration::default();
+ input.parse_until_before(Delimiter::Bang, |input| {
+ PropertyDeclaration::parse_into(&mut declarations, id, &context, input)
+ .map_err(|_| input.new_custom_error(()))
+ })?;
+ let _ = input.try_parse(parse_important);
+ Ok(())
+ })
+ .is_ok()
+ }
+}
diff --git a/servo/components/style/stylist.rs b/servo/components/style/stylist.rs
new file mode 100644
index 0000000000..cc1cb75689
--- /dev/null
+++ b/servo/components/style/stylist.rs
@@ -0,0 +1,3503 @@
+/* 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/. */
+
+//! Selector matching.
+
+use crate::applicable_declarations::{
+ ApplicableDeclarationBlock, ApplicableDeclarationList, CascadePriority,
+};
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::context::{CascadeInputs, QuirksMode};
+use crate::custom_properties::ComputedCustomProperties;
+use crate::dom::TElement;
+#[cfg(feature = "gecko")]
+use crate::gecko_bindings::structs::{ServoStyleSetSizes, StyleRuleInclusion};
+use crate::invalidation::element::invalidation_map::{
+ note_selector_for_invalidation, InvalidationMap, RelativeSelectorInvalidationMap,
+};
+use crate::invalidation::media_queries::{
+ EffectiveMediaQueryResults, MediaListKey, ToMediaListKey,
+};
+use crate::invalidation::stylesheets::RuleChangeKind;
+use crate::media_queries::Device;
+use crate::properties::{self, CascadeMode, ComputedValues, FirstLineReparenting};
+use crate::properties::{AnimationDeclarations, PropertyDeclarationBlock};
+use crate::properties_and_values::registry::{
+ PropertyRegistration, PropertyRegistrationData, ScriptRegistry as CustomPropertyScriptRegistry,
+};
+use crate::rule_cache::{RuleCache, RuleCacheConditions};
+use crate::rule_collector::RuleCollector;
+use crate::rule_tree::{CascadeLevel, RuleTree, StrongRuleNode, StyleSource};
+use crate::sharing::RevalidationResult;
+use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet, SelectorMap, SelectorMapEntry};
+use crate::selector_parser::{PerPseudoElementMap, PseudoElement, SelectorImpl, SnapshotMap};
+use crate::shared_lock::{Locked, SharedRwLockReadGuard, StylesheetGuards};
+use crate::stylesheet_set::{DataValidity, DocumentStylesheetSet, SheetRebuildKind};
+use crate::stylesheet_set::{DocumentStylesheetFlusher, SheetCollectionFlusher};
+use crate::stylesheets::container_rule::ContainerCondition;
+use crate::stylesheets::import_rule::ImportLayer;
+use crate::stylesheets::keyframes_rule::KeyframesAnimation;
+use crate::stylesheets::layer_rule::{LayerName, LayerOrder};
+#[cfg(feature = "gecko")]
+use crate::stylesheets::{
+ CounterStyleRule, FontFaceRule, FontFeatureValuesRule, FontPaletteValuesRule,
+};
+use crate::stylesheets::{
+ CssRule, EffectiveRulesIterator, Origin, OriginSet, PagePseudoClassFlags, PageRule, PerOrigin,
+ PerOriginIter,
+};
+use crate::stylesheets::{StyleRule, StylesheetContents, StylesheetInDocument};
+use crate::values::computed;
+use crate::AllocErr;
+use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded, WeakAtom};
+use dom::{DocumentState, ElementState};
+use fxhash::FxHashMap;
+use malloc_size_of::MallocSizeOf;
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocShallowSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
+use selectors::attr::{CaseSensitivity, NamespaceConstraint};
+use selectors::bloom::BloomFilter;
+use selectors::matching::{
+ matches_selector, MatchingContext, MatchingMode, NeedsSelectorFlags, SelectorCaches,
+};
+use selectors::matching::{MatchingForInvalidation, VisitedHandlingMode};
+use selectors::parser::{
+ AncestorHashes, Combinator, Component, Selector, SelectorIter, SelectorList,
+};
+use selectors::visitor::{SelectorListKind, SelectorVisitor};
+use servo_arc::{Arc, ArcBorrow};
+use smallvec::SmallVec;
+use std::cmp::Ordering;
+use std::hash::{Hash, Hasher};
+use std::sync::Mutex;
+use std::{mem, ops};
+
+/// The type of the stylesheets that the stylist contains.
+#[cfg(feature = "servo")]
+pub type StylistSheet = crate::stylesheets::DocumentStyleSheet;
+
+/// The type of the stylesheets that the stylist contains.
+#[cfg(feature = "gecko")]
+pub type StylistSheet = crate::gecko::data::GeckoStyleSheet;
+
+#[derive(Debug, Clone)]
+struct StylesheetContentsPtr(Arc<StylesheetContents>);
+
+impl PartialEq for StylesheetContentsPtr {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ Arc::ptr_eq(&self.0, &other.0)
+ }
+}
+
+impl Eq for StylesheetContentsPtr {}
+
+impl Hash for StylesheetContentsPtr {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ let contents: &StylesheetContents = &*self.0;
+ (contents as *const StylesheetContents).hash(state)
+ }
+}
+
+type StyleSheetContentList = Vec<StylesheetContentsPtr>;
+
+/// A key in the cascade data cache.
+#[derive(Debug, Hash, Default, PartialEq, Eq)]
+struct CascadeDataCacheKey {
+ media_query_results: Vec<MediaListKey>,
+ contents: StyleSheetContentList,
+}
+
+unsafe impl Send for CascadeDataCacheKey {}
+unsafe impl Sync for CascadeDataCacheKey {}
+
+trait CascadeDataCacheEntry: Sized {
+ /// Returns a reference to the cascade data.
+ fn cascade_data(&self) -> &CascadeData;
+ /// Rebuilds the cascade data for the new stylesheet collection. The
+ /// collection is guaranteed to be dirty.
+ fn rebuild<S>(
+ device: &Device,
+ quirks_mode: QuirksMode,
+ collection: SheetCollectionFlusher<S>,
+ guard: &SharedRwLockReadGuard,
+ old_entry: &Self,
+ ) -> Result<Arc<Self>, AllocErr>
+ where
+ S: StylesheetInDocument + PartialEq + 'static;
+ /// Measures heap memory usage.
+ #[cfg(feature = "gecko")]
+ fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes);
+}
+
+struct CascadeDataCache<Entry> {
+ entries: FxHashMap<CascadeDataCacheKey, Arc<Entry>>,
+}
+
+impl<Entry> CascadeDataCache<Entry>
+where
+ Entry: CascadeDataCacheEntry,
+{
+ fn new() -> Self {
+ Self {
+ entries: Default::default(),
+ }
+ }
+
+ fn len(&self) -> usize {
+ self.entries.len()
+ }
+
+ // FIXME(emilio): This may need to be keyed on quirks-mode too, though for
+ // UA sheets there aren't class / id selectors on those sheets, usually, so
+ // it's probably ok... For the other cache the quirks mode shouldn't differ
+ // so also should be fine.
+ fn lookup<'a, S>(
+ &'a mut self,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ collection: SheetCollectionFlusher<S>,
+ guard: &SharedRwLockReadGuard,
+ old_entry: &Entry,
+ ) -> Result<Option<Arc<Entry>>, AllocErr>
+ where
+ S: StylesheetInDocument + PartialEq + 'static,
+ {
+ use std::collections::hash_map::Entry as HashMapEntry;
+ debug!("StyleSheetCache::lookup({})", self.len());
+
+ if !collection.dirty() {
+ return Ok(None);
+ }
+
+ let mut key = CascadeDataCacheKey::default();
+ for sheet in collection.sheets() {
+ CascadeData::collect_applicable_media_query_results_into(
+ device,
+ sheet,
+ guard,
+ &mut key.media_query_results,
+ &mut key.contents,
+ )
+ }
+
+ let new_entry;
+ match self.entries.entry(key) {
+ HashMapEntry::Vacant(e) => {
+ debug!("> Picking the slow path (not in the cache)");
+ new_entry = Entry::rebuild(device, quirks_mode, collection, guard, old_entry)?;
+ e.insert(new_entry.clone());
+ },
+ HashMapEntry::Occupied(mut e) => {
+ // Avoid reusing our old entry (this can happen if we get
+ // invalidated due to CSSOM mutations and our old stylesheet
+ // contents were already unique, for example).
+ if !std::ptr::eq(&**e.get(), old_entry) {
+ if log_enabled!(log::Level::Debug) {
+ debug!("cache hit for:");
+ for sheet in collection.sheets() {
+ debug!(" > {:?}", sheet);
+ }
+ }
+ // The line below ensures the "committed" bit is updated
+ // properly.
+ collection.each(|_, _| true);
+ return Ok(Some(e.get().clone()));
+ }
+
+ debug!("> Picking the slow path due to same entry as old");
+ new_entry = Entry::rebuild(device, quirks_mode, collection, guard, old_entry)?;
+ e.insert(new_entry.clone());
+ },
+ }
+
+ Ok(Some(new_entry))
+ }
+
+ /// Returns all the cascade datas that are not being used (that is, that are
+ /// held alive just by this cache).
+ ///
+ /// We return them instead of dropping in place because some of them may
+ /// keep alive some other documents (like the SVG documents kept alive by
+ /// URL references), and thus we don't want to drop them while locking the
+ /// cache to not deadlock.
+ fn take_unused(&mut self) -> SmallVec<[Arc<Entry>; 3]> {
+ let mut unused = SmallVec::new();
+ self.entries.retain(|_key, value| {
+ // is_unique() returns false for static references, but we never
+ // have static references to UserAgentCascadeDatas. If we did, it
+ // may not make sense to put them in the cache in the first place.
+ if !value.is_unique() {
+ return true;
+ }
+ unused.push(value.clone());
+ false
+ });
+ unused
+ }
+
+ fn take_all(&mut self) -> FxHashMap<CascadeDataCacheKey, Arc<Entry>> {
+ mem::take(&mut self.entries)
+ }
+
+ #[cfg(feature = "gecko")]
+ fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) {
+ sizes.mOther += self.entries.shallow_size_of(ops);
+ for (_key, arc) in self.entries.iter() {
+ // These are primary Arc references that can be measured
+ // unconditionally.
+ sizes.mOther += arc.unconditional_shallow_size_of(ops);
+ arc.add_size_of(ops, sizes);
+ }
+ }
+}
+
+/// Measure heap usage of UA_CASCADE_DATA_CACHE.
+#[cfg(feature = "gecko")]
+pub fn add_size_of_ua_cache(ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) {
+ UA_CASCADE_DATA_CACHE
+ .lock()
+ .unwrap()
+ .add_size_of(ops, sizes);
+}
+
+lazy_static! {
+ /// A cache of computed user-agent data, to be shared across documents.
+ static ref UA_CASCADE_DATA_CACHE: Mutex<UserAgentCascadeDataCache> =
+ Mutex::new(UserAgentCascadeDataCache::new());
+}
+
+impl CascadeDataCacheEntry for UserAgentCascadeData {
+ fn cascade_data(&self) -> &CascadeData {
+ &self.cascade_data
+ }
+
+ fn rebuild<S>(
+ device: &Device,
+ quirks_mode: QuirksMode,
+ collection: SheetCollectionFlusher<S>,
+ guard: &SharedRwLockReadGuard,
+ _old: &Self,
+ ) -> Result<Arc<Self>, AllocErr>
+ where
+ S: StylesheetInDocument + PartialEq + 'static,
+ {
+ // TODO: Maybe we should support incremental rebuilds, though they seem
+ // uncommon and rebuild() doesn't deal with
+ // precomputed_pseudo_element_decls for now so...
+ let mut new_data = Self {
+ cascade_data: CascadeData::new(),
+ precomputed_pseudo_element_decls: PrecomputedPseudoElementDeclarations::default(),
+ };
+
+ for sheet in collection.sheets() {
+ new_data.cascade_data.add_stylesheet(
+ device,
+ quirks_mode,
+ sheet,
+ guard,
+ SheetRebuildKind::Full,
+ Some(&mut new_data.precomputed_pseudo_element_decls),
+ )?;
+ }
+
+ new_data.cascade_data.did_finish_rebuild();
+
+ Ok(Arc::new(new_data))
+ }
+
+ #[cfg(feature = "gecko")]
+ fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) {
+ self.cascade_data.add_size_of(ops, sizes);
+ sizes.mPrecomputedPseudos += self.precomputed_pseudo_element_decls.size_of(ops);
+ }
+}
+
+type UserAgentCascadeDataCache = CascadeDataCache<UserAgentCascadeData>;
+
+type PrecomputedPseudoElementDeclarations = PerPseudoElementMap<Vec<ApplicableDeclarationBlock>>;
+
+#[derive(Default)]
+struct UserAgentCascadeData {
+ cascade_data: CascadeData,
+
+ /// Applicable declarations for a given non-eagerly cascaded pseudo-element.
+ ///
+ /// These are eagerly computed once, and then used to resolve the new
+ /// computed values on the fly on layout.
+ ///
+ /// These are only filled from UA stylesheets.
+ precomputed_pseudo_element_decls: PrecomputedPseudoElementDeclarations,
+}
+
+lazy_static! {
+ /// The empty UA cascade data for un-filled stylists.
+ static ref EMPTY_UA_CASCADE_DATA: Arc<UserAgentCascadeData> = {
+ let arc = Arc::new(UserAgentCascadeData::default());
+ arc.mark_as_intentionally_leaked();
+ arc
+ };
+}
+
+/// All the computed information for all the stylesheets that apply to the
+/// document.
+#[derive(MallocSizeOf)]
+pub struct DocumentCascadeData {
+ #[ignore_malloc_size_of = "Arc, owned by UserAgentCascadeDataCache or empty"]
+ user_agent: Arc<UserAgentCascadeData>,
+ user: CascadeData,
+ author: CascadeData,
+ per_origin: PerOrigin<()>,
+}
+
+impl Default for DocumentCascadeData {
+ fn default() -> Self {
+ Self {
+ user_agent: EMPTY_UA_CASCADE_DATA.clone(),
+ user: Default::default(),
+ author: Default::default(),
+ per_origin: Default::default(),
+ }
+ }
+}
+
+/// An iterator over the cascade data of a given document.
+pub struct DocumentCascadeDataIter<'a> {
+ iter: PerOriginIter<'a, ()>,
+ cascade_data: &'a DocumentCascadeData,
+}
+
+impl<'a> Iterator for DocumentCascadeDataIter<'a> {
+ type Item = (&'a CascadeData, Origin);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let (_, origin) = self.iter.next()?;
+ Some((self.cascade_data.borrow_for_origin(origin), origin))
+ }
+}
+
+impl DocumentCascadeData {
+ /// Borrows the cascade data for a given origin.
+ #[inline]
+ pub fn borrow_for_origin(&self, origin: Origin) -> &CascadeData {
+ match origin {
+ Origin::UserAgent => &self.user_agent.cascade_data,
+ Origin::Author => &self.author,
+ Origin::User => &self.user,
+ }
+ }
+
+ fn iter_origins(&self) -> DocumentCascadeDataIter {
+ DocumentCascadeDataIter {
+ iter: self.per_origin.iter_origins(),
+ cascade_data: self,
+ }
+ }
+
+ fn iter_origins_rev(&self) -> DocumentCascadeDataIter {
+ DocumentCascadeDataIter {
+ iter: self.per_origin.iter_origins_rev(),
+ cascade_data: self,
+ }
+ }
+
+ /// Rebuild the cascade data for the given document stylesheets, and
+ /// optionally with a set of user agent stylesheets. Returns Err(..)
+ /// to signify OOM.
+ fn rebuild<'a, S>(
+ &mut self,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ mut flusher: DocumentStylesheetFlusher<'a, S>,
+ guards: &StylesheetGuards,
+ ) -> Result<(), AllocErr>
+ where
+ S: StylesheetInDocument + PartialEq + 'static,
+ {
+ // First do UA sheets.
+ {
+ let origin_flusher = flusher.flush_origin(Origin::UserAgent);
+ // Dirty check is just a minor optimization (no need to grab the
+ // lock if nothing has changed).
+ if origin_flusher.dirty() {
+ let mut ua_cache = UA_CASCADE_DATA_CACHE.lock().unwrap();
+ let new_data = ua_cache.lookup(
+ device,
+ quirks_mode,
+ origin_flusher,
+ guards.ua_or_user,
+ &self.user_agent,
+ )?;
+ if let Some(new_data) = new_data {
+ self.user_agent = new_data;
+ }
+ let _unused_entries = ua_cache.take_unused();
+ // See the comments in take_unused() as for why the following
+ // line.
+ std::mem::drop(ua_cache);
+ }
+ }
+
+ // Now do the user sheets.
+ self.user.rebuild(
+ device,
+ quirks_mode,
+ flusher.flush_origin(Origin::User),
+ guards.ua_or_user,
+ )?;
+
+ // And now the author sheets.
+ self.author.rebuild(
+ device,
+ quirks_mode,
+ flusher.flush_origin(Origin::Author),
+ guards.author,
+ )?;
+
+ Ok(())
+ }
+
+ /// Measures heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) {
+ self.user.add_size_of(ops, sizes);
+ self.author.add_size_of(ops, sizes);
+ }
+}
+
+/// Whether author styles are enabled.
+///
+/// This is used to support Gecko.
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)]
+pub enum AuthorStylesEnabled {
+ Yes,
+ No,
+}
+
+/// A wrapper over a DocumentStylesheetSet that can be `Sync`, since it's only
+/// used and exposed via mutable methods in the `Stylist`.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+struct StylistStylesheetSet(DocumentStylesheetSet<StylistSheet>);
+// Read above to see why this is fine.
+unsafe impl Sync for StylistStylesheetSet {}
+
+impl StylistStylesheetSet {
+ fn new() -> Self {
+ StylistStylesheetSet(DocumentStylesheetSet::new())
+ }
+}
+
+impl ops::Deref for StylistStylesheetSet {
+ type Target = DocumentStylesheetSet<StylistSheet>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl ops::DerefMut for StylistStylesheetSet {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+/// This structure holds all the selectors and device characteristics
+/// for a given document. The selectors are converted into `Rule`s
+/// and sorted into `SelectorMap`s keyed off stylesheet origin and
+/// pseudo-element (see `CascadeData`).
+///
+/// This structure is effectively created once per pipeline, in the
+/// LayoutThread corresponding to that pipeline.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Stylist {
+ /// Device that the stylist is currently evaluating against.
+ ///
+ /// This field deserves a bigger comment due to the different use that Gecko
+ /// and Servo give to it (that we should eventually unify).
+ ///
+ /// With Gecko, the device is never changed. Gecko manually tracks whether
+ /// the device data should be reconstructed, and "resets" the state of the
+ /// device.
+ ///
+ /// On Servo, on the other hand, the device is a really cheap representation
+ /// that is recreated each time some constraint changes and calling
+ /// `set_device`.
+ device: Device,
+
+ /// The list of stylesheets.
+ stylesheets: StylistStylesheetSet,
+
+ /// A cache of CascadeDatas for AuthorStylesheetSets (i.e., shadow DOM).
+ author_data_cache: CascadeDataCache<CascadeData>,
+
+ /// If true, the quirks-mode stylesheet is applied.
+ #[cfg_attr(feature = "servo", ignore_malloc_size_of = "defined in selectors")]
+ quirks_mode: QuirksMode,
+
+ /// Selector maps for all of the style sheets in the stylist, after
+ /// evalutaing media rules against the current device, split out per
+ /// cascade level.
+ cascade_data: DocumentCascadeData,
+
+ /// Whether author styles are enabled.
+ author_styles_enabled: AuthorStylesEnabled,
+
+ /// The rule tree, that stores the results of selector matching.
+ rule_tree: RuleTree,
+
+ /// The set of registered custom properties from script.
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#dom-window-registeredpropertyset-slot>
+ script_custom_properties: CustomPropertyScriptRegistry,
+
+ /// Initial values for registered custom properties.
+ initial_values_for_custom_properties: ComputedCustomProperties,
+
+ /// Flags set from computing registered custom property initial values.
+ initial_values_for_custom_properties_flags: ComputedValueFlags,
+
+ /// The total number of times the stylist has been rebuilt.
+ num_rebuilds: usize,
+}
+
+/// What cascade levels to include when styling elements.
+#[derive(Clone, Copy, PartialEq)]
+pub enum RuleInclusion {
+ /// Include rules for style sheets at all cascade levels. This is the
+ /// normal rule inclusion mode.
+ All,
+ /// Only include rules from UA and user level sheets. Used to implement
+ /// `getDefaultComputedStyle`.
+ DefaultOnly,
+}
+
+#[cfg(feature = "gecko")]
+impl From<StyleRuleInclusion> for RuleInclusion {
+ fn from(value: StyleRuleInclusion) -> Self {
+ match value {
+ StyleRuleInclusion::All => RuleInclusion::All,
+ StyleRuleInclusion::DefaultOnly => RuleInclusion::DefaultOnly,
+ }
+ }
+}
+
+/// A struct containing state from ancestor rules like @layer / @import /
+/// @container / nesting.
+struct ContainingRuleState {
+ layer_name: LayerName,
+ layer_id: LayerId,
+ container_condition_id: ContainerConditionId,
+ ancestor_selector_lists: SmallVec<[SelectorList<SelectorImpl>; 2]>,
+}
+
+impl Default for ContainingRuleState {
+ fn default() -> Self {
+ Self {
+ layer_name: LayerName::new_empty(),
+ layer_id: LayerId::root(),
+ container_condition_id: ContainerConditionId::none(),
+ ancestor_selector_lists: Default::default(),
+ }
+ }
+}
+
+struct SavedContainingRuleState {
+ ancestor_selector_lists_len: usize,
+ layer_name_len: usize,
+ layer_id: LayerId,
+ container_condition_id: ContainerConditionId,
+}
+
+impl ContainingRuleState {
+ fn save(&self) -> SavedContainingRuleState {
+ SavedContainingRuleState {
+ ancestor_selector_lists_len: self.ancestor_selector_lists.len(),
+ layer_name_len: self.layer_name.0.len(),
+ layer_id: self.layer_id,
+ container_condition_id: self.container_condition_id,
+ }
+ }
+
+ fn restore(&mut self, saved: &SavedContainingRuleState) {
+ debug_assert!(self.layer_name.0.len() >= saved.layer_name_len);
+ debug_assert!(self.ancestor_selector_lists.len() >= saved.ancestor_selector_lists_len);
+ self.ancestor_selector_lists
+ .truncate(saved.ancestor_selector_lists_len);
+ self.layer_name.0.truncate(saved.layer_name_len);
+ self.layer_id = saved.layer_id;
+ self.container_condition_id = saved.container_condition_id;
+ }
+}
+
+impl Stylist {
+ /// Construct a new `Stylist`, using given `Device` and `QuirksMode`.
+ /// If more members are added here, think about whether they should
+ /// be reset in clear().
+ #[inline]
+ pub fn new(device: Device, quirks_mode: QuirksMode) -> Self {
+ Self {
+ device,
+ quirks_mode,
+ stylesheets: StylistStylesheetSet::new(),
+ author_data_cache: CascadeDataCache::new(),
+ cascade_data: Default::default(),
+ author_styles_enabled: AuthorStylesEnabled::Yes,
+ rule_tree: RuleTree::new(),
+ script_custom_properties: Default::default(),
+ initial_values_for_custom_properties: Default::default(),
+ initial_values_for_custom_properties_flags: Default::default(),
+ num_rebuilds: 0,
+ }
+ }
+
+ /// Returns the document cascade data.
+ #[inline]
+ pub fn cascade_data(&self) -> &DocumentCascadeData {
+ &self.cascade_data
+ }
+
+ /// Returns whether author styles are enabled or not.
+ #[inline]
+ pub fn author_styles_enabled(&self) -> AuthorStylesEnabled {
+ self.author_styles_enabled
+ }
+
+ /// Iterate through all the cascade datas from the document.
+ #[inline]
+ pub fn iter_origins(&self) -> DocumentCascadeDataIter {
+ self.cascade_data.iter_origins()
+ }
+
+ /// Does what the name says, to prevent author_data_cache to grow without
+ /// bound.
+ pub fn remove_unique_author_data_cache_entries(&mut self) {
+ self.author_data_cache.take_unused();
+ }
+
+ /// Returns the custom property registration for this property's name.
+ /// https://drafts.css-houdini.org/css-properties-values-api-1/#determining-registration
+ pub fn get_custom_property_registration(&self, name: &Atom) -> &PropertyRegistrationData {
+ if let Some(registration) = self.custom_property_script_registry().get(name) {
+ return &registration.data;
+ }
+ for (data, _) in self.iter_origins() {
+ if let Some(registration) = data.custom_property_registrations.get(name) {
+ return &registration.data;
+ }
+ }
+ PropertyRegistrationData::unregistered()
+ }
+
+ /// Returns custom properties with their registered initial values.
+ pub fn get_custom_property_initial_values(&self) -> &ComputedCustomProperties {
+ &self.initial_values_for_custom_properties
+ }
+
+ /// Returns flags set from computing the registered custom property initial values.
+ pub fn get_custom_property_initial_values_flags(&self) -> ComputedValueFlags {
+ self.initial_values_for_custom_properties_flags
+ }
+
+ /// Rebuild custom properties with their registered initial values.
+ /// https://drafts.css-houdini.org/css-properties-values-api-1/#determining-registration
+ pub fn rebuild_initial_values_for_custom_properties(&mut self) {
+ let mut initial_values = ComputedCustomProperties::default();
+ let initial_values_flags;
+ {
+ let mut seen_names = PrecomputedHashSet::default();
+ let mut rule_cache_conditions = RuleCacheConditions::default();
+ let context = computed::Context::new_for_initial_at_property_value(
+ self,
+ &mut rule_cache_conditions,
+ );
+
+ for (k, v) in self.custom_property_script_registry().properties().iter() {
+ seen_names.insert(k.clone());
+ let Ok(value) = v.compute_initial_value(&context) else {
+ continue;
+ };
+ let map = if v.inherits() {
+ &mut initial_values.inherited
+ } else {
+ &mut initial_values.non_inherited
+ };
+ map.insert(k, value);
+ }
+ for (data, _) in self.iter_origins() {
+ for (k, v) in data.custom_property_registrations.iter() {
+ if seen_names.insert(k.clone()) {
+ let last_value = &v.last().unwrap().0;
+ let Ok(value) = last_value.compute_initial_value(&context) else {
+ continue;
+ };
+ let map = if last_value.inherits() {
+ &mut initial_values.inherited
+ } else {
+ &mut initial_values.non_inherited
+ };
+ map.insert(k, value);
+ }
+ }
+ }
+ initial_values_flags = context.builder.flags();
+ }
+ self.initial_values_for_custom_properties_flags = initial_values_flags;
+ self.initial_values_for_custom_properties = initial_values;
+ }
+
+ /// Rebuilds (if needed) the CascadeData given a sheet collection.
+ pub fn rebuild_author_data<S>(
+ &mut self,
+ old_data: &CascadeData,
+ collection: SheetCollectionFlusher<S>,
+ guard: &SharedRwLockReadGuard,
+ ) -> Result<Option<Arc<CascadeData>>, AllocErr>
+ where
+ S: StylesheetInDocument + PartialEq + 'static,
+ {
+ self.author_data_cache
+ .lookup(&self.device, self.quirks_mode, collection, guard, old_data)
+ }
+
+ /// Iterate over the extra data in origin order.
+ #[inline]
+ pub fn iter_extra_data_origins(&self) -> ExtraStyleDataIterator {
+ ExtraStyleDataIterator(self.cascade_data.iter_origins())
+ }
+
+ /// Iterate over the extra data in reverse origin order.
+ #[inline]
+ pub fn iter_extra_data_origins_rev(&self) -> ExtraStyleDataIterator {
+ ExtraStyleDataIterator(self.cascade_data.iter_origins_rev())
+ }
+
+ /// Returns the number of selectors.
+ pub fn num_selectors(&self) -> usize {
+ self.cascade_data
+ .iter_origins()
+ .map(|(d, _)| d.num_selectors)
+ .sum()
+ }
+
+ /// Returns the number of declarations.
+ pub fn num_declarations(&self) -> usize {
+ self.cascade_data
+ .iter_origins()
+ .map(|(d, _)| d.num_declarations)
+ .sum()
+ }
+
+ /// Returns the number of times the stylist has been rebuilt.
+ pub fn num_rebuilds(&self) -> usize {
+ self.num_rebuilds
+ }
+
+ /// Returns the number of revalidation_selectors.
+ pub fn num_revalidation_selectors(&self) -> usize {
+ self.cascade_data
+ .iter_origins()
+ .map(|(data, _)| data.selectors_for_cache_revalidation.len())
+ .sum()
+ }
+
+ /// Returns the number of entries in invalidation maps.
+ pub fn num_invalidations(&self) -> usize {
+ self.cascade_data
+ .iter_origins()
+ .map(|(data, _)| {
+ data.invalidation_map.len() + data.relative_selector_invalidation_map.len()
+ })
+ .sum()
+ }
+
+ /// Returns whether the given DocumentState bit is relied upon by a selector
+ /// of some rule.
+ pub fn has_document_state_dependency(&self, state: DocumentState) -> bool {
+ self.cascade_data
+ .iter_origins()
+ .any(|(d, _)| d.document_state_dependencies.intersects(state))
+ }
+
+ /// Flush the list of stylesheets if they changed, ensuring the stylist is
+ /// up-to-date.
+ pub fn flush<E>(
+ &mut self,
+ guards: &StylesheetGuards,
+ document_element: Option<E>,
+ snapshots: Option<&SnapshotMap>,
+ ) -> bool
+ where
+ E: TElement,
+ {
+ if !self.stylesheets.has_changed() {
+ return false;
+ }
+
+ self.num_rebuilds += 1;
+
+ let flusher = self.stylesheets.flush(document_element, snapshots);
+
+ let had_invalidations = flusher.had_invalidations();
+
+ self.cascade_data
+ .rebuild(&self.device, self.quirks_mode, flusher, guards)
+ .unwrap_or_else(|_| warn!("OOM in Stylist::flush"));
+
+ self.rebuild_initial_values_for_custom_properties();
+
+ had_invalidations
+ }
+
+ /// Insert a given stylesheet before another stylesheet in the document.
+ pub fn insert_stylesheet_before(
+ &mut self,
+ sheet: StylistSheet,
+ before_sheet: StylistSheet,
+ guard: &SharedRwLockReadGuard,
+ ) {
+ self.stylesheets
+ .insert_stylesheet_before(Some(&self.device), sheet, before_sheet, guard)
+ }
+
+ /// Marks a given stylesheet origin as dirty, due to, for example, changes
+ /// in the declarations that affect a given rule.
+ ///
+ /// FIXME(emilio): Eventually it'd be nice for this to become more
+ /// fine-grained.
+ pub fn force_stylesheet_origins_dirty(&mut self, origins: OriginSet) {
+ self.stylesheets.force_dirty(origins)
+ }
+
+ /// Sets whether author style is enabled or not.
+ pub fn set_author_styles_enabled(&mut self, enabled: AuthorStylesEnabled) {
+ self.author_styles_enabled = enabled;
+ }
+
+ /// Returns whether we've recorded any stylesheet change so far.
+ pub fn stylesheets_have_changed(&self) -> bool {
+ self.stylesheets.has_changed()
+ }
+
+ /// Appends a new stylesheet to the current set.
+ pub fn append_stylesheet(&mut self, sheet: StylistSheet, guard: &SharedRwLockReadGuard) {
+ self.stylesheets
+ .append_stylesheet(Some(&self.device), sheet, guard)
+ }
+
+ /// Remove a given stylesheet to the current set.
+ pub fn remove_stylesheet(&mut self, sheet: StylistSheet, guard: &SharedRwLockReadGuard) {
+ self.stylesheets
+ .remove_stylesheet(Some(&self.device), sheet, guard)
+ }
+
+ /// Notify of a change of a given rule.
+ pub fn rule_changed(
+ &mut self,
+ sheet: &StylistSheet,
+ rule: &CssRule,
+ guard: &SharedRwLockReadGuard,
+ change_kind: RuleChangeKind,
+ ) {
+ self.stylesheets
+ .rule_changed(Some(&self.device), sheet, rule, guard, change_kind)
+ }
+
+ /// Appends a new stylesheet to the current set.
+ #[inline]
+ pub fn sheet_count(&self, origin: Origin) -> usize {
+ self.stylesheets.sheet_count(origin)
+ }
+
+ /// Appends a new stylesheet to the current set.
+ #[inline]
+ pub fn sheet_at(&self, origin: Origin, index: usize) -> Option<&StylistSheet> {
+ self.stylesheets.get(origin, index)
+ }
+
+ /// Returns whether for any of the applicable style rule data a given
+ /// condition is true.
+ pub fn any_applicable_rule_data<E, F>(&self, element: E, mut f: F) -> bool
+ where
+ E: TElement,
+ F: FnMut(&CascadeData) -> bool,
+ {
+ if f(&self.cascade_data.user_agent.cascade_data) {
+ return true;
+ }
+
+ let mut maybe = false;
+
+ let doc_author_rules_apply =
+ element.each_applicable_non_document_style_rule_data(|data, _| {
+ maybe = maybe || f(&*data);
+ });
+
+ if maybe || f(&self.cascade_data.user) {
+ return true;
+ }
+
+ doc_author_rules_apply && f(&self.cascade_data.author)
+ }
+
+ /// Execute callback for all applicable style rule data.
+ pub fn for_each_cascade_data_with_scope<'a, E, F>(&'a self, element: E, mut f: F)
+ where
+ E: TElement + 'a,
+ F: FnMut(&'a CascadeData, Option<E>),
+ {
+ f(&self.cascade_data.user_agent.cascade_data, None);
+ element.each_applicable_non_document_style_rule_data(|data, scope| {
+ f(data, Some(scope));
+ });
+ f(&self.cascade_data.user, None);
+ f(&self.cascade_data.author, None);
+ }
+
+ /// Computes the style for a given "precomputed" pseudo-element, taking the
+ /// universal rules and applying them.
+ pub fn precomputed_values_for_pseudo<E>(
+ &self,
+ guards: &StylesheetGuards,
+ pseudo: &PseudoElement,
+ parent: Option<&ComputedValues>,
+ ) -> Arc<ComputedValues>
+ where
+ E: TElement,
+ {
+ debug_assert!(pseudo.is_precomputed());
+
+ let rule_node = self.rule_node_for_precomputed_pseudo(guards, pseudo, vec![]);
+
+ self.precomputed_values_for_pseudo_with_rule_node::<E>(guards, pseudo, parent, rule_node)
+ }
+
+ /// Computes the style for a given "precomputed" pseudo-element with
+ /// given rule node.
+ ///
+ /// TODO(emilio): The type parameter could go away with a void type
+ /// implementing TElement.
+ pub fn precomputed_values_for_pseudo_with_rule_node<E>(
+ &self,
+ guards: &StylesheetGuards,
+ pseudo: &PseudoElement,
+ parent: Option<&ComputedValues>,
+ rules: StrongRuleNode,
+ ) -> Arc<ComputedValues>
+ where
+ E: TElement,
+ {
+ self.compute_pseudo_element_style_with_inputs::<E>(
+ CascadeInputs {
+ rules: Some(rules),
+ visited_rules: None,
+ flags: Default::default(),
+ },
+ pseudo,
+ guards,
+ parent,
+ /* element */ None,
+ )
+ }
+
+ /// Returns the rule node for a given precomputed pseudo-element.
+ ///
+ /// If we want to include extra declarations to this precomputed
+ /// pseudo-element, we can provide a vector of ApplicableDeclarationBlocks
+ /// to extra_declarations. This is useful for @page rules.
+ pub fn rule_node_for_precomputed_pseudo(
+ &self,
+ guards: &StylesheetGuards,
+ pseudo: &PseudoElement,
+ mut extra_declarations: Vec<ApplicableDeclarationBlock>,
+ ) -> StrongRuleNode {
+ let mut declarations_with_extra;
+ let declarations = match self
+ .cascade_data
+ .user_agent
+ .precomputed_pseudo_element_decls
+ .get(pseudo)
+ {
+ Some(declarations) => {
+ if !extra_declarations.is_empty() {
+ declarations_with_extra = declarations.clone();
+ declarations_with_extra.append(&mut extra_declarations);
+ &*declarations_with_extra
+ } else {
+ &**declarations
+ }
+ },
+ None => &[],
+ };
+
+ self.rule_tree.insert_ordered_rules_with_important(
+ declarations.into_iter().map(|a| a.clone().for_rule_tree()),
+ guards,
+ )
+ }
+
+ /// Returns the style for an anonymous box of the given type.
+ ///
+ /// TODO(emilio): The type parameter could go away with a void type
+ /// implementing TElement.
+ #[cfg(feature = "servo")]
+ pub fn style_for_anonymous<E>(
+ &self,
+ guards: &StylesheetGuards,
+ pseudo: &PseudoElement,
+ parent_style: &ComputedValues,
+ ) -> Arc<ComputedValues>
+ where
+ E: TElement,
+ {
+ self.precomputed_values_for_pseudo::<E>(guards, &pseudo, Some(parent_style))
+ }
+
+ /// Computes a pseudo-element style lazily during layout.
+ ///
+ /// This can only be done for a certain set of pseudo-elements, like
+ /// :selection.
+ ///
+ /// Check the documentation on lazy pseudo-elements in
+ /// docs/components/style.md
+ pub fn lazily_compute_pseudo_element_style<E>(
+ &self,
+ guards: &StylesheetGuards,
+ element: E,
+ pseudo: &PseudoElement,
+ rule_inclusion: RuleInclusion,
+ originating_element_style: &ComputedValues,
+ is_probe: bool,
+ matching_fn: Option<&dyn Fn(&PseudoElement) -> bool>,
+ ) -> Option<Arc<ComputedValues>>
+ where
+ E: TElement,
+ {
+ let cascade_inputs = self.lazy_pseudo_rules(
+ guards,
+ element,
+ originating_element_style,
+ pseudo,
+ is_probe,
+ rule_inclusion,
+ matching_fn,
+ )?;
+
+ Some(self.compute_pseudo_element_style_with_inputs(
+ cascade_inputs,
+ pseudo,
+ guards,
+ Some(originating_element_style),
+ Some(element),
+ ))
+ }
+
+ /// Computes a pseudo-element style lazily using the given CascadeInputs.
+ /// This can be used for truly lazy pseudo-elements or to avoid redoing
+ /// selector matching for eager pseudo-elements when we need to recompute
+ /// their style with a new parent style.
+ pub fn compute_pseudo_element_style_with_inputs<E>(
+ &self,
+ inputs: CascadeInputs,
+ pseudo: &PseudoElement,
+ guards: &StylesheetGuards,
+ parent_style: Option<&ComputedValues>,
+ element: Option<E>,
+ ) -> Arc<ComputedValues>
+ where
+ E: TElement,
+ {
+ // FIXME(emilio): The lack of layout_parent_style here could be
+ // worrying, but we're probably dropping the display fixup for
+ // pseudos other than before and after, so it's probably ok.
+ //
+ // (Though the flags don't indicate so!)
+ //
+ // It'd be fine to assert that this isn't called with a parent style
+ // where display contents is in effect, but in practice this is hard to
+ // do for stuff like :-moz-fieldset-content with a
+ // <fieldset style="display: contents">. That is, the computed value of
+ // display for the fieldset is "contents", even though it's not the used
+ // value, so we don't need to adjust in a different way anyway.
+ self.cascade_style_and_visited(
+ element,
+ Some(pseudo),
+ inputs,
+ guards,
+ parent_style,
+ parent_style,
+ FirstLineReparenting::No,
+ /* rule_cache = */ None,
+ &mut RuleCacheConditions::default(),
+ )
+ }
+
+ /// Computes a style using the given CascadeInputs. This can be used to
+ /// compute a style any time we know what rules apply and just need to use
+ /// the given parent styles.
+ ///
+ /// parent_style is the style to inherit from for properties affected by
+ /// first-line ancestors.
+ ///
+ /// parent_style_ignoring_first_line is the style to inherit from for
+ /// properties not affected by first-line ancestors.
+ ///
+ /// layout_parent_style is the style used for some property fixups. It's
+ /// the style of the nearest ancestor with a layout box.
+ pub fn cascade_style_and_visited<E>(
+ &self,
+ element: Option<E>,
+ pseudo: Option<&PseudoElement>,
+ inputs: CascadeInputs,
+ guards: &StylesheetGuards,
+ parent_style: Option<&ComputedValues>,
+ layout_parent_style: Option<&ComputedValues>,
+ first_line_reparenting: FirstLineReparenting,
+ rule_cache: Option<&RuleCache>,
+ rule_cache_conditions: &mut RuleCacheConditions,
+ ) -> Arc<ComputedValues>
+ where
+ E: TElement,
+ {
+ debug_assert!(pseudo.is_some() || element.is_some(), "Huh?");
+
+ // We need to compute visited values if we have visited rules or if our
+ // parent has visited values.
+ let visited_rules = match inputs.visited_rules.as_ref() {
+ Some(rules) => Some(rules),
+ None => {
+ if parent_style.and_then(|s| s.visited_style()).is_some() {
+ Some(inputs.rules.as_ref().unwrap_or(self.rule_tree.root()))
+ } else {
+ None
+ }
+ },
+ };
+
+ // Read the comment on `precomputed_values_for_pseudo` to see why it's
+ // difficult to assert that display: contents nodes never arrive here
+ // (tl;dr: It doesn't apply for replaced elements and such, but the
+ // computed value is still "contents").
+ //
+ // FIXME(emilio): We should assert that it holds if pseudo.is_none()!
+ properties::cascade::<E>(
+ &self,
+ pseudo,
+ inputs.rules.as_ref().unwrap_or(self.rule_tree.root()),
+ guards,
+ parent_style,
+ layout_parent_style,
+ first_line_reparenting,
+ visited_rules,
+ inputs.flags,
+ rule_cache,
+ rule_cache_conditions,
+ element,
+ )
+ }
+
+ /// Computes the cascade inputs for a lazily-cascaded pseudo-element.
+ ///
+ /// See the documentation on lazy pseudo-elements in
+ /// docs/components/style.md
+ fn lazy_pseudo_rules<E>(
+ &self,
+ guards: &StylesheetGuards,
+ element: E,
+ originating_element_style: &ComputedValues,
+ pseudo: &PseudoElement,
+ is_probe: bool,
+ rule_inclusion: RuleInclusion,
+ matching_fn: Option<&dyn Fn(&PseudoElement) -> bool>,
+ ) -> Option<CascadeInputs>
+ where
+ E: TElement,
+ {
+ debug_assert!(pseudo.is_lazy());
+
+ let mut selector_caches = SelectorCaches::default();
+ // No need to bother setting the selector flags when we're computing
+ // default styles.
+ let needs_selector_flags = if rule_inclusion == RuleInclusion::DefaultOnly {
+ NeedsSelectorFlags::No
+ } else {
+ NeedsSelectorFlags::Yes
+ };
+
+ let mut declarations = ApplicableDeclarationList::new();
+ let mut matching_context = MatchingContext::<'_, E::Impl>::new(
+ MatchingMode::ForStatelessPseudoElement,
+ None,
+ &mut selector_caches,
+ self.quirks_mode,
+ needs_selector_flags,
+ MatchingForInvalidation::No,
+ );
+
+ matching_context.pseudo_element_matching_fn = matching_fn;
+ matching_context.extra_data.originating_element_style = Some(originating_element_style);
+
+ self.push_applicable_declarations(
+ element,
+ Some(&pseudo),
+ None,
+ None,
+ /* animation_declarations = */ Default::default(),
+ rule_inclusion,
+ &mut declarations,
+ &mut matching_context,
+ );
+
+ if declarations.is_empty() && is_probe {
+ return None;
+ }
+
+ let rules = self.rule_tree.compute_rule_node(&mut declarations, guards);
+
+ let mut visited_rules = None;
+ if originating_element_style.visited_style().is_some() {
+ let mut declarations = ApplicableDeclarationList::new();
+ let mut selector_caches = SelectorCaches::default();
+
+ let mut matching_context = MatchingContext::<'_, E::Impl>::new_for_visited(
+ MatchingMode::ForStatelessPseudoElement,
+ None,
+ &mut selector_caches,
+ VisitedHandlingMode::RelevantLinkVisited,
+ self.quirks_mode,
+ needs_selector_flags,
+ MatchingForInvalidation::No,
+ );
+ matching_context.pseudo_element_matching_fn = matching_fn;
+ matching_context.extra_data.originating_element_style = Some(originating_element_style);
+
+ self.push_applicable_declarations(
+ element,
+ Some(&pseudo),
+ None,
+ None,
+ /* animation_declarations = */ Default::default(),
+ rule_inclusion,
+ &mut declarations,
+ &mut matching_context,
+ );
+ if !declarations.is_empty() {
+ let rule_node = self.rule_tree.insert_ordered_rules_with_important(
+ declarations.drain(..).map(|a| a.for_rule_tree()),
+ guards,
+ );
+ if rule_node != *self.rule_tree.root() {
+ visited_rules = Some(rule_node);
+ }
+ }
+ }
+
+ Some(CascadeInputs {
+ rules: Some(rules),
+ visited_rules,
+ flags: matching_context.extra_data.cascade_input_flags,
+ })
+ }
+
+ /// Set a given device, which may change the styles that apply to the
+ /// document.
+ ///
+ /// Returns the sheet origins that were actually affected.
+ ///
+ /// This means that we may need to rebuild style data even if the
+ /// stylesheets haven't changed.
+ ///
+ /// Also, the device that arrives here may need to take the viewport rules
+ /// into account.
+ pub fn set_device(&mut self, device: Device, guards: &StylesheetGuards) -> OriginSet {
+ self.device = device;
+ self.media_features_change_changed_style(guards, &self.device)
+ }
+
+ /// Returns whether, given a media feature change, any previously-applicable
+ /// style has become non-applicable, or vice-versa for each origin, using
+ /// `device`.
+ pub fn media_features_change_changed_style(
+ &self,
+ guards: &StylesheetGuards,
+ device: &Device,
+ ) -> OriginSet {
+ debug!("Stylist::media_features_change_changed_style {:?}", device);
+
+ let mut origins = OriginSet::empty();
+ let stylesheets = self.stylesheets.iter();
+
+ for (stylesheet, origin) in stylesheets {
+ if origins.contains(origin.into()) {
+ continue;
+ }
+
+ let guard = guards.for_origin(origin);
+ let origin_cascade_data = self.cascade_data.borrow_for_origin(origin);
+
+ let affected_changed = !origin_cascade_data.media_feature_affected_matches(
+ stylesheet,
+ guard,
+ device,
+ self.quirks_mode,
+ );
+
+ if affected_changed {
+ origins |= origin;
+ }
+ }
+
+ origins
+ }
+
+ /// Returns the Quirks Mode of the document.
+ pub fn quirks_mode(&self) -> QuirksMode {
+ self.quirks_mode
+ }
+
+ /// Sets the quirks mode of the document.
+ pub fn set_quirks_mode(&mut self, quirks_mode: QuirksMode) {
+ if self.quirks_mode == quirks_mode {
+ return;
+ }
+ self.quirks_mode = quirks_mode;
+ self.force_stylesheet_origins_dirty(OriginSet::all());
+ }
+
+ /// Returns the applicable CSS declarations for the given element.
+ pub fn push_applicable_declarations<E>(
+ &self,
+ element: E,
+ pseudo_element: Option<&PseudoElement>,
+ style_attribute: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>,
+ smil_override: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>,
+ animation_declarations: AnimationDeclarations,
+ rule_inclusion: RuleInclusion,
+ applicable_declarations: &mut ApplicableDeclarationList,
+ context: &mut MatchingContext<E::Impl>,
+ ) where
+ E: TElement,
+ {
+ RuleCollector::new(
+ self,
+ element,
+ pseudo_element,
+ style_attribute,
+ smil_override,
+ animation_declarations,
+ rule_inclusion,
+ applicable_declarations,
+ context,
+ )
+ .collect_all();
+ }
+
+ /// Given an id, returns whether there might be any rules for that id in any
+ /// of our rule maps.
+ #[inline]
+ pub fn may_have_rules_for_id<E>(&self, id: &WeakAtom, element: E) -> bool
+ where
+ E: TElement,
+ {
+ // If id needs to be compared case-insensitively, the logic below
+ // wouldn't work. Just conservatively assume it may have such rules.
+ match self.quirks_mode().classes_and_ids_case_sensitivity() {
+ CaseSensitivity::AsciiCaseInsensitive => return true,
+ CaseSensitivity::CaseSensitive => {},
+ }
+
+ self.any_applicable_rule_data(element, |data| data.mapped_ids.contains(id))
+ }
+
+ /// Returns the registered `@keyframes` animation for the specified name.
+ #[inline]
+ pub fn get_animation<'a, E>(&'a self, name: &Atom, element: E) -> Option<&'a KeyframesAnimation>
+ where
+ E: TElement + 'a,
+ {
+ macro_rules! try_find_in {
+ ($data:expr) => {
+ if let Some(animation) = $data.animations.get(name) {
+ return Some(animation);
+ }
+ };
+ }
+
+ // NOTE(emilio): This is a best-effort thing, the right fix is a bit TBD because it
+ // involves "recording" which tree the name came from, see [1][2].
+ //
+ // [1]: https://github.com/w3c/csswg-drafts/issues/1995
+ // [2]: https://bugzil.la/1458189
+ let mut animation = None;
+ let doc_rules_apply =
+ element.each_applicable_non_document_style_rule_data(|data, _host| {
+ if animation.is_none() {
+ animation = data.animations.get(name);
+ }
+ });
+
+ if animation.is_some() {
+ return animation;
+ }
+
+ if doc_rules_apply {
+ try_find_in!(self.cascade_data.author);
+ }
+ try_find_in!(self.cascade_data.user);
+ try_find_in!(self.cascade_data.user_agent.cascade_data);
+
+ None
+ }
+
+ /// Computes the match results of a given element against the set of
+ /// revalidation selectors.
+ pub fn match_revalidation_selectors<E>(
+ &self,
+ element: E,
+ bloom: Option<&BloomFilter>,
+ selector_caches: &mut SelectorCaches,
+ needs_selector_flags: NeedsSelectorFlags,
+ ) -> RevalidationResult
+ where
+ E: TElement,
+ {
+ // NB: `MatchingMode` doesn't really matter, given we don't share style
+ // between pseudos.
+ let mut matching_context = MatchingContext::new(
+ MatchingMode::Normal,
+ bloom,
+ selector_caches,
+ self.quirks_mode,
+ needs_selector_flags,
+ MatchingForInvalidation::No,
+ );
+
+ // Note that, by the time we're revalidating, we're guaranteed that the
+ // candidate and the entry have the same id, classes, and local name.
+ // This means we're guaranteed to get the same rulehash buckets for all
+ // the lookups, which means that the bitvecs are comparable. We verify
+ // this in the caller by asserting that the bitvecs are same-length.
+ let mut result = RevalidationResult::default();
+ let mut relevant_attributes = &mut result.relevant_attributes;
+ let selectors_matched = &mut result.selectors_matched;
+
+ let matches_document_rules =
+ element.each_applicable_non_document_style_rule_data(|data, host| {
+ matching_context.with_shadow_host(Some(host), |matching_context| {
+ data.selectors_for_cache_revalidation.lookup(
+ element,
+ self.quirks_mode,
+ Some(&mut relevant_attributes),
+ |selector_and_hashes| {
+ selectors_matched.push(matches_selector(
+ &selector_and_hashes.selector,
+ selector_and_hashes.selector_offset,
+ Some(&selector_and_hashes.hashes),
+ &element,
+ matching_context,
+ ));
+ true
+ },
+ );
+ })
+ });
+
+ for (data, origin) in self.cascade_data.iter_origins() {
+ if origin == Origin::Author && !matches_document_rules {
+ continue;
+ }
+
+ data.selectors_for_cache_revalidation.lookup(
+ element,
+ self.quirks_mode,
+ Some(&mut relevant_attributes),
+ |selector_and_hashes| {
+ selectors_matched.push(matches_selector(
+ &selector_and_hashes.selector,
+ selector_and_hashes.selector_offset,
+ Some(&selector_and_hashes.hashes),
+ &element,
+ &mut matching_context,
+ ));
+ true
+ },
+ );
+ }
+
+ result
+ }
+
+ /// Computes styles for a given declaration with parent_style.
+ ///
+ /// FIXME(emilio): the lack of pseudo / cascade flags look quite dubious,
+ /// hopefully this is only used for some canvas font stuff.
+ ///
+ /// TODO(emilio): The type parameter can go away when
+ /// https://github.com/rust-lang/rust/issues/35121 is fixed.
+ pub fn compute_for_declarations<E>(
+ &self,
+ guards: &StylesheetGuards,
+ parent_style: &ComputedValues,
+ declarations: Arc<Locked<PropertyDeclarationBlock>>,
+ ) -> Arc<ComputedValues>
+ where
+ E: TElement,
+ {
+ let block = declarations.read_with(guards.author);
+
+ // We don't bother inserting these declarations in the rule tree, since
+ // it'd be quite useless and slow.
+ //
+ // TODO(emilio): Now that we fixed bug 1493420, we should consider
+ // reversing this as it shouldn't be slow anymore, and should avoid
+ // generating two instantiations of apply_declarations.
+ properties::apply_declarations::<E, _>(
+ &self,
+ /* pseudo = */ None,
+ self.rule_tree.root(),
+ guards,
+ block.declaration_importance_iter().map(|(declaration, _)| {
+ (
+ declaration,
+ CascadePriority::new(
+ CascadeLevel::same_tree_author_normal(),
+ LayerOrder::root(),
+ ),
+ )
+ }),
+ Some(parent_style),
+ Some(parent_style),
+ FirstLineReparenting::No,
+ CascadeMode::Unvisited {
+ visited_rules: None,
+ },
+ Default::default(),
+ /* rule_cache = */ None,
+ &mut Default::default(),
+ /* element = */ None,
+ )
+ }
+
+ /// Accessor for a shared reference to the device.
+ #[inline]
+ pub fn device(&self) -> &Device {
+ &self.device
+ }
+
+ /// Accessor for a mutable reference to the device.
+ #[inline]
+ pub fn device_mut(&mut self) -> &mut Device {
+ &mut self.device
+ }
+
+ /// Accessor for a shared reference to the rule tree.
+ #[inline]
+ pub fn rule_tree(&self) -> &RuleTree {
+ &self.rule_tree
+ }
+
+ /// Returns the script-registered custom property registry.
+ #[inline]
+ pub fn custom_property_script_registry(&self) -> &CustomPropertyScriptRegistry {
+ &self.script_custom_properties
+ }
+
+ /// Returns the script-registered custom property registry, as a mutable ref.
+ #[inline]
+ pub fn custom_property_script_registry_mut(&mut self) -> &mut CustomPropertyScriptRegistry {
+ &mut self.script_custom_properties
+ }
+
+ /// Measures heap usage.
+ #[cfg(feature = "gecko")]
+ pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) {
+ self.cascade_data.add_size_of(ops, sizes);
+ self.author_data_cache.add_size_of(ops, sizes);
+ sizes.mRuleTree += self.rule_tree.size_of(ops);
+
+ // We may measure other fields in the future if DMD says it's worth it.
+ }
+
+ /// Shutdown the static data that this module stores.
+ pub fn shutdown() {
+ let _entries = UA_CASCADE_DATA_CACHE.lock().unwrap().take_all();
+ }
+}
+
+/// A vector that is sorted in layer order.
+#[derive(Clone, Debug, Deref, MallocSizeOf)]
+pub struct LayerOrderedVec<T>(Vec<(T, LayerId)>);
+impl<T> Default for LayerOrderedVec<T> {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+/// A map that is sorted in layer order.
+#[derive(Clone, Debug, Deref, MallocSizeOf)]
+pub struct LayerOrderedMap<T>(PrecomputedHashMap<Atom, SmallVec<[(T, LayerId); 1]>>);
+impl<T> Default for LayerOrderedMap<T> {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+impl<T: 'static> LayerOrderedVec<T> {
+ fn clear(&mut self) {
+ self.0.clear();
+ }
+ fn push(&mut self, v: T, id: LayerId) {
+ self.0.push((v, id));
+ }
+ fn sort(&mut self, layers: &[CascadeLayer]) {
+ self.0
+ .sort_by_key(|&(_, ref id)| layers[id.0 as usize].order)
+ }
+}
+
+impl<T: 'static> LayerOrderedMap<T> {
+ fn shrink_if_needed(&mut self) {
+ self.0.shrink_if_needed();
+ }
+ fn clear(&mut self) {
+ self.0.clear();
+ }
+ fn try_insert(&mut self, name: Atom, v: T, id: LayerId) -> Result<(), AllocErr> {
+ self.try_insert_with(name, v, id, |_, _| Ordering::Equal)
+ }
+ fn try_insert_with(
+ &mut self,
+ name: Atom,
+ v: T,
+ id: LayerId,
+ cmp: impl Fn(&T, &T) -> Ordering,
+ ) -> Result<(), AllocErr> {
+ self.0.try_reserve(1)?;
+ let vec = self.0.entry(name).or_default();
+ if let Some(&mut (ref mut val, ref last_id)) = vec.last_mut() {
+ if *last_id == id {
+ if cmp(&val, &v) != Ordering::Greater {
+ *val = v;
+ }
+ return Ok(());
+ }
+ }
+ vec.push((v, id));
+ Ok(())
+ }
+ fn sort(&mut self, layers: &[CascadeLayer]) {
+ self.sort_with(layers, |_, _| Ordering::Equal)
+ }
+ fn sort_with(&mut self, layers: &[CascadeLayer], cmp: impl Fn(&T, &T) -> Ordering) {
+ for (_, v) in self.0.iter_mut() {
+ v.sort_by(|&(ref v1, ref id1), &(ref v2, ref id2)| {
+ let order1 = layers[id1.0 as usize].order;
+ let order2 = layers[id2.0 as usize].order;
+ order1.cmp(&order2).then_with(|| cmp(v1, v2))
+ })
+ }
+ }
+ /// Get an entry on the LayerOrderedMap by name.
+ pub fn get(&self, name: &Atom) -> Option<&T> {
+ let vec = self.0.get(name)?;
+ Some(&vec.last()?.0)
+ }
+}
+
+/// Wrapper to allow better tracking of memory usage by page rule lists.
+///
+/// This includes the layer ID for use with the named page table.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct PageRuleData {
+ /// Layer ID for sorting page rules after matching.
+ pub layer: LayerId,
+ /// Page rule
+ #[ignore_malloc_size_of = "Arc, stylesheet measures as primary ref"]
+ pub rule: Arc<Locked<PageRule>>,
+}
+
+/// Stores page rules indexed by page names.
+#[derive(Clone, Debug, Default, MallocSizeOf)]
+pub struct PageRuleMap {
+ /// Page rules, indexed by page name. An empty atom indicates no page name.
+ pub rules: PrecomputedHashMap<Atom, SmallVec<[PageRuleData; 1]>>,
+}
+
+impl PageRuleMap {
+ #[inline]
+ fn clear(&mut self) {
+ self.rules.clear();
+ }
+
+ /// Uses page-name and pseudo-classes to match all applicable
+ /// page-rules and append them to the matched_rules vec.
+ /// This will ensure correct rule order for cascading.
+ pub fn match_and_append_rules(
+ &self,
+ matched_rules: &mut Vec<ApplicableDeclarationBlock>,
+ origin: Origin,
+ guards: &StylesheetGuards,
+ cascade_data: &DocumentCascadeData,
+ name: &Option<Atom>,
+ pseudos: PagePseudoClassFlags,
+ ) {
+ let level = match origin {
+ Origin::UserAgent => CascadeLevel::UANormal,
+ Origin::User => CascadeLevel::UserNormal,
+ Origin::Author => CascadeLevel::same_tree_author_normal(),
+ };
+ let cascade_data = cascade_data.borrow_for_origin(origin);
+ let start = matched_rules.len();
+
+ self.match_and_add_rules(
+ matched_rules,
+ level,
+ guards,
+ cascade_data,
+ &atom!(""),
+ pseudos,
+ );
+ if let Some(name) = name {
+ self.match_and_add_rules(matched_rules, level, guards, cascade_data, name, pseudos);
+ }
+
+ // Because page-rules do not have source location information stored,
+ // use stable sort to ensure source locations are preserved.
+ matched_rules[start..]
+ .sort_by_key(|block| (block.layer_order(), block.specificity, block.source_order()));
+ }
+
+ fn match_and_add_rules(
+ &self,
+ extra_declarations: &mut Vec<ApplicableDeclarationBlock>,
+ level: CascadeLevel,
+ guards: &StylesheetGuards,
+ cascade_data: &CascadeData,
+ name: &Atom,
+ pseudos: PagePseudoClassFlags,
+ ) {
+ let rules = match self.rules.get(name) {
+ Some(rules) => rules,
+ None => return,
+ };
+ for data in rules.iter() {
+ let rule = data.rule.read_with(level.guard(&guards));
+ let specificity = match rule.match_specificity(pseudos) {
+ Some(specificity) => specificity,
+ None => continue,
+ };
+ let block = rule.block.clone();
+ extra_declarations.push(ApplicableDeclarationBlock::new(
+ StyleSource::from_declarations(block),
+ 0,
+ level,
+ specificity,
+ cascade_data.layer_order_for(data.layer),
+ ));
+ }
+ }
+}
+
+impl MallocShallowSizeOf for PageRuleMap {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.rules.shallow_size_of(ops)
+ }
+}
+
+/// This struct holds data which users of Stylist may want to extract
+/// from stylesheets which can be done at the same time as updating.
+#[derive(Clone, Debug, Default)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct ExtraStyleData {
+ /// A list of effective font-face rules and their origin.
+ #[cfg(feature = "gecko")]
+ pub font_faces: LayerOrderedVec<Arc<Locked<FontFaceRule>>>,
+
+ /// A list of effective font-feature-values rules.
+ #[cfg(feature = "gecko")]
+ pub font_feature_values: LayerOrderedVec<Arc<FontFeatureValuesRule>>,
+
+ /// A list of effective font-palette-values rules.
+ #[cfg(feature = "gecko")]
+ pub font_palette_values: LayerOrderedVec<Arc<FontPaletteValuesRule>>,
+
+ /// A map of effective counter-style rules.
+ #[cfg(feature = "gecko")]
+ pub counter_styles: LayerOrderedMap<Arc<Locked<CounterStyleRule>>>,
+
+ /// A map of effective page rules.
+ #[cfg(feature = "gecko")]
+ pub pages: PageRuleMap,
+}
+
+#[cfg(feature = "gecko")]
+impl ExtraStyleData {
+ /// Add the given @font-face rule.
+ fn add_font_face(&mut self, rule: &Arc<Locked<FontFaceRule>>, layer: LayerId) {
+ self.font_faces.push(rule.clone(), layer);
+ }
+
+ /// Add the given @font-feature-values rule.
+ fn add_font_feature_values(&mut self, rule: &Arc<FontFeatureValuesRule>, layer: LayerId) {
+ self.font_feature_values.push(rule.clone(), layer);
+ }
+
+ /// Add the given @font-palette-values rule.
+ fn add_font_palette_values(&mut self, rule: &Arc<FontPaletteValuesRule>, layer: LayerId) {
+ self.font_palette_values.push(rule.clone(), layer);
+ }
+
+ /// Add the given @counter-style rule.
+ fn add_counter_style(
+ &mut self,
+ guard: &SharedRwLockReadGuard,
+ rule: &Arc<Locked<CounterStyleRule>>,
+ layer: LayerId,
+ ) -> Result<(), AllocErr> {
+ let name = rule.read_with(guard).name().0.clone();
+ self.counter_styles.try_insert(name, rule.clone(), layer)
+ }
+
+ /// Add the given @page rule.
+ fn add_page(
+ &mut self,
+ guard: &SharedRwLockReadGuard,
+ rule: &Arc<Locked<PageRule>>,
+ layer: LayerId,
+ ) -> Result<(), AllocErr> {
+ let page_rule = rule.read_with(guard);
+ let mut add_rule = |name| {
+ let vec = self.pages.rules.entry(name).or_default();
+ vec.push(PageRuleData {
+ layer,
+ rule: rule.clone(),
+ });
+ };
+ if page_rule.selectors.0.is_empty() {
+ add_rule(atom!(""));
+ } else {
+ for selector in page_rule.selectors.as_slice() {
+ add_rule(selector.name.0.clone());
+ }
+ }
+ Ok(())
+ }
+
+ fn sort_by_layer(&mut self, layers: &[CascadeLayer]) {
+ self.font_faces.sort(layers);
+ self.font_feature_values.sort(layers);
+ self.font_palette_values.sort(layers);
+ self.counter_styles.sort(layers);
+ }
+
+ fn clear(&mut self) {
+ #[cfg(feature = "gecko")]
+ {
+ self.font_faces.clear();
+ self.font_feature_values.clear();
+ self.font_palette_values.clear();
+ self.counter_styles.clear();
+ self.pages.clear();
+ }
+ }
+}
+
+// Don't let a prefixed keyframes animation override
+// a non-prefixed one.
+fn compare_keyframes_in_same_layer(v1: &KeyframesAnimation, v2: &KeyframesAnimation) -> Ordering {
+ if v1.vendor_prefix.is_some() == v2.vendor_prefix.is_some() {
+ Ordering::Equal
+ } else if v2.vendor_prefix.is_some() {
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+}
+
+/// An iterator over the different ExtraStyleData.
+pub struct ExtraStyleDataIterator<'a>(DocumentCascadeDataIter<'a>);
+
+impl<'a> Iterator for ExtraStyleDataIterator<'a> {
+ type Item = (&'a ExtraStyleData, Origin);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.0.next().map(|d| (&d.0.extra_data, d.1))
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl MallocSizeOf for ExtraStyleData {
+ /// Measure heap usage.
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut n = 0;
+ n += self.font_faces.shallow_size_of(ops);
+ n += self.font_feature_values.shallow_size_of(ops);
+ n += self.font_palette_values.shallow_size_of(ops);
+ n += self.counter_styles.shallow_size_of(ops);
+ n += self.pages.shallow_size_of(ops);
+ n
+ }
+}
+
+/// SelectorMapEntry implementation for use in our revalidation selector map.
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(Clone, Debug)]
+struct RevalidationSelectorAndHashes {
+ #[cfg_attr(
+ feature = "gecko",
+ ignore_malloc_size_of = "CssRules have primary refs, we measure there"
+ )]
+ selector: Selector<SelectorImpl>,
+ selector_offset: usize,
+ hashes: AncestorHashes,
+}
+
+impl RevalidationSelectorAndHashes {
+ fn new(selector: Selector<SelectorImpl>, hashes: AncestorHashes) -> Self {
+ let selector_offset = {
+ // We basically want to check whether the first combinator is a
+ // pseudo-element combinator. If it is, we want to use the offset
+ // one past it. Otherwise, our offset is 0.
+ let mut index = 0;
+ let mut iter = selector.iter();
+
+ // First skip over the first ComplexSelector.
+ //
+ // We can't check what sort of what combinator we have until we do
+ // that.
+ for _ in &mut iter {
+ index += 1; // Simple selector
+ }
+
+ match iter.next_sequence() {
+ Some(Combinator::PseudoElement) => index + 1, // +1 for the combinator
+ _ => 0,
+ }
+ };
+
+ RevalidationSelectorAndHashes {
+ selector,
+ selector_offset,
+ hashes,
+ }
+ }
+}
+
+impl SelectorMapEntry for RevalidationSelectorAndHashes {
+ fn selector(&self) -> SelectorIter<SelectorImpl> {
+ self.selector.iter_from(self.selector_offset)
+ }
+}
+
+/// A selector visitor implementation that collects all the state the Stylist
+/// cares about a selector.
+struct StylistSelectorVisitor<'a> {
+ /// Whether we've past the rightmost compound selector, not counting
+ /// pseudo-elements.
+ passed_rightmost_selector: bool,
+
+ /// Whether the selector needs revalidation for the style sharing cache.
+ needs_revalidation: &'a mut bool,
+
+ /// Flags for which selector list-containing components the visitor is
+ /// inside of, if any
+ in_selector_list_of: SelectorListKind,
+
+ /// The filter with all the id's getting referenced from rightmost
+ /// selectors.
+ mapped_ids: &'a mut PrecomputedHashSet<Atom>,
+
+ /// The filter with the IDs getting referenced from the selector list of
+ /// :nth-child(... of <selector list>) selectors.
+ nth_of_mapped_ids: &'a mut PrecomputedHashSet<Atom>,
+
+ /// The filter with the local names of attributes there are selectors for.
+ attribute_dependencies: &'a mut PrecomputedHashSet<LocalName>,
+
+ /// The filter with the classes getting referenced from the selector list of
+ /// :nth-child(... of <selector list>) selectors.
+ nth_of_class_dependencies: &'a mut PrecomputedHashSet<Atom>,
+
+ /// The filter with the local names of attributes there are selectors for
+ /// within the selector list of :nth-child(... of <selector list>)
+ /// selectors.
+ nth_of_attribute_dependencies: &'a mut PrecomputedHashSet<LocalName>,
+
+ /// All the states selectors in the page reference.
+ state_dependencies: &'a mut ElementState,
+
+ /// All the state selectors in the page reference within the selector list
+ /// of :nth-child(... of <selector list>) selectors.
+ nth_of_state_dependencies: &'a mut ElementState,
+
+ /// All the document states selectors in the page reference.
+ document_state_dependencies: &'a mut DocumentState,
+}
+
+fn component_needs_revalidation(
+ c: &Component<SelectorImpl>,
+ passed_rightmost_selector: bool,
+) -> bool {
+ match *c {
+ Component::ID(_) => {
+ // TODO(emilio): This could also check that the ID is not already in
+ // the rule hash. In that case, we could avoid making this a
+ // revalidation selector too.
+ //
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1369611
+ passed_rightmost_selector
+ },
+ Component::AttributeInNoNamespaceExists { .. } |
+ Component::AttributeInNoNamespace { .. } |
+ Component::AttributeOther(_) |
+ Component::Empty |
+ Component::Nth(_) |
+ Component::NthOf(_) |
+ Component::Has(_) => true,
+ Component::NonTSPseudoClass(ref p) => p.needs_cache_revalidation(),
+ _ => false,
+ }
+}
+
+impl<'a> StylistSelectorVisitor<'a> {
+ fn visit_nested_selector(
+ &mut self,
+ in_selector_list_of: SelectorListKind,
+ selector: &Selector<SelectorImpl>,
+ ) {
+ let old_passed_rightmost_selector = self.passed_rightmost_selector;
+ let old_in_selector_list_of = self.in_selector_list_of;
+
+ self.passed_rightmost_selector = false;
+ self.in_selector_list_of = in_selector_list_of;
+ let _ret = selector.visit(self);
+ debug_assert!(_ret, "We never return false");
+
+ self.passed_rightmost_selector = old_passed_rightmost_selector;
+ self.in_selector_list_of = old_in_selector_list_of;
+ }
+}
+
+impl<'a> SelectorVisitor for StylistSelectorVisitor<'a> {
+ type Impl = SelectorImpl;
+
+ fn visit_complex_selector(&mut self, combinator: Option<Combinator>) -> bool {
+ *self.needs_revalidation =
+ *self.needs_revalidation || combinator.map_or(false, |c| c.is_sibling());
+
+ // NOTE(emilio): this call happens before we visit any of the simple
+ // selectors in the next ComplexSelector, so we can use this to skip
+ // looking at them.
+ self.passed_rightmost_selector = self.passed_rightmost_selector ||
+ !matches!(combinator, None | Some(Combinator::PseudoElement));
+
+ true
+ }
+
+ fn visit_selector_list(
+ &mut self,
+ list_kind: SelectorListKind,
+ list: &[Selector<Self::Impl>],
+ ) -> bool {
+ let in_selector_list_of = self.in_selector_list_of | list_kind;
+ for selector in list {
+ self.visit_nested_selector(in_selector_list_of, selector);
+ }
+ true
+ }
+
+ fn visit_relative_selector_list(
+ &mut self,
+ list: &[selectors::parser::RelativeSelector<Self::Impl>],
+ ) -> bool {
+ let in_selector_list_of = self.in_selector_list_of | SelectorListKind::HAS;
+ for selector in list {
+ self.visit_nested_selector(in_selector_list_of, &selector.selector);
+ }
+ true
+ }
+
+ fn visit_attribute_selector(
+ &mut self,
+ _ns: &NamespaceConstraint<&Namespace>,
+ name: &LocalName,
+ lower_name: &LocalName,
+ ) -> bool {
+ if self.in_selector_list_of.relevant_to_nth_of_dependencies() {
+ self.nth_of_attribute_dependencies.insert(name.clone());
+ if name != lower_name {
+ self.nth_of_attribute_dependencies
+ .insert(lower_name.clone());
+ }
+ }
+
+ self.attribute_dependencies.insert(name.clone());
+ if name != lower_name {
+ self.attribute_dependencies.insert(lower_name.clone());
+ }
+
+ true
+ }
+
+ fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool {
+ *self.needs_revalidation = *self.needs_revalidation ||
+ component_needs_revalidation(s, self.passed_rightmost_selector);
+
+ match *s {
+ Component::NonTSPseudoClass(ref p) => {
+ self.state_dependencies.insert(p.state_flag());
+ self.document_state_dependencies
+ .insert(p.document_state_flag());
+
+ if self.in_selector_list_of.relevant_to_nth_of_dependencies() {
+ self.nth_of_state_dependencies.insert(p.state_flag());
+ }
+ },
+ Component::ID(ref id) => {
+ // We want to stop storing mapped ids as soon as we've moved off
+ // the rightmost ComplexSelector that is not a pseudo-element.
+ //
+ // That can be detected by a visit_complex_selector call with a
+ // combinator other than None and PseudoElement.
+ //
+ // Importantly, this call happens before we visit any of the
+ // simple selectors in that ComplexSelector.
+ //
+ // NOTE(emilio): See the comment regarding on when this may
+ // break in visit_complex_selector.
+ if !self.passed_rightmost_selector {
+ self.mapped_ids.insert(id.0.clone());
+ }
+
+ if self.in_selector_list_of.relevant_to_nth_of_dependencies() {
+ self.nth_of_mapped_ids.insert(id.0.clone());
+ }
+ },
+ Component::Class(ref class)
+ if self.in_selector_list_of.relevant_to_nth_of_dependencies() =>
+ {
+ self.nth_of_class_dependencies.insert(class.0.clone());
+ },
+ _ => {},
+ }
+
+ true
+ }
+}
+
+/// A set of rules for element and pseudo-elements.
+#[derive(Clone, Debug, Default, MallocSizeOf)]
+struct GenericElementAndPseudoRules<Map> {
+ /// Rules from stylesheets at this `CascadeData`'s origin.
+ element_map: Map,
+
+ /// Rules from stylesheets at this `CascadeData`'s origin that correspond
+ /// to a given pseudo-element.
+ ///
+ /// FIXME(emilio): There are a bunch of wasted entries here in practice.
+ /// Figure out a good way to do a `PerNonAnonBox` and `PerAnonBox` (for
+ /// `precomputed_values_for_pseudo`) without duplicating a lot of code.
+ pseudos_map: PerPseudoElementMap<Box<Map>>,
+}
+
+impl<Map: Default + MallocSizeOf> GenericElementAndPseudoRules<Map> {
+ #[inline(always)]
+ fn for_insertion(&mut self, pseudo_element: Option<&PseudoElement>) -> &mut Map {
+ debug_assert!(
+ pseudo_element.map_or(true, |pseudo| {
+ !pseudo.is_precomputed() && !pseudo.is_unknown_webkit_pseudo_element()
+ }),
+ "Precomputed pseudos should end up in precomputed_pseudo_element_decls, \
+ and unknown webkit pseudos should be discarded before getting here"
+ );
+
+ match pseudo_element {
+ None => &mut self.element_map,
+ Some(pseudo) => self
+ .pseudos_map
+ .get_or_insert_with(pseudo, || Box::new(Default::default())),
+ }
+ }
+
+ #[inline]
+ fn rules(&self, pseudo: Option<&PseudoElement>) -> Option<&Map> {
+ match pseudo {
+ Some(pseudo) => self.pseudos_map.get(pseudo).map(|p| &**p),
+ None => Some(&self.element_map),
+ }
+ }
+
+ /// Measures heap usage.
+ #[cfg(feature = "gecko")]
+ fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) {
+ sizes.mElementAndPseudosMaps += self.element_map.size_of(ops);
+
+ for elem in self.pseudos_map.iter() {
+ if let Some(ref elem) = *elem {
+ sizes.mElementAndPseudosMaps += <Box<_> as MallocSizeOf>::size_of(elem, ops);
+ }
+ }
+ }
+}
+
+type ElementAndPseudoRules = GenericElementAndPseudoRules<SelectorMap<Rule>>;
+type PartMap = PrecomputedHashMap<Atom, SmallVec<[Rule; 1]>>;
+type PartElementAndPseudoRules = GenericElementAndPseudoRules<PartMap>;
+
+impl ElementAndPseudoRules {
+ // TODO(emilio): Should we retain storage of these?
+ fn clear(&mut self) {
+ self.element_map.clear();
+ self.pseudos_map.clear();
+ }
+
+ fn shrink_if_needed(&mut self) {
+ self.element_map.shrink_if_needed();
+ for pseudo in self.pseudos_map.iter_mut() {
+ if let Some(ref mut pseudo) = pseudo {
+ pseudo.shrink_if_needed();
+ }
+ }
+ }
+}
+
+impl PartElementAndPseudoRules {
+ // TODO(emilio): Should we retain storage of these?
+ fn clear(&mut self) {
+ self.element_map.clear();
+ self.pseudos_map.clear();
+ }
+}
+
+/// The id of a given layer, a sequentially-increasing identifier.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord)]
+pub struct LayerId(u16);
+
+impl LayerId {
+ /// The id of the root layer.
+ pub const fn root() -> Self {
+ Self(0)
+ }
+}
+
+#[derive(Clone, Debug, MallocSizeOf)]
+struct CascadeLayer {
+ id: LayerId,
+ order: LayerOrder,
+ children: Vec<LayerId>,
+}
+
+impl CascadeLayer {
+ const fn root() -> Self {
+ Self {
+ id: LayerId::root(),
+ order: LayerOrder::root(),
+ children: vec![],
+ }
+ }
+}
+
+/// The id of a given container condition, a sequentially-increasing identifier
+/// for a given style set.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord)]
+pub struct ContainerConditionId(u16);
+
+impl ContainerConditionId {
+ /// A special id that represents no container rule all.
+ pub const fn none() -> Self {
+ Self(0)
+ }
+}
+
+#[derive(Clone, Debug, MallocSizeOf)]
+struct ContainerConditionReference {
+ parent: ContainerConditionId,
+ #[ignore_malloc_size_of = "Arc"]
+ condition: Option<Arc<ContainerCondition>>,
+}
+
+impl ContainerConditionReference {
+ const fn none() -> Self {
+ Self {
+ parent: ContainerConditionId::none(),
+ condition: None,
+ }
+ }
+}
+
+/// Data resulting from performing the CSS cascade that is specific to a given
+/// origin.
+///
+/// FIXME(emilio): Consider renaming and splitting in `CascadeData` and
+/// `InvalidationData`? That'd make `clear_cascade_data()` clearer.
+#[derive(Debug, Clone, MallocSizeOf)]
+pub struct CascadeData {
+ /// The data coming from normal style rules that apply to elements at this
+ /// cascade level.
+ normal_rules: ElementAndPseudoRules,
+
+ /// The `:host` pseudo rules that are the rightmost selector (without
+ /// accounting for pseudo-elements).
+ host_rules: Option<Box<ElementAndPseudoRules>>,
+
+ /// The data coming from ::slotted() pseudo-element rules.
+ ///
+ /// We need to store them separately because an element needs to match
+ /// ::slotted() pseudo-element rules in different shadow roots.
+ ///
+ /// In particular, we need to go through all the style data in all the
+ /// containing style scopes starting from the closest assigned slot.
+ slotted_rules: Option<Box<ElementAndPseudoRules>>,
+
+ /// The data coming from ::part() pseudo-element rules.
+ ///
+ /// We need to store them separately because an element needs to match
+ /// ::part() pseudo-element rules in different shadow roots.
+ part_rules: Option<Box<PartElementAndPseudoRules>>,
+
+ /// The invalidation map for these rules.
+ invalidation_map: InvalidationMap,
+
+ /// The relative selector equivalent of the invalidation map.
+ relative_selector_invalidation_map: RelativeSelectorInvalidationMap,
+
+ /// The attribute local names that appear in attribute selectors. Used
+ /// to avoid taking element snapshots when an irrelevant attribute changes.
+ /// (We don't bother storing the namespace, since namespaced attributes are
+ /// rare.)
+ attribute_dependencies: PrecomputedHashSet<LocalName>,
+
+ /// The classes that appear in the selector list of
+ /// :nth-child(... of <selector list>). Used to avoid restyling siblings of
+ /// an element when an irrelevant class changes.
+ nth_of_class_dependencies: PrecomputedHashSet<Atom>,
+
+ /// The attributes that appear in the selector list of
+ /// :nth-child(... of <selector list>). Used to avoid restyling siblings of
+ /// an element when an irrelevant attribute changes.
+ nth_of_attribute_dependencies: PrecomputedHashSet<LocalName>,
+
+ /// The element state bits that are relied on by selectors. Like
+ /// `attribute_dependencies`, this is used to avoid taking element snapshots
+ /// when an irrelevant element state bit changes.
+ state_dependencies: ElementState,
+
+ /// The element state bits that are relied on by selectors that appear in
+ /// the selector list of :nth-child(... of <selector list>).
+ nth_of_state_dependencies: ElementState,
+
+ /// The document state bits that are relied on by selectors. This is used
+ /// to tell whether we need to restyle the entire document when a document
+ /// state bit changes.
+ document_state_dependencies: DocumentState,
+
+ /// The ids that appear in the rightmost complex selector of selectors (and
+ /// hence in our selector maps). Used to determine when sharing styles is
+ /// safe: we disallow style sharing for elements whose id matches this
+ /// filter, and hence might be in one of our selector maps.
+ mapped_ids: PrecomputedHashSet<Atom>,
+
+ /// The IDs that appear in the selector list of
+ /// :nth-child(... of <selector list>). Used to avoid restyling siblings
+ /// of an element when an irrelevant ID changes.
+ nth_of_mapped_ids: PrecomputedHashSet<Atom>,
+
+ /// Selectors that require explicit cache revalidation (i.e. which depend
+ /// on state that is not otherwise visible to the cache, like attributes or
+ /// tree-structural state like child index and pseudos).
+ #[ignore_malloc_size_of = "Arc"]
+ selectors_for_cache_revalidation: SelectorMap<RevalidationSelectorAndHashes>,
+
+ /// A map with all the animations at this `CascadeData`'s origin, indexed
+ /// by name.
+ animations: LayerOrderedMap<KeyframesAnimation>,
+
+ /// A map with all the layer-ordered registrations from style at this `CascadeData`'s origin,
+ /// indexed by name.
+ #[ignore_malloc_size_of = "Arc"]
+ custom_property_registrations: LayerOrderedMap<Arc<PropertyRegistration>>,
+
+ /// A map from cascade layer name to layer order.
+ layer_id: FxHashMap<LayerName, LayerId>,
+
+ /// The list of cascade layers, indexed by their layer id.
+ layers: SmallVec<[CascadeLayer; 1]>,
+
+ /// The list of container conditions, indexed by their id.
+ container_conditions: SmallVec<[ContainerConditionReference; 1]>,
+
+ /// Effective media query results cached from the last rebuild.
+ effective_media_query_results: EffectiveMediaQueryResults,
+
+ /// Extra data, like different kinds of rules, etc.
+ extra_data: ExtraStyleData,
+
+ /// A monotonically increasing counter to represent the order on which a
+ /// style rule appears in a stylesheet, needed to sort them by source order.
+ rules_source_order: u32,
+
+ /// The total number of selectors.
+ num_selectors: usize,
+
+ /// The total number of declarations.
+ num_declarations: usize,
+}
+
+impl CascadeData {
+ /// Creates an empty `CascadeData`.
+ pub fn new() -> Self {
+ Self {
+ normal_rules: ElementAndPseudoRules::default(),
+ host_rules: None,
+ slotted_rules: None,
+ part_rules: None,
+ invalidation_map: InvalidationMap::new(),
+ relative_selector_invalidation_map: RelativeSelectorInvalidationMap::new(),
+ nth_of_mapped_ids: PrecomputedHashSet::default(),
+ nth_of_class_dependencies: PrecomputedHashSet::default(),
+ nth_of_attribute_dependencies: PrecomputedHashSet::default(),
+ nth_of_state_dependencies: ElementState::empty(),
+ attribute_dependencies: PrecomputedHashSet::default(),
+ state_dependencies: ElementState::empty(),
+ document_state_dependencies: DocumentState::empty(),
+ mapped_ids: PrecomputedHashSet::default(),
+ selectors_for_cache_revalidation: SelectorMap::new(),
+ animations: Default::default(),
+ custom_property_registrations: Default::default(),
+ layer_id: Default::default(),
+ layers: smallvec::smallvec![CascadeLayer::root()],
+ container_conditions: smallvec::smallvec![ContainerConditionReference::none()],
+ extra_data: ExtraStyleData::default(),
+ effective_media_query_results: EffectiveMediaQueryResults::new(),
+ rules_source_order: 0,
+ num_selectors: 0,
+ num_declarations: 0,
+ }
+ }
+
+ /// Rebuild the cascade data from a given SheetCollection, incrementally if
+ /// possible.
+ pub fn rebuild<'a, S>(
+ &mut self,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ collection: SheetCollectionFlusher<S>,
+ guard: &SharedRwLockReadGuard,
+ ) -> Result<(), AllocErr>
+ where
+ S: StylesheetInDocument + PartialEq + 'static,
+ {
+ if !collection.dirty() {
+ return Ok(());
+ }
+
+ let validity = collection.data_validity();
+
+ match validity {
+ DataValidity::Valid => {},
+ DataValidity::CascadeInvalid => self.clear_cascade_data(),
+ DataValidity::FullyInvalid => self.clear(),
+ }
+
+ let mut result = Ok(());
+
+ collection.each(|stylesheet, rebuild_kind| {
+ result = self.add_stylesheet(
+ device,
+ quirks_mode,
+ stylesheet,
+ guard,
+ rebuild_kind,
+ /* precomputed_pseudo_element_decls = */ None,
+ );
+ result.is_ok()
+ });
+
+ self.did_finish_rebuild();
+
+ result
+ }
+
+ /// Returns the invalidation map.
+ pub fn invalidation_map(&self) -> &InvalidationMap {
+ &self.invalidation_map
+ }
+
+ /// Returns the relative selector invalidation map.
+ pub fn relative_selector_invalidation_map(&self) -> &RelativeSelectorInvalidationMap {
+ &self.relative_selector_invalidation_map
+ }
+
+ /// Returns whether the given ElementState bit is relied upon by a selector
+ /// of some rule.
+ #[inline]
+ pub fn has_state_dependency(&self, state: ElementState) -> bool {
+ self.state_dependencies.intersects(state)
+ }
+
+ /// Returns whether the given ElementState bit is relied upon by a selector
+ /// of some rule in the selector list of :nth-child(... of <selector list>).
+ #[inline]
+ pub fn has_nth_of_state_dependency(&self, state: ElementState) -> bool {
+ self.nth_of_state_dependencies.intersects(state)
+ }
+
+ /// Returns whether the given attribute might appear in an attribute
+ /// selector of some rule.
+ #[inline]
+ pub fn might_have_attribute_dependency(&self, local_name: &LocalName) -> bool {
+ self.attribute_dependencies.contains(local_name)
+ }
+
+ /// Returns whether the given ID might appear in an ID selector in the
+ /// selector list of :nth-child(... of <selector list>).
+ #[inline]
+ pub fn might_have_nth_of_id_dependency(&self, id: &Atom) -> bool {
+ self.nth_of_mapped_ids.contains(id)
+ }
+
+ /// Returns whether the given class might appear in a class selector in the
+ /// selector list of :nth-child(... of <selector list>).
+ #[inline]
+ pub fn might_have_nth_of_class_dependency(&self, class: &Atom) -> bool {
+ self.nth_of_class_dependencies.contains(class)
+ }
+
+ /// Returns whether the given attribute might appear in an attribute
+ /// selector in the selector list of :nth-child(... of <selector list>).
+ #[inline]
+ pub fn might_have_nth_of_attribute_dependency(&self, local_name: &LocalName) -> bool {
+ self.nth_of_attribute_dependencies.contains(local_name)
+ }
+
+ /// Returns the normal rule map for a given pseudo-element.
+ #[inline]
+ pub fn normal_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap<Rule>> {
+ self.normal_rules.rules(pseudo)
+ }
+
+ /// Returns the host pseudo rule map for a given pseudo-element.
+ #[inline]
+ pub fn host_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap<Rule>> {
+ self.host_rules.as_ref().and_then(|d| d.rules(pseudo))
+ }
+
+ /// Whether there's any host rule that could match in this scope.
+ pub fn any_host_rules(&self) -> bool {
+ self.host_rules.is_some()
+ }
+
+ /// Returns the slotted rule map for a given pseudo-element.
+ #[inline]
+ pub fn slotted_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap<Rule>> {
+ self.slotted_rules.as_ref().and_then(|d| d.rules(pseudo))
+ }
+
+ /// Whether there's any ::slotted rule that could match in this scope.
+ pub fn any_slotted_rule(&self) -> bool {
+ self.slotted_rules.is_some()
+ }
+
+ /// Returns the parts rule map for a given pseudo-element.
+ #[inline]
+ pub fn part_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&PartMap> {
+ self.part_rules.as_ref().and_then(|d| d.rules(pseudo))
+ }
+
+ /// Whether there's any ::part rule that could match in this scope.
+ pub fn any_part_rule(&self) -> bool {
+ self.part_rules.is_some()
+ }
+
+ #[inline]
+ fn layer_order_for(&self, id: LayerId) -> LayerOrder {
+ self.layers[id.0 as usize].order
+ }
+
+ pub(crate) fn container_condition_matches<E>(
+ &self,
+ mut id: ContainerConditionId,
+ stylist: &Stylist,
+ element: E,
+ context: &mut MatchingContext<E::Impl>,
+ ) -> bool
+ where
+ E: TElement,
+ {
+ loop {
+ let condition_ref = &self.container_conditions[id.0 as usize];
+ let condition = match condition_ref.condition {
+ None => return true,
+ Some(ref c) => c,
+ };
+ let matches = condition
+ .matches(
+ stylist,
+ element,
+ context.extra_data.originating_element_style,
+ &mut context.extra_data.cascade_input_flags,
+ )
+ .to_bool(/* unknown = */ false);
+ if !matches {
+ return false;
+ }
+ id = condition_ref.parent;
+ }
+ }
+
+ fn did_finish_rebuild(&mut self) {
+ self.shrink_maps_if_needed();
+ self.compute_layer_order();
+ }
+
+ fn shrink_maps_if_needed(&mut self) {
+ self.normal_rules.shrink_if_needed();
+ if let Some(ref mut host_rules) = self.host_rules {
+ host_rules.shrink_if_needed();
+ }
+ if let Some(ref mut slotted_rules) = self.slotted_rules {
+ slotted_rules.shrink_if_needed();
+ }
+ self.animations.shrink_if_needed();
+ self.custom_property_registrations.shrink_if_needed();
+ self.invalidation_map.shrink_if_needed();
+ self.relative_selector_invalidation_map.shrink_if_needed();
+ self.attribute_dependencies.shrink_if_needed();
+ self.nth_of_attribute_dependencies.shrink_if_needed();
+ self.nth_of_class_dependencies.shrink_if_needed();
+ self.nth_of_mapped_ids.shrink_if_needed();
+ self.mapped_ids.shrink_if_needed();
+ self.layer_id.shrink_if_needed();
+ self.selectors_for_cache_revalidation.shrink_if_needed();
+ }
+
+ fn compute_layer_order(&mut self) {
+ debug_assert_ne!(
+ self.layers.len(),
+ 0,
+ "There should be at least the root layer!"
+ );
+ if self.layers.len() == 1 {
+ return; // Nothing to do
+ }
+ let (first, remaining) = self.layers.split_at_mut(1);
+ let root = &mut first[0];
+ let mut order = LayerOrder::first();
+ compute_layer_order_for_subtree(root, remaining, &mut order);
+
+ // NOTE(emilio): This is a bit trickier than it should to avoid having
+ // to clone() around layer indices.
+ fn compute_layer_order_for_subtree(
+ parent: &mut CascadeLayer,
+ remaining_layers: &mut [CascadeLayer],
+ order: &mut LayerOrder,
+ ) {
+ for child in parent.children.iter() {
+ debug_assert!(
+ parent.id < *child,
+ "Children are always registered after parents"
+ );
+ let child_index = (child.0 - parent.id.0 - 1) as usize;
+ let (first, remaining) = remaining_layers.split_at_mut(child_index + 1);
+ let child = &mut first[child_index];
+ compute_layer_order_for_subtree(child, remaining, order);
+ }
+
+ if parent.id != LayerId::root() {
+ parent.order = *order;
+ order.inc();
+ }
+ }
+ self.extra_data.sort_by_layer(&self.layers);
+ self.animations
+ .sort_with(&self.layers, compare_keyframes_in_same_layer);
+ self.custom_property_registrations.sort(&self.layers)
+ }
+
+ /// Collects all the applicable media query results into `results`.
+ ///
+ /// This duplicates part of the logic in `add_stylesheet`, which is
+ /// a bit unfortunate.
+ ///
+ /// FIXME(emilio): With a bit of smartness in
+ /// `media_feature_affected_matches`, we could convert
+ /// `EffectiveMediaQueryResults` into a vector without too much effort.
+ fn collect_applicable_media_query_results_into<S>(
+ device: &Device,
+ stylesheet: &S,
+ guard: &SharedRwLockReadGuard,
+ results: &mut Vec<MediaListKey>,
+ contents_list: &mut StyleSheetContentList,
+ ) where
+ S: StylesheetInDocument + 'static,
+ {
+ if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) {
+ return;
+ }
+
+ debug!(" + {:?}", stylesheet);
+ let contents = stylesheet.contents();
+ results.push(contents.to_media_list_key());
+
+ // Safety: StyleSheetContents are reference-counted with Arc.
+ contents_list.push(StylesheetContentsPtr(unsafe {
+ Arc::from_raw_addrefed(contents)
+ }));
+
+ for rule in stylesheet.effective_rules(device, guard) {
+ match *rule {
+ CssRule::Import(ref lock) => {
+ let import_rule = lock.read_with(guard);
+ debug!(" + {:?}", import_rule.stylesheet.media(guard));
+ results.push(import_rule.to_media_list_key());
+ },
+ CssRule::Media(ref media_rule) => {
+ debug!(" + {:?}", media_rule.media_queries.read_with(guard));
+ results.push(media_rule.to_media_list_key());
+ },
+ _ => {},
+ }
+ }
+ }
+
+ fn add_rule_list<S>(
+ &mut self,
+ rules: std::slice::Iter<CssRule>,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ stylesheet: &S,
+ guard: &SharedRwLockReadGuard,
+ rebuild_kind: SheetRebuildKind,
+ containing_rule_state: &mut ContainingRuleState,
+ mut precomputed_pseudo_element_decls: Option<&mut PrecomputedPseudoElementDeclarations>,
+ ) -> Result<(), AllocErr>
+ where
+ S: StylesheetInDocument + 'static,
+ {
+ for rule in rules {
+ // Handle leaf rules first, as those are by far the most common
+ // ones, and are always effective, so we can skip some checks.
+ let mut handled = true;
+ let mut list_for_nested_rules = None;
+ match *rule {
+ CssRule::Style(ref locked) => {
+ let style_rule = locked.read_with(guard);
+ self.num_declarations += style_rule.block.read_with(&guard).len();
+
+ let has_nested_rules = style_rule.rules.is_some();
+ let mut ancestor_selectors =
+ containing_rule_state.ancestor_selector_lists.last_mut();
+ let mut replaced_selectors = SmallVec::<[Selector<SelectorImpl>; 4]>::new();
+ let collect_replaced_selectors =
+ has_nested_rules && ancestor_selectors.is_some();
+
+ for selector in style_rule.selectors.slice() {
+ self.num_selectors += 1;
+
+ let pseudo_element = selector.pseudo_element();
+ if let Some(pseudo) = pseudo_element {
+ if pseudo.is_precomputed() {
+ debug_assert!(selector.is_universal());
+ debug_assert!(ancestor_selectors.is_none());
+ debug_assert!(!has_nested_rules);
+ debug_assert_eq!(stylesheet.contents().origin, Origin::UserAgent);
+ debug_assert_eq!(containing_rule_state.layer_id, LayerId::root());
+
+ precomputed_pseudo_element_decls
+ .as_mut()
+ .expect("Expected precomputed declarations for the UA level")
+ .get_or_insert_with(pseudo, Vec::new)
+ .push(ApplicableDeclarationBlock::new(
+ StyleSource::from_rule(locked.clone()),
+ self.rules_source_order,
+ CascadeLevel::UANormal,
+ selector.specificity(),
+ LayerOrder::root(),
+ ));
+ continue;
+ }
+ if pseudo.is_unknown_webkit_pseudo_element() {
+ continue;
+ }
+ }
+
+ let selector = match ancestor_selectors {
+ Some(ref mut s) => selector.replace_parent_selector(&s),
+ None => selector.clone(),
+ };
+
+ let hashes = AncestorHashes::new(&selector, quirks_mode);
+
+ let rule = Rule::new(
+ selector,
+ hashes,
+ locked.clone(),
+ self.rules_source_order,
+ containing_rule_state.layer_id,
+ containing_rule_state.container_condition_id,
+ );
+
+ if collect_replaced_selectors {
+ replaced_selectors.push(rule.selector.clone())
+ }
+
+ if rebuild_kind.should_rebuild_invalidation() {
+ note_selector_for_invalidation(
+ &rule.selector,
+ quirks_mode,
+ &mut self.invalidation_map,
+ &mut self.relative_selector_invalidation_map,
+ )?;
+ let mut needs_revalidation = false;
+ let mut visitor = StylistSelectorVisitor {
+ needs_revalidation: &mut needs_revalidation,
+ passed_rightmost_selector: false,
+ in_selector_list_of: SelectorListKind::default(),
+ mapped_ids: &mut self.mapped_ids,
+ nth_of_mapped_ids: &mut self.nth_of_mapped_ids,
+ attribute_dependencies: &mut self.attribute_dependencies,
+ nth_of_class_dependencies: &mut self.nth_of_class_dependencies,
+ nth_of_attribute_dependencies: &mut self
+ .nth_of_attribute_dependencies,
+ state_dependencies: &mut self.state_dependencies,
+ nth_of_state_dependencies: &mut self.nth_of_state_dependencies,
+ document_state_dependencies: &mut self.document_state_dependencies,
+ };
+ rule.selector.visit(&mut visitor);
+
+ if needs_revalidation {
+ self.selectors_for_cache_revalidation.insert(
+ RevalidationSelectorAndHashes::new(
+ rule.selector.clone(),
+ rule.hashes.clone(),
+ ),
+ quirks_mode,
+ )?;
+ }
+ }
+
+ // Part is special, since given it doesn't have any
+ // selectors inside, it's not worth using a whole
+ // SelectorMap for it.
+ if let Some(parts) = rule.selector.parts() {
+ // ::part() has all semantics, so we just need to
+ // put any of them in the selector map.
+ //
+ // We choose the last one quite arbitrarily,
+ // expecting it's slightly more likely to be more
+ // specific.
+ let map = self
+ .part_rules
+ .get_or_insert_with(|| Box::new(Default::default()))
+ .for_insertion(pseudo_element);
+ map.try_reserve(1)?;
+ let vec = map.entry(parts.last().unwrap().clone().0).or_default();
+ vec.try_reserve(1)?;
+ vec.push(rule);
+ } else {
+ // NOTE(emilio): It's fine to look at :host and then at
+ // ::slotted(..), since :host::slotted(..) could never
+ // possibly match, as <slot> is not a valid shadow host.
+ let rules = if rule
+ .selector
+ .is_featureless_host_selector_or_pseudo_element()
+ {
+ self.host_rules
+ .get_or_insert_with(|| Box::new(Default::default()))
+ } else if rule.selector.is_slotted() {
+ self.slotted_rules
+ .get_or_insert_with(|| Box::new(Default::default()))
+ } else {
+ &mut self.normal_rules
+ }
+ .for_insertion(pseudo_element);
+ rules.insert(rule, quirks_mode)?;
+ }
+ }
+ self.rules_source_order += 1;
+ handled = true;
+ if has_nested_rules {
+ handled = false;
+ list_for_nested_rules = Some(if collect_replaced_selectors {
+ SelectorList::from_iter(replaced_selectors.drain(..))
+ } else {
+ style_rule.selectors.clone()
+ });
+ }
+ },
+ CssRule::Keyframes(ref keyframes_rule) => {
+ debug!("Found valid keyframes rule: {:?}", *keyframes_rule);
+ let keyframes_rule = keyframes_rule.read_with(guard);
+ let name = keyframes_rule.name.as_atom().clone();
+ let animation = KeyframesAnimation::from_keyframes(
+ &keyframes_rule.keyframes,
+ keyframes_rule.vendor_prefix.clone(),
+ guard,
+ );
+ self.animations.try_insert_with(
+ name,
+ animation,
+ containing_rule_state.layer_id,
+ compare_keyframes_in_same_layer,
+ )?;
+ },
+ CssRule::Property(ref registration) => {
+ self.custom_property_registrations.try_insert(
+ registration.name.0.clone(),
+ Arc::clone(registration),
+ containing_rule_state.layer_id,
+ )?;
+ },
+ #[cfg(feature = "gecko")]
+ CssRule::FontFace(ref rule) => {
+ // NOTE(emilio): We don't care about container_condition_id
+ // because:
+ //
+ // Global, name-defining at-rules such as @keyframes or
+ // @font-face or @layer that are defined inside container
+ // queries are not constrained by the container query
+ // conditions.
+ //
+ // https://drafts.csswg.org/css-contain-3/#container-rule
+ // (Same elsewhere)
+ self.extra_data
+ .add_font_face(rule, containing_rule_state.layer_id);
+ },
+ #[cfg(feature = "gecko")]
+ CssRule::FontFeatureValues(ref rule) => {
+ self.extra_data
+ .add_font_feature_values(rule, containing_rule_state.layer_id);
+ },
+ #[cfg(feature = "gecko")]
+ CssRule::FontPaletteValues(ref rule) => {
+ self.extra_data
+ .add_font_palette_values(rule, containing_rule_state.layer_id);
+ },
+ #[cfg(feature = "gecko")]
+ CssRule::CounterStyle(ref rule) => {
+ self.extra_data.add_counter_style(
+ guard,
+ rule,
+ containing_rule_state.layer_id,
+ )?;
+ },
+ #[cfg(feature = "gecko")]
+ CssRule::Page(ref rule) => {
+ self.extra_data
+ .add_page(guard, rule, containing_rule_state.layer_id)?;
+ handled = false;
+ },
+ _ => {
+ handled = false;
+ },
+ }
+
+ if handled {
+ // Assert that there are no children, and that the rule is
+ // effective.
+ if cfg!(debug_assertions) {
+ let mut effective = false;
+ let children = EffectiveRulesIterator::children(
+ rule,
+ device,
+ quirks_mode,
+ guard,
+ &mut effective,
+ );
+ debug_assert!(children.is_none());
+ debug_assert!(effective);
+ }
+ continue;
+ }
+
+ let mut effective = false;
+ let children =
+ EffectiveRulesIterator::children(rule, device, quirks_mode, guard, &mut effective);
+
+ if !effective {
+ continue;
+ }
+
+ fn maybe_register_layer(data: &mut CascadeData, layer: &LayerName) -> LayerId {
+ // TODO: Measure what's more common / expensive, if
+ // layer.clone() or the double hash lookup in the insert
+ // case.
+ if let Some(id) = data.layer_id.get(layer) {
+ return *id;
+ }
+ let id = LayerId(data.layers.len() as u16);
+
+ let parent_layer_id = if layer.layer_names().len() > 1 {
+ let mut parent = layer.clone();
+ parent.0.pop();
+
+ *data
+ .layer_id
+ .get_mut(&parent)
+ .expect("Parent layers should be registered before child layers")
+ } else {
+ LayerId::root()
+ };
+
+ data.layers[parent_layer_id.0 as usize].children.push(id);
+ data.layers.push(CascadeLayer {
+ id,
+ // NOTE(emilio): Order is evaluated after rebuild in
+ // compute_layer_order.
+ order: LayerOrder::first(),
+ children: vec![],
+ });
+
+ data.layer_id.insert(layer.clone(), id);
+
+ id
+ }
+
+ fn maybe_register_layers(
+ data: &mut CascadeData,
+ name: Option<&LayerName>,
+ containing_rule_state: &mut ContainingRuleState,
+ ) {
+ let anon_name;
+ let name = match name {
+ Some(name) => name,
+ None => {
+ anon_name = LayerName::new_anonymous();
+ &anon_name
+ },
+ };
+ for name in name.layer_names() {
+ containing_rule_state.layer_name.0.push(name.clone());
+ containing_rule_state.layer_id =
+ maybe_register_layer(data, &containing_rule_state.layer_name);
+ }
+ debug_assert_ne!(containing_rule_state.layer_id, LayerId::root());
+ }
+
+ let saved_containing_rule_state = containing_rule_state.save();
+ match *rule {
+ CssRule::Import(ref lock) => {
+ let import_rule = lock.read_with(guard);
+ if rebuild_kind.should_rebuild_invalidation() {
+ self.effective_media_query_results
+ .saw_effective(import_rule);
+ }
+ match import_rule.layer {
+ ImportLayer::Named(ref name) => {
+ maybe_register_layers(self, Some(name), containing_rule_state)
+ },
+ ImportLayer::Anonymous => {
+ maybe_register_layers(self, None, containing_rule_state)
+ },
+ ImportLayer::None => {},
+ }
+ },
+ CssRule::Media(ref media_rule) => {
+ if rebuild_kind.should_rebuild_invalidation() {
+ self.effective_media_query_results
+ .saw_effective(&**media_rule);
+ }
+ },
+ CssRule::LayerBlock(ref rule) => {
+ maybe_register_layers(self, rule.name.as_ref(), containing_rule_state);
+ },
+ CssRule::LayerStatement(ref rule) => {
+ for name in &*rule.names {
+ maybe_register_layers(self, Some(name), containing_rule_state);
+ // Register each layer individually.
+ containing_rule_state.restore(&saved_containing_rule_state);
+ }
+ },
+ CssRule::Style(..) => {
+ if let Some(s) = list_for_nested_rules {
+ containing_rule_state.ancestor_selector_lists.push(s);
+ }
+ },
+ CssRule::Container(ref rule) => {
+ let id = ContainerConditionId(self.container_conditions.len() as u16);
+ self.container_conditions.push(ContainerConditionReference {
+ parent: containing_rule_state.container_condition_id,
+ condition: Some(rule.condition.clone()),
+ });
+ containing_rule_state.container_condition_id = id;
+ },
+ // We don't care about any other rule.
+ _ => {},
+ }
+
+ if let Some(children) = children {
+ self.add_rule_list(
+ children,
+ device,
+ quirks_mode,
+ stylesheet,
+ guard,
+ rebuild_kind,
+ containing_rule_state,
+ precomputed_pseudo_element_decls.as_deref_mut(),
+ )?;
+ }
+
+ containing_rule_state.restore(&saved_containing_rule_state);
+ }
+
+ Ok(())
+ }
+
+ // Returns Err(..) to signify OOM
+ fn add_stylesheet<S>(
+ &mut self,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ stylesheet: &S,
+ guard: &SharedRwLockReadGuard,
+ rebuild_kind: SheetRebuildKind,
+ mut precomputed_pseudo_element_decls: Option<&mut PrecomputedPseudoElementDeclarations>,
+ ) -> Result<(), AllocErr>
+ where
+ S: StylesheetInDocument + 'static,
+ {
+ if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) {
+ return Ok(());
+ }
+
+ let contents = stylesheet.contents();
+
+ if rebuild_kind.should_rebuild_invalidation() {
+ self.effective_media_query_results.saw_effective(contents);
+ }
+
+ let mut state = ContainingRuleState::default();
+ self.add_rule_list(
+ contents.rules(guard).iter(),
+ device,
+ quirks_mode,
+ stylesheet,
+ guard,
+ rebuild_kind,
+ &mut state,
+ precomputed_pseudo_element_decls.as_deref_mut(),
+ )?;
+
+ Ok(())
+ }
+
+ /// Returns whether all the media-feature affected values matched before and
+ /// match now in the given stylesheet.
+ pub fn media_feature_affected_matches<S>(
+ &self,
+ stylesheet: &S,
+ guard: &SharedRwLockReadGuard,
+ device: &Device,
+ quirks_mode: QuirksMode,
+ ) -> bool
+ where
+ S: StylesheetInDocument + 'static,
+ {
+ use crate::invalidation::media_queries::PotentiallyEffectiveMediaRules;
+
+ let effective_now = stylesheet.is_effective_for_device(device, guard);
+
+ let effective_then = self
+ .effective_media_query_results
+ .was_effective(stylesheet.contents());
+
+ if effective_now != effective_then {
+ debug!(
+ " > Stylesheet {:?} changed -> {}, {}",
+ stylesheet.media(guard),
+ effective_then,
+ effective_now
+ );
+ return false;
+ }
+
+ if !effective_now {
+ return true;
+ }
+
+ let mut iter = stylesheet.iter_rules::<PotentiallyEffectiveMediaRules>(device, guard);
+
+ while let Some(rule) = iter.next() {
+ match *rule {
+ CssRule::Style(..) |
+ CssRule::Namespace(..) |
+ CssRule::FontFace(..) |
+ CssRule::Container(..) |
+ CssRule::CounterStyle(..) |
+ CssRule::Supports(..) |
+ CssRule::Keyframes(..) |
+ CssRule::Margin(..) |
+ CssRule::Page(..) |
+ CssRule::Property(..) |
+ CssRule::Document(..) |
+ CssRule::LayerBlock(..) |
+ CssRule::LayerStatement(..) |
+ CssRule::FontPaletteValues(..) |
+ CssRule::FontFeatureValues(..) => {
+ // Not affected by device changes.
+ continue;
+ },
+ CssRule::Import(ref lock) => {
+ let import_rule = lock.read_with(guard);
+ let effective_now = match import_rule.stylesheet.media(guard) {
+ Some(m) => m.evaluate(device, quirks_mode),
+ None => true,
+ };
+ let effective_then = self
+ .effective_media_query_results
+ .was_effective(import_rule);
+ if effective_now != effective_then {
+ debug!(
+ " > @import rule {:?} changed {} -> {}",
+ import_rule.stylesheet.media(guard),
+ effective_then,
+ effective_now
+ );
+ return false;
+ }
+
+ if !effective_now {
+ iter.skip_children();
+ }
+ },
+ CssRule::Media(ref media_rule) => {
+ let mq = media_rule.media_queries.read_with(guard);
+ let effective_now = mq.evaluate(device, quirks_mode);
+ let effective_then = self
+ .effective_media_query_results
+ .was_effective(&**media_rule);
+
+ if effective_now != effective_then {
+ debug!(
+ " > @media rule {:?} changed {} -> {}",
+ mq, effective_then, effective_now
+ );
+ return false;
+ }
+
+ if !effective_now {
+ iter.skip_children();
+ }
+ },
+ }
+ }
+
+ true
+ }
+
+ /// Returns the custom properties map.
+ pub fn custom_property_registrations(&self) -> &LayerOrderedMap<Arc<PropertyRegistration>> {
+ &self.custom_property_registrations
+ }
+
+ /// Clears the cascade data, but not the invalidation data.
+ fn clear_cascade_data(&mut self) {
+ self.normal_rules.clear();
+ if let Some(ref mut slotted_rules) = self.slotted_rules {
+ slotted_rules.clear();
+ }
+ if let Some(ref mut part_rules) = self.part_rules {
+ part_rules.clear();
+ }
+ if let Some(ref mut host_rules) = self.host_rules {
+ host_rules.clear();
+ }
+ self.animations.clear();
+ self.custom_property_registrations.clear();
+ self.layer_id.clear();
+ self.layers.clear();
+ self.layers.push(CascadeLayer::root());
+ self.container_conditions.clear();
+ self.container_conditions
+ .push(ContainerConditionReference::none());
+ self.extra_data.clear();
+ self.rules_source_order = 0;
+ self.num_selectors = 0;
+ self.num_declarations = 0;
+ }
+
+ fn clear(&mut self) {
+ self.clear_cascade_data();
+ self.invalidation_map.clear();
+ self.relative_selector_invalidation_map.clear();
+ self.attribute_dependencies.clear();
+ self.nth_of_attribute_dependencies.clear();
+ self.nth_of_class_dependencies.clear();
+ self.state_dependencies = ElementState::empty();
+ self.nth_of_state_dependencies = ElementState::empty();
+ self.document_state_dependencies = DocumentState::empty();
+ self.mapped_ids.clear();
+ self.nth_of_mapped_ids.clear();
+ self.selectors_for_cache_revalidation.clear();
+ self.effective_media_query_results.clear();
+ }
+}
+
+impl CascadeDataCacheEntry for CascadeData {
+ fn cascade_data(&self) -> &CascadeData {
+ self
+ }
+
+ fn rebuild<S>(
+ device: &Device,
+ quirks_mode: QuirksMode,
+ collection: SheetCollectionFlusher<S>,
+ guard: &SharedRwLockReadGuard,
+ old: &Self,
+ ) -> Result<Arc<Self>, AllocErr>
+ where
+ S: StylesheetInDocument + PartialEq + 'static,
+ {
+ debug_assert!(collection.dirty(), "We surely need to do something?");
+ // If we're doing a full rebuild anyways, don't bother cloning the data.
+ let mut updatable_entry = match collection.data_validity() {
+ DataValidity::Valid | DataValidity::CascadeInvalid => old.clone(),
+ DataValidity::FullyInvalid => Self::new(),
+ };
+ updatable_entry.rebuild(device, quirks_mode, collection, guard)?;
+ Ok(Arc::new(updatable_entry))
+ }
+
+ #[cfg(feature = "gecko")]
+ fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) {
+ self.normal_rules.add_size_of(ops, sizes);
+ if let Some(ref slotted_rules) = self.slotted_rules {
+ slotted_rules.add_size_of(ops, sizes);
+ }
+ if let Some(ref part_rules) = self.part_rules {
+ part_rules.add_size_of(ops, sizes);
+ }
+ if let Some(ref host_rules) = self.host_rules {
+ host_rules.add_size_of(ops, sizes);
+ }
+ sizes.mInvalidationMap += self.invalidation_map.size_of(ops);
+ sizes.mRevalidationSelectors += self.selectors_for_cache_revalidation.size_of(ops);
+ sizes.mOther += self.animations.size_of(ops);
+ sizes.mOther += self.effective_media_query_results.size_of(ops);
+ sizes.mOther += self.extra_data.size_of(ops);
+ }
+}
+
+impl Default for CascadeData {
+ fn default() -> Self {
+ CascadeData::new()
+ }
+}
+
+/// A rule, that wraps a style rule, but represents a single selector of the
+/// rule.
+#[derive(Clone, Debug, MallocSizeOf)]
+pub struct Rule {
+ /// The selector this struct represents. We store this and the
+ /// any_{important,normal} booleans inline in the Rule to avoid
+ /// pointer-chasing when gathering applicable declarations, which
+ /// can ruin performance when there are a lot of rules.
+ #[ignore_malloc_size_of = "CssRules have primary refs, we measure there"]
+ pub selector: Selector<SelectorImpl>,
+
+ /// The ancestor hashes associated with the selector.
+ pub hashes: AncestorHashes,
+
+ /// The source order this style rule appears in. Note that we only use
+ /// three bytes to store this value in ApplicableDeclarationsBlock, so
+ /// we could repurpose that storage here if we needed to.
+ pub source_order: u32,
+
+ /// The current layer id of this style rule.
+ pub layer_id: LayerId,
+
+ /// The current @container rule id.
+ pub container_condition_id: ContainerConditionId,
+
+ /// The actual style rule.
+ #[cfg_attr(
+ feature = "gecko",
+ ignore_malloc_size_of = "Secondary ref. Primary ref is in StyleRule under Stylesheet."
+ )]
+ #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")]
+ pub style_rule: Arc<Locked<StyleRule>>,
+}
+
+impl SelectorMapEntry for Rule {
+ fn selector(&self) -> SelectorIter<SelectorImpl> {
+ self.selector.iter()
+ }
+}
+
+impl Rule {
+ /// Returns the specificity of the rule.
+ pub fn specificity(&self) -> u32 {
+ self.selector.specificity()
+ }
+
+ /// Turns this rule into an `ApplicableDeclarationBlock` for the given
+ /// cascade level.
+ pub fn to_applicable_declaration_block(
+ &self,
+ level: CascadeLevel,
+ cascade_data: &CascadeData,
+ ) -> ApplicableDeclarationBlock {
+ let source = StyleSource::from_rule(self.style_rule.clone());
+ ApplicableDeclarationBlock::new(
+ source,
+ self.source_order,
+ level,
+ self.specificity(),
+ cascade_data.layer_order_for(self.layer_id),
+ )
+ }
+
+ /// Creates a new Rule.
+ pub fn new(
+ selector: Selector<SelectorImpl>,
+ hashes: AncestorHashes,
+ style_rule: Arc<Locked<StyleRule>>,
+ source_order: u32,
+ layer_id: LayerId,
+ container_condition_id: ContainerConditionId,
+ ) -> Self {
+ Rule {
+ selector,
+ hashes,
+ style_rule,
+ source_order,
+ layer_id,
+ container_condition_id,
+ }
+ }
+}
+
+// The size of this is critical to performance on the bloom-basic
+// microbenchmark.
+// When iterating over a large Rule array, we want to be able to fast-reject
+// selectors (with the inline hashes) with as few cache misses as possible.
+size_of_test!(Rule, 40);
+
+/// A function to be able to test the revalidation stuff.
+pub fn needs_revalidation_for_testing(s: &Selector<SelectorImpl>) -> bool {
+ let mut needs_revalidation = false;
+ let mut mapped_ids = Default::default();
+ let mut nth_of_mapped_ids = Default::default();
+ let mut attribute_dependencies = Default::default();
+ let mut nth_of_class_dependencies = Default::default();
+ let mut nth_of_attribute_dependencies = Default::default();
+ let mut state_dependencies = ElementState::empty();
+ let mut nth_of_state_dependencies = ElementState::empty();
+ let mut document_state_dependencies = DocumentState::empty();
+ let mut visitor = StylistSelectorVisitor {
+ passed_rightmost_selector: false,
+ needs_revalidation: &mut needs_revalidation,
+ in_selector_list_of: SelectorListKind::default(),
+ mapped_ids: &mut mapped_ids,
+ nth_of_mapped_ids: &mut nth_of_mapped_ids,
+ attribute_dependencies: &mut attribute_dependencies,
+ nth_of_class_dependencies: &mut nth_of_class_dependencies,
+ nth_of_attribute_dependencies: &mut nth_of_attribute_dependencies,
+ state_dependencies: &mut state_dependencies,
+ nth_of_state_dependencies: &mut nth_of_state_dependencies,
+ document_state_dependencies: &mut document_state_dependencies,
+ };
+ s.visit(&mut visitor);
+ needs_revalidation
+}
diff --git a/servo/components/style/thread_state.rs b/servo/components/style/thread_state.rs
new file mode 100644
index 0000000000..e07a567fe7
--- /dev/null
+++ b/servo/components/style/thread_state.rs
@@ -0,0 +1,98 @@
+/* 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/. */
+
+//! Supports dynamic assertions in about what sort of thread is running and
+//! what state it's in.
+
+#![deny(missing_docs)]
+
+use std::cell::RefCell;
+
+bitflags! {
+ /// A thread state flag, used for multiple assertions.
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
+ pub struct ThreadState: u32 {
+ /// Whether we're in a script thread.
+ const SCRIPT = 0x01;
+ /// Whether we're in a layout thread.
+ const LAYOUT = 0x02;
+
+ /// Whether we're in a script worker thread (actual web workers), or in
+ /// a layout worker thread.
+ const IN_WORKER = 0x0100;
+
+ /// Whether the current thread is going through a GC.
+ const IN_GC = 0x0200;
+ }
+}
+
+macro_rules! thread_types ( ( $( $fun:ident = $flag:path ; )* ) => (
+ impl ThreadState {
+ /// Whether the current thread is a worker thread.
+ pub fn is_worker(self) -> bool {
+ self.contains(ThreadState::IN_WORKER)
+ }
+
+ $(
+ #[allow(missing_docs)]
+ pub fn $fun(self) -> bool {
+ self.contains($flag)
+ }
+ )*
+ }
+));
+
+thread_types! {
+ is_script = ThreadState::SCRIPT;
+ is_layout = ThreadState::LAYOUT;
+}
+
+thread_local!(static STATE: RefCell<Option<ThreadState>> = RefCell::new(None));
+
+/// Initializes the current thread state.
+pub fn initialize(x: ThreadState) {
+ STATE.with(|ref k| {
+ if let Some(ref s) = *k.borrow() {
+ if x != *s {
+ panic!("Thread state already initialized as {:?}", s);
+ }
+ }
+ *k.borrow_mut() = Some(x);
+ });
+}
+
+/// Initializes the current thread as a layout worker thread.
+pub fn initialize_layout_worker_thread() {
+ initialize(ThreadState::LAYOUT | ThreadState::IN_WORKER);
+}
+
+/// Gets the current thread state.
+pub fn get() -> ThreadState {
+ let state = STATE.with(|ref k| {
+ match *k.borrow() {
+ None => ThreadState::empty(), // Unknown thread.
+ Some(s) => s,
+ }
+ });
+
+ state
+}
+
+/// Enters into a given temporary state. Panics if re-entring.
+pub fn enter(x: ThreadState) {
+ let state = get();
+ debug_assert!(!state.intersects(x));
+ STATE.with(|ref k| {
+ *k.borrow_mut() = Some(state | x);
+ })
+}
+
+/// Exits a given temporary state.
+pub fn exit(x: ThreadState) {
+ let state = get();
+ debug_assert!(state.contains(x));
+ STATE.with(|ref k| {
+ *k.borrow_mut() = Some(state & !x);
+ })
+}
diff --git a/servo/components/style/traversal.rs b/servo/components/style/traversal.rs
new file mode 100644
index 0000000000..d63c3cb965
--- /dev/null
+++ b/servo/components/style/traversal.rs
@@ -0,0 +1,842 @@
+/* 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/. */
+
+//! Traversing the DOM tree; the bloom filter.
+
+use crate::context::{ElementCascadeInputs, SharedStyleContext, StyleContext};
+use crate::data::{ElementData, ElementStyles, RestyleKind};
+use crate::dom::{NodeInfo, OpaqueNode, TElement, TNode};
+use crate::invalidation::element::restyle_hints::RestyleHint;
+use crate::matching::{ChildRestyleRequirement, MatchMethods};
+use crate::selector_parser::PseudoElement;
+use crate::sharing::StyleSharingTarget;
+use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement};
+use crate::stylist::RuleInclusion;
+use crate::traversal_flags::TraversalFlags;
+use selectors::matching::SelectorCaches;
+use smallvec::SmallVec;
+use std::collections::HashMap;
+
+/// A cache from element reference to known-valid computed style.
+pub type UndisplayedStyleCache =
+ HashMap<selectors::OpaqueElement, servo_arc::Arc<crate::properties::ComputedValues>>;
+
+/// A per-traversal-level chunk of data. This is sent down by the traversal, and
+/// currently only holds the dom depth for the bloom filter.
+///
+/// NB: Keep this as small as possible, please!
+#[derive(Clone, Copy, Debug)]
+pub struct PerLevelTraversalData {
+ /// The current dom depth.
+ ///
+ /// This is kept with cooperation from the traversal code and the bloom
+ /// filter.
+ pub current_dom_depth: usize,
+}
+
+/// We use this structure, rather than just returning a boolean from pre_traverse,
+/// to enfore that callers process root invalidations before starting the traversal.
+pub struct PreTraverseToken<E: TElement>(Option<E>);
+impl<E: TElement> PreTraverseToken<E> {
+ /// Whether we should traverse children.
+ pub fn should_traverse(&self) -> bool {
+ self.0.is_some()
+ }
+
+ /// Returns the traversal root for the current traversal.
+ pub(crate) fn traversal_root(self) -> Option<E> {
+ self.0
+ }
+}
+
+/// A global variable holding the state of
+/// `is_servo_nonincremental_layout()`.
+/// See [#22854](https://github.com/servo/servo/issues/22854).
+#[cfg(feature = "servo")]
+pub static IS_SERVO_NONINCREMENTAL_LAYOUT: std::sync::atomic::AtomicBool =
+ std::sync::atomic::AtomicBool::new(false);
+
+#[cfg(feature = "servo")]
+#[inline]
+fn is_servo_nonincremental_layout() -> bool {
+ use std::sync::atomic::Ordering;
+
+ IS_SERVO_NONINCREMENTAL_LAYOUT.load(Ordering::Relaxed)
+}
+
+#[cfg(not(feature = "servo"))]
+#[inline]
+fn is_servo_nonincremental_layout() -> bool {
+ false
+}
+
+/// A DOM Traversal trait, that is used to generically implement styling for
+/// Gecko and Servo.
+pub trait DomTraversal<E: TElement>: Sync {
+ /// Process `node` on the way down, before its children have been processed.
+ ///
+ /// The callback is invoked for each child node that should be processed by
+ /// the traversal.
+ fn process_preorder<F>(
+ &self,
+ data: &PerLevelTraversalData,
+ context: &mut StyleContext<E>,
+ node: E::ConcreteNode,
+ note_child: F,
+ ) where
+ F: FnMut(E::ConcreteNode);
+
+ /// Process `node` on the way up, after its children have been processed.
+ ///
+ /// This is only executed if `needs_postorder_traversal` returns true.
+ fn process_postorder(&self, contect: &mut StyleContext<E>, node: E::ConcreteNode);
+
+ /// Boolean that specifies whether a bottom up traversal should be
+ /// performed.
+ ///
+ /// If it's false, then process_postorder has no effect at all.
+ fn needs_postorder_traversal() -> bool {
+ true
+ }
+
+ /// Handles the postorder step of the traversal, if it exists, by bubbling
+ /// up the parent chain.
+ ///
+ /// If we are the last child that finished processing, recursively process
+ /// our parent. Else, stop. Also, stop at the root.
+ ///
+ /// Thus, if we start with all the leaves of a tree, we end up traversing
+ /// the whole tree bottom-up because each parent will be processed exactly
+ /// once (by the last child that finishes processing).
+ ///
+ /// The only communication between siblings is that they both
+ /// fetch-and-subtract the parent's children count. This makes it safe to
+ /// call durign the parallel traversal.
+ fn handle_postorder_traversal(
+ &self,
+ context: &mut StyleContext<E>,
+ root: OpaqueNode,
+ mut node: E::ConcreteNode,
+ children_to_process: isize,
+ ) {
+ // If the postorder step is a no-op, don't bother.
+ if !Self::needs_postorder_traversal() {
+ return;
+ }
+
+ if children_to_process == 0 {
+ // We are a leaf. Walk up the chain.
+ loop {
+ self.process_postorder(context, node);
+ if node.opaque() == root {
+ break;
+ }
+ let parent = node.traversal_parent().unwrap();
+ let remaining = parent.did_process_child();
+ if remaining != 0 {
+ // The parent has other unprocessed descendants. We only
+ // perform postorder processing after the last descendant
+ // has been processed.
+ break;
+ }
+
+ node = parent.as_node();
+ }
+ } else {
+ // Otherwise record the number of children to process when the time
+ // comes.
+ node.as_element()
+ .unwrap()
+ .store_children_to_process(children_to_process);
+ }
+ }
+
+ /// Style invalidations happen when traversing from a parent to its children.
+ /// However, this mechanism can't handle style invalidations on the root. As
+ /// such, we have a pre-traversal step to handle that part and determine whether
+ /// a full traversal is needed.
+ fn pre_traverse(root: E, shared_context: &SharedStyleContext) -> PreTraverseToken<E> {
+ use crate::invalidation::element::state_and_attributes::propagate_dirty_bit_up_to;
+
+ let traversal_flags = shared_context.traversal_flags;
+
+ let mut data = root.mutate_data();
+ let mut data = data.as_mut().map(|d| &mut **d);
+
+ if let Some(ref mut data) = data {
+ if !traversal_flags.for_animation_only() {
+ // Invalidate our style, and that of our siblings and
+ // descendants as needed.
+ let invalidation_result = data.invalidate_style_if_needed(
+ root,
+ shared_context,
+ None,
+ &mut SelectorCaches::default(),
+ );
+
+ if invalidation_result.has_invalidated_siblings() {
+ let actual_root = root.as_node().parent_element_or_host().expect(
+ "How in the world can you invalidate \
+ siblings without a parent?",
+ );
+ propagate_dirty_bit_up_to(actual_root, root);
+ return PreTraverseToken(Some(actual_root));
+ }
+ }
+ }
+
+ let should_traverse =
+ Self::element_needs_traversal(root, traversal_flags, data.as_mut().map(|d| &**d));
+
+ // If we're not going to traverse at all, we may need to clear some state
+ // off the root (which would normally be done at the end of recalc_style_at).
+ if !should_traverse && data.is_some() {
+ clear_state_after_traversing(root, data.unwrap(), traversal_flags);
+ }
+
+ PreTraverseToken(if should_traverse { Some(root) } else { None })
+ }
+
+ /// Returns true if traversal should visit a text node. The style system
+ /// never processes text nodes, but Servo overrides this to visit them for
+ /// flow construction when necessary.
+ fn text_node_needs_traversal(node: E::ConcreteNode, _parent_data: &ElementData) -> bool {
+ debug_assert!(node.is_text_node());
+ false
+ }
+
+ /// Returns true if traversal is needed for the given element and subtree.
+ fn element_needs_traversal(
+ el: E,
+ traversal_flags: TraversalFlags,
+ data: Option<&ElementData>,
+ ) -> bool {
+ debug!(
+ "element_needs_traversal({:?}, {:?}, {:?})",
+ el, traversal_flags, data
+ );
+
+ // In case of animation-only traversal we need to traverse the element if the element has
+ // animation only dirty descendants bit, animation-only restyle hint.
+ if traversal_flags.for_animation_only() {
+ return data.map_or(false, |d| d.has_styles()) &&
+ (el.has_animation_only_dirty_descendants() ||
+ data.as_ref()
+ .unwrap()
+ .hint
+ .has_animation_hint_or_recascade());
+ }
+
+ // Non-incremental layout visits every node.
+ if is_servo_nonincremental_layout() {
+ return true;
+ }
+
+ // Unwrap the data.
+ let data = match data {
+ Some(d) if d.has_styles() => d,
+ _ => return true,
+ };
+
+ // If the dirty descendants bit is set, we need to traverse no matter
+ // what. Skip examining the ElementData.
+ if el.has_dirty_descendants() {
+ return true;
+ }
+
+ // If we have a restyle hint or need to recascade, we need to visit the
+ // element.
+ //
+ // Note that this is different than checking has_current_styles_for_traversal(),
+ // since that can return true even if we have a restyle hint indicating
+ // that the element's descendants (but not necessarily the element) need
+ // restyling.
+ if !data.hint.is_empty() {
+ return true;
+ }
+
+ // Servo uses the post-order traversal for flow construction, so we need
+ // to traverse any element with damage so that we can perform fixup /
+ // reconstruction on our way back up the tree.
+ if cfg!(feature = "servo") && !data.damage.is_empty() {
+ return true;
+ }
+
+ trace!("{:?} doesn't need traversal", el);
+ false
+ }
+
+ /// Return the shared style context common to all worker threads.
+ fn shared_context(&self) -> &SharedStyleContext;
+}
+
+/// Manually resolve style by sequentially walking up the parent chain to the
+/// first styled Element, ignoring pending restyles. The resolved style is made
+/// available via a callback, and can be dropped by the time this function
+/// returns in the display:none subtree case.
+pub fn resolve_style<E>(
+ context: &mut StyleContext<E>,
+ element: E,
+ rule_inclusion: RuleInclusion,
+ pseudo: Option<&PseudoElement>,
+ mut undisplayed_style_cache: Option<&mut UndisplayedStyleCache>,
+) -> ElementStyles
+where
+ E: TElement,
+{
+ debug_assert!(
+ rule_inclusion == RuleInclusion::DefaultOnly ||
+ pseudo.map_or(false, |p| p.is_before_or_after()) ||
+ element.borrow_data().map_or(true, |d| !d.has_styles()),
+ "Why are we here?"
+ );
+ debug_assert!(
+ rule_inclusion == RuleInclusion::All || undisplayed_style_cache.is_none(),
+ "can't use the cache for default styles only"
+ );
+
+ let mut ancestors_requiring_style_resolution = SmallVec::<[E; 16]>::new();
+
+ // Clear the bloom filter, just in case the caller is reusing TLS.
+ context.thread_local.bloom_filter.clear();
+
+ let mut style = None;
+ let mut ancestor = element.traversal_parent();
+ while let Some(current) = ancestor {
+ if rule_inclusion == RuleInclusion::All {
+ if let Some(data) = current.borrow_data() {
+ if let Some(ancestor_style) = data.styles.get_primary() {
+ style = Some(ancestor_style.clone());
+ break;
+ }
+ }
+ }
+ if let Some(ref mut cache) = undisplayed_style_cache {
+ if let Some(s) = cache.get(&current.opaque()) {
+ style = Some(s.clone());
+ break;
+ }
+ }
+ ancestors_requiring_style_resolution.push(current);
+ ancestor = current.traversal_parent();
+ }
+
+ if let Some(ancestor) = ancestor {
+ context.thread_local.bloom_filter.rebuild(ancestor);
+ context.thread_local.bloom_filter.push(ancestor);
+ }
+
+ let mut layout_parent_style = style.clone();
+ while let Some(style) = layout_parent_style.take() {
+ if !style.is_display_contents() {
+ layout_parent_style = Some(style);
+ break;
+ }
+
+ ancestor = ancestor.unwrap().traversal_parent();
+ layout_parent_style =
+ ancestor.and_then(|a| a.borrow_data().map(|data| data.styles.primary().clone()));
+ }
+
+ for ancestor in ancestors_requiring_style_resolution.iter().rev() {
+ context.thread_local.bloom_filter.assert_complete(*ancestor);
+
+ // Actually `PseudoElementResolution` doesn't really matter here.
+ // (but it does matter below!).
+ let primary_style = StyleResolverForElement::new(
+ *ancestor,
+ context,
+ rule_inclusion,
+ PseudoElementResolution::IfApplicable,
+ )
+ .resolve_primary_style(style.as_deref(), layout_parent_style.as_deref());
+
+ let is_display_contents = primary_style.style().is_display_contents();
+
+ style = Some(primary_style.style.0);
+ if !is_display_contents {
+ layout_parent_style = style.clone();
+ }
+
+ if let Some(ref mut cache) = undisplayed_style_cache {
+ cache.insert(ancestor.opaque(), style.clone().unwrap());
+ }
+ context.thread_local.bloom_filter.push(*ancestor);
+ }
+
+ context.thread_local.bloom_filter.assert_complete(element);
+ let styles: ElementStyles = StyleResolverForElement::new(
+ element,
+ context,
+ rule_inclusion,
+ PseudoElementResolution::Force,
+ )
+ .resolve_style(style.as_deref(), layout_parent_style.as_deref())
+ .into();
+
+ if let Some(ref mut cache) = undisplayed_style_cache {
+ cache.insert(element.opaque(), styles.primary().clone());
+ }
+
+ styles
+}
+
+/// Calculates the style for a single node.
+#[inline]
+#[allow(unsafe_code)]
+pub fn recalc_style_at<E, D, F>(
+ _traversal: &D,
+ traversal_data: &PerLevelTraversalData,
+ context: &mut StyleContext<E>,
+ element: E,
+ data: &mut ElementData,
+ note_child: F,
+) where
+ E: TElement,
+ D: DomTraversal<E>,
+ F: FnMut(E::ConcreteNode),
+{
+ use std::cmp;
+
+ let flags = context.shared.traversal_flags;
+ let is_initial_style = !data.has_styles();
+
+ context.thread_local.statistics.elements_traversed += 1;
+ debug_assert!(
+ flags.intersects(TraversalFlags::AnimationOnly) ||
+ is_initial_style ||
+ !element.has_snapshot() ||
+ element.handled_snapshot(),
+ "Should've handled snapshots here already"
+ );
+
+ let restyle_kind = data.restyle_kind(&context.shared);
+ debug!(
+ "recalc_style_at: {:?} (restyle_kind={:?}, dirty_descendants={:?}, data={:?})",
+ element,
+ restyle_kind,
+ element.has_dirty_descendants(),
+ data
+ );
+
+ let mut child_restyle_requirement = ChildRestyleRequirement::CanSkipCascade;
+
+ // Compute style for this element if necessary.
+ if let Some(restyle_kind) = restyle_kind {
+ child_restyle_requirement =
+ compute_style(traversal_data, context, element, data, restyle_kind);
+
+ if !element.matches_user_and_content_rules() {
+ // We must always cascade native anonymous subtrees, since they
+ // may have pseudo-elements underneath that would inherit from the
+ // closest non-NAC ancestor instead of us.
+ child_restyle_requirement = cmp::max(
+ child_restyle_requirement,
+ ChildRestyleRequirement::MustCascadeChildren,
+ );
+ }
+
+ // If we're restyling this element to display:none, throw away all style
+ // data in the subtree, notify the caller to early-return.
+ if data.styles.is_display_none() {
+ debug!(
+ "{:?} style is display:none - clearing data from descendants.",
+ element
+ );
+ unsafe {
+ clear_descendant_data(element);
+ }
+ }
+
+ // Inform any paint worklets of changed style, to speculatively
+ // evaluate the worklet code. In the case that the size hasn't changed,
+ // this will result in increased concurrency between script and layout.
+ notify_paint_worklet(context, data);
+ } else {
+ debug_assert!(data.has_styles());
+ data.set_traversed_without_styling();
+ }
+
+ // Now that matching and cascading is done, clear the bits corresponding to
+ // those operations and compute the propagated restyle hint (unless we're
+ // not processing invalidations, in which case don't need to propagate it
+ // and must avoid clearing it).
+ debug_assert!(
+ flags.for_animation_only() || !data.hint.has_animation_hint(),
+ "animation restyle hint should be handled during \
+ animation-only restyles"
+ );
+ let mut propagated_hint = data.hint.propagate(&flags);
+ trace!(
+ "propagated_hint={:?}, restyle_requirement={:?}, \
+ is_display_none={:?}, implementing_pseudo={:?}",
+ propagated_hint,
+ child_restyle_requirement,
+ data.styles.is_display_none(),
+ element.implemented_pseudo_element()
+ );
+
+ // Integrate the child cascade requirement into the propagated hint.
+ match child_restyle_requirement {
+ ChildRestyleRequirement::CanSkipCascade => {},
+ ChildRestyleRequirement::MustCascadeDescendants => {
+ propagated_hint |= RestyleHint::RECASCADE_SELF | RestyleHint::RECASCADE_DESCENDANTS;
+ },
+ ChildRestyleRequirement::MustCascadeChildrenIfInheritResetStyle => {
+ propagated_hint |= RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE;
+ },
+ ChildRestyleRequirement::MustCascadeChildren => {
+ propagated_hint |= RestyleHint::RECASCADE_SELF;
+ },
+ ChildRestyleRequirement::MustMatchDescendants => {
+ propagated_hint |= RestyleHint::restyle_subtree();
+ },
+ }
+
+ let has_dirty_descendants_for_this_restyle = if flags.for_animation_only() {
+ element.has_animation_only_dirty_descendants()
+ } else {
+ element.has_dirty_descendants()
+ };
+
+ // Before examining each child individually, try to prove that our children
+ // don't need style processing. They need processing if any of the following
+ // conditions hold:
+ //
+ // * We have the dirty descendants bit.
+ // * We're propagating a restyle hint.
+ // * This is a servo non-incremental traversal.
+ //
+ // We only do this if we're not a display: none root, since in that case
+ // it's useless to style children.
+ let mut traverse_children = has_dirty_descendants_for_this_restyle ||
+ !propagated_hint.is_empty() ||
+ is_servo_nonincremental_layout();
+
+ traverse_children = traverse_children && !data.styles.is_display_none();
+
+ // Examine our children, and enqueue the appropriate ones for traversal.
+ if traverse_children {
+ note_children::<E, D, F>(
+ context,
+ element,
+ data,
+ propagated_hint,
+ is_initial_style,
+ note_child,
+ );
+ }
+
+ // FIXME(bholley): Make these assertions pass for servo.
+ if cfg!(feature = "gecko") && cfg!(debug_assertions) && data.styles.is_display_none() {
+ debug_assert!(!element.has_dirty_descendants());
+ debug_assert!(!element.has_animation_only_dirty_descendants());
+ }
+
+ clear_state_after_traversing(element, data, flags);
+}
+
+fn clear_state_after_traversing<E>(element: E, data: &mut ElementData, flags: TraversalFlags)
+where
+ E: TElement,
+{
+ if flags.intersects(TraversalFlags::FinalAnimationTraversal) {
+ debug_assert!(flags.for_animation_only());
+ data.clear_restyle_flags_and_damage();
+ unsafe {
+ element.unset_animation_only_dirty_descendants();
+ }
+ }
+}
+
+fn compute_style<E>(
+ traversal_data: &PerLevelTraversalData,
+ context: &mut StyleContext<E>,
+ element: E,
+ data: &mut ElementData,
+ kind: RestyleKind,
+) -> ChildRestyleRequirement
+where
+ E: TElement,
+{
+ use crate::data::RestyleKind::*;
+
+ context.thread_local.statistics.elements_styled += 1;
+ debug!("compute_style: {:?} (kind={:?})", element, kind);
+
+ if data.has_styles() {
+ data.set_restyled();
+ }
+
+ let mut important_rules_changed = false;
+ let new_styles = match kind {
+ MatchAndCascade => {
+ debug_assert!(
+ !context.shared.traversal_flags.for_animation_only(),
+ "MatchAndCascade shouldn't be processed during \
+ animation-only traversal"
+ );
+ // Ensure the bloom filter is up to date.
+ context
+ .thread_local
+ .bloom_filter
+ .insert_parents_recovering(element, traversal_data.current_dom_depth);
+
+ context.thread_local.bloom_filter.assert_complete(element);
+ debug_assert_eq!(
+ context.thread_local.bloom_filter.matching_depth(),
+ traversal_data.current_dom_depth
+ );
+
+ // This is only relevant for animations as of right now.
+ important_rules_changed = true;
+
+ let mut target = StyleSharingTarget::new(element);
+
+ // Now that our bloom filter is set up, try the style sharing
+ // cache.
+ match target.share_style_if_possible(context) {
+ Some(shared_styles) => {
+ context.thread_local.statistics.styles_shared += 1;
+ shared_styles
+ },
+ None => {
+ context.thread_local.statistics.elements_matched += 1;
+ // Perform the matching and cascading.
+ let new_styles = {
+ let mut resolver = StyleResolverForElement::new(
+ element,
+ context,
+ RuleInclusion::All,
+ PseudoElementResolution::IfApplicable,
+ );
+
+ resolver.resolve_style_with_default_parents()
+ };
+
+ context.thread_local.sharing_cache.insert_if_possible(
+ &element,
+ &new_styles.primary,
+ Some(&mut target),
+ traversal_data.current_dom_depth,
+ &context.shared,
+ );
+
+ new_styles
+ },
+ }
+ },
+ CascadeWithReplacements(flags) => {
+ // Skipping full matching, load cascade inputs from previous values.
+ let mut cascade_inputs = ElementCascadeInputs::new_from_element_data(data);
+ important_rules_changed = element.replace_rules(flags, context, &mut cascade_inputs);
+
+ let mut resolver = StyleResolverForElement::new(
+ element,
+ context,
+ RuleInclusion::All,
+ PseudoElementResolution::IfApplicable,
+ );
+
+ resolver.cascade_styles_with_default_parents(cascade_inputs)
+ },
+ CascadeOnly => {
+ // Skipping full matching, load cascade inputs from previous values.
+ let cascade_inputs = ElementCascadeInputs::new_from_element_data(data);
+
+ let new_styles = {
+ let mut resolver = StyleResolverForElement::new(
+ element,
+ context,
+ RuleInclusion::All,
+ PseudoElementResolution::IfApplicable,
+ );
+
+ resolver.cascade_styles_with_default_parents(cascade_inputs)
+ };
+
+ // Insert into the cache, but only if this style isn't reused from a
+ // sibling or cousin. Otherwise, recascading a bunch of identical
+ // elements would unnecessarily flood the cache with identical entries.
+ //
+ // This is analogous to the obvious "don't insert an element that just
+ // got a hit in the style sharing cache" behavior in the MatchAndCascade
+ // handling above.
+ //
+ // Note that, for the MatchAndCascade path, we still insert elements that
+ // shared styles via the rule node, because we know that there's something
+ // different about them that caused them to miss the sharing cache before
+ // selector matching. If we didn't, we would still end up with the same
+ // number of eventual styles, but would potentially miss out on various
+ // opportunities for skipping selector matching, which could hurt
+ // performance.
+ if !new_styles.primary.reused_via_rule_node {
+ context.thread_local.sharing_cache.insert_if_possible(
+ &element,
+ &new_styles.primary,
+ None,
+ traversal_data.current_dom_depth,
+ &context.shared,
+ );
+ }
+
+ new_styles
+ },
+ };
+
+ element.finish_restyle(context, data, new_styles, important_rules_changed)
+}
+
+#[cfg(feature = "servo-layout-2013")]
+fn notify_paint_worklet<E>(context: &StyleContext<E>, data: &ElementData)
+where
+ E: TElement,
+{
+ use crate::values::generics::image::Image;
+ use style_traits::ToCss;
+
+ // We speculatively evaluate any paint worklets during styling.
+ // This allows us to run paint worklets in parallel with style and layout.
+ // Note that this is wasted effort if the size of the node has
+ // changed, but in may cases it won't have.
+ if let Some(ref values) = data.styles.primary {
+ for image in &values.get_background().background_image.0 {
+ let (name, arguments) = match *image {
+ Image::PaintWorklet(ref worklet) => (&worklet.name, &worklet.arguments),
+ _ => continue,
+ };
+ let painter = match context.shared.registered_speculative_painters.get(name) {
+ Some(painter) => painter,
+ None => continue,
+ };
+ let properties = painter
+ .properties()
+ .iter()
+ .filter_map(|(name, id)| id.as_shorthand().err().map(|id| (name, id)))
+ .map(|(name, id)| (name.clone(), values.computed_value_to_string(id)))
+ .collect();
+ let arguments = arguments
+ .iter()
+ .map(|argument| argument.to_css_string())
+ .collect();
+ debug!("Notifying paint worklet {}.", painter.name());
+ painter.speculatively_draw_a_paint_image(properties, arguments);
+ }
+ }
+}
+
+#[cfg(not(feature = "servo-layout-2013"))]
+fn notify_paint_worklet<E>(_context: &StyleContext<E>, _data: &ElementData)
+where
+ E: TElement,
+{
+ // The CSS paint API is Servo-only at the moment
+}
+
+fn note_children<E, D, F>(
+ context: &mut StyleContext<E>,
+ element: E,
+ data: &ElementData,
+ propagated_hint: RestyleHint,
+ is_initial_style: bool,
+ mut note_child: F,
+) where
+ E: TElement,
+ D: DomTraversal<E>,
+ F: FnMut(E::ConcreteNode),
+{
+ trace!("note_children: {:?}", element);
+ let flags = context.shared.traversal_flags;
+
+ // Loop over all the traversal children.
+ for child_node in element.traversal_children() {
+ let child = match child_node.as_element() {
+ Some(el) => el,
+ None => {
+ if is_servo_nonincremental_layout() ||
+ D::text_node_needs_traversal(child_node, data)
+ {
+ note_child(child_node);
+ }
+ continue;
+ },
+ };
+
+ let mut child_data = child.mutate_data();
+ let mut child_data = child_data.as_mut().map(|d| &mut **d);
+ trace!(
+ " > {:?} -> {:?} + {:?}, pseudo: {:?}",
+ child,
+ child_data.as_ref().map(|d| d.hint),
+ propagated_hint,
+ child.implemented_pseudo_element()
+ );
+
+ if let Some(ref mut child_data) = child_data {
+ child_data.hint.insert(propagated_hint);
+
+ // Handle element snapshots and invalidation of descendants and siblings
+ // as needed.
+ //
+ // NB: This will be a no-op if there's no snapshot.
+ child_data.invalidate_style_if_needed(
+ child,
+ &context.shared,
+ Some(&context.thread_local.stack_limit_checker),
+ &mut context.thread_local.selector_caches,
+ );
+ }
+
+ if D::element_needs_traversal(child, flags, child_data.map(|d| &*d)) {
+ note_child(child_node);
+
+ // Set the dirty descendants bit on the parent as needed, so that we
+ // can find elements during the post-traversal.
+ //
+ // Note that these bits may be cleared again at the bottom of
+ // recalc_style_at if requested by the caller.
+ if !is_initial_style {
+ if flags.for_animation_only() {
+ unsafe {
+ element.set_animation_only_dirty_descendants();
+ }
+ } else {
+ unsafe {
+ element.set_dirty_descendants();
+ }
+ }
+ }
+ }
+ }
+}
+
+/// Clear style data for all the subtree under `root` (but not for root itself).
+///
+/// We use a list to avoid unbounded recursion, which we need to avoid in the
+/// parallel traversal because the rayon stacks are small.
+pub unsafe fn clear_descendant_data<E>(root: E)
+where
+ E: TElement,
+{
+ let mut parents = SmallVec::<[E; 32]>::new();
+ parents.push(root);
+ while let Some(p) = parents.pop() {
+ for kid in p.traversal_children() {
+ if let Some(kid) = kid.as_element() {
+ // We maintain an invariant that, if an element has data, all its
+ // ancestors have data as well.
+ //
+ // By consequence, any element without data has no descendants with
+ // data.
+ if kid.has_data() {
+ kid.clear_data();
+ parents.push(kid);
+ }
+ }
+ }
+ }
+
+ // Make sure not to clear NODE_NEEDS_FRAME on the root.
+ root.clear_descendant_bits();
+}
diff --git a/servo/components/style/traversal_flags.rs b/servo/components/style/traversal_flags.rs
new file mode 100644
index 0000000000..54972043fd
--- /dev/null
+++ b/servo/components/style/traversal_flags.rs
@@ -0,0 +1,68 @@
+/* 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/. */
+
+//! Flags that control the traversal process.
+//!
+//! We CamelCase rather than UPPER_CASING so that we can grep for the same
+//! strings across gecko and servo.
+#![allow(non_upper_case_globals)]
+
+bitflags! {
+ /// Flags that control the traversal process.
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
+ pub struct TraversalFlags: u32 {
+ /// Traverse only elements for animation restyles.
+ const AnimationOnly = 1 << 0;
+ /// Traverse and update all elements with CSS animations since
+ /// @keyframes rules may have changed. Triggered by CSS rule changes.
+ const ForCSSRuleChanges = 1 << 1;
+ /// The final animation-only traversal, which shouldn't really care about other
+ /// style changes anymore.
+ const FinalAnimationTraversal = 1 << 2;
+ /// Allows the traversal to run in parallel if there are sufficient cores on
+ /// the machine.
+ const ParallelTraversal = 1 << 7;
+ /// Flush throttled animations. By default, we only update throttled animations
+ /// when we have other non-throttled work to do. With this flag, we
+ /// unconditionally tick and process them.
+ const FlushThrottledAnimations = 1 << 8;
+
+ }
+}
+
+/// Asserts that all TraversalFlags flags have a matching ServoTraversalFlags value in gecko.
+#[cfg(feature = "gecko")]
+#[inline]
+pub fn assert_traversal_flags_match() {
+ use crate::gecko_bindings::structs;
+
+ macro_rules! check_traversal_flags {
+ ( $( $a:ident => $b:path ),*, ) => {
+ if cfg!(debug_assertions) {
+ let mut modes = TraversalFlags::all();
+ $(
+ assert_eq!(structs::$a as usize, $b.bits() as usize, stringify!($b));
+ modes.remove($b);
+ )*
+ assert_eq!(modes, TraversalFlags::empty(), "all TraversalFlags bits should have an assertion");
+ }
+ }
+ }
+
+ check_traversal_flags! {
+ ServoTraversalFlags_AnimationOnly => TraversalFlags::AnimationOnly,
+ ServoTraversalFlags_ForCSSRuleChanges => TraversalFlags::ForCSSRuleChanges,
+ ServoTraversalFlags_FinalAnimationTraversal => TraversalFlags::FinalAnimationTraversal,
+ ServoTraversalFlags_ParallelTraversal => TraversalFlags::ParallelTraversal,
+ ServoTraversalFlags_FlushThrottledAnimations => TraversalFlags::FlushThrottledAnimations,
+ }
+}
+
+impl TraversalFlags {
+ /// Returns true if the traversal is for animation-only restyles.
+ #[inline]
+ pub fn for_animation_only(&self) -> bool {
+ self.contains(TraversalFlags::AnimationOnly)
+ }
+}
diff --git a/servo/components/style/use_counters/mod.rs b/servo/components/style/use_counters/mod.rs
new file mode 100644
index 0000000000..5a87372570
--- /dev/null
+++ b/servo/components/style/use_counters/mod.rs
@@ -0,0 +1,96 @@
+/* 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/. */
+
+//! Various stuff for CSS property use counters.
+
+use crate::properties::{property_counts, CountedUnknownProperty, NonCustomPropertyId};
+use std::cell::Cell;
+
+#[cfg(target_pointer_width = "64")]
+const BITS_PER_ENTRY: usize = 64;
+
+#[cfg(target_pointer_width = "32")]
+const BITS_PER_ENTRY: usize = 32;
+
+/// One bit per each non-custom CSS property.
+#[derive(Default)]
+pub struct CountedUnknownPropertyUseCounters {
+ storage:
+ [Cell<usize>; (property_counts::COUNTED_UNKNOWN - 1 + BITS_PER_ENTRY) / BITS_PER_ENTRY],
+}
+
+/// One bit per each non-custom CSS property.
+#[derive(Default)]
+pub struct NonCustomPropertyUseCounters {
+ storage: [Cell<usize>; (property_counts::NON_CUSTOM - 1 + BITS_PER_ENTRY) / BITS_PER_ENTRY],
+}
+
+macro_rules! property_use_counters_methods {
+ ($id: ident) => {
+ /// Returns the bucket a given property belongs in, and the bitmask for that
+ /// property.
+ #[inline(always)]
+ fn bucket_and_pattern(id: $id) -> (usize, usize) {
+ let bit = id.bit();
+ let bucket = bit / BITS_PER_ENTRY;
+ let bit_in_bucket = bit % BITS_PER_ENTRY;
+ (bucket, 1 << bit_in_bucket)
+ }
+
+ /// Record that a given property ID has been parsed.
+ #[inline]
+ pub fn record(&self, id: $id) {
+ let (bucket, pattern) = Self::bucket_and_pattern(id);
+ let bucket = &self.storage[bucket];
+ bucket.set(bucket.get() | pattern)
+ }
+
+ /// Returns whether a given property ID has been recorded
+ /// earlier.
+ #[inline]
+ pub fn recorded(&self, id: $id) -> bool {
+ let (bucket, pattern) = Self::bucket_and_pattern(id);
+ self.storage[bucket].get() & pattern != 0
+ }
+
+ /// Merge `other` into `self`.
+ #[inline]
+ fn merge(&self, other: &Self) {
+ for (bucket, other_bucket) in self.storage.iter().zip(other.storage.iter()) {
+ bucket.set(bucket.get() | other_bucket.get())
+ }
+ }
+ };
+}
+
+impl CountedUnknownPropertyUseCounters {
+ property_use_counters_methods!(CountedUnknownProperty);
+}
+
+impl NonCustomPropertyUseCounters {
+ property_use_counters_methods!(NonCustomPropertyId);
+}
+
+/// The use-counter data related to a given document we want to store.
+#[derive(Default)]
+pub struct UseCounters {
+ /// The counters for non-custom properties that have been parsed in the
+ /// document's stylesheets.
+ pub non_custom_properties: NonCustomPropertyUseCounters,
+ /// The counters for css properties which we haven't implemented yet.
+ pub counted_unknown_properties: CountedUnknownPropertyUseCounters,
+}
+
+impl UseCounters {
+ /// Merge the use counters.
+ ///
+ /// Used for parallel parsing, where we parse off-main-thread.
+ #[inline]
+ pub fn merge(&self, other: &Self) {
+ self.non_custom_properties
+ .merge(&other.non_custom_properties);
+ self.counted_unknown_properties
+ .merge(&other.counted_unknown_properties);
+ }
+}
diff --git a/servo/components/style/values/animated/color.rs b/servo/components/style/values/animated/color.rs
new file mode 100644
index 0000000000..f608b72e53
--- /dev/null
+++ b/servo/components/style/values/animated/color.rs
@@ -0,0 +1,88 @@
+/* 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/. */
+
+//! Animated types for CSS colors.
+
+use crate::color::mix::ColorInterpolationMethod;
+use crate::color::AbsoluteColor;
+use crate::values::animated::{Animate, Procedure, ToAnimatedZero};
+use crate::values::computed::Percentage;
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::color::{ColorMixFlags, GenericColor, GenericColorMix};
+
+impl Animate for AbsoluteColor {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (left_weight, right_weight) = procedure.weights();
+ Ok(crate::color::mix::mix(
+ ColorInterpolationMethod::best_interpolation_between(self, other),
+ self,
+ left_weight as f32,
+ other,
+ right_weight as f32,
+ ColorMixFlags::empty(),
+ ))
+ }
+}
+
+impl ComputeSquaredDistance for AbsoluteColor {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let start = [
+ self.alpha,
+ self.components.0 * self.alpha,
+ self.components.1 * self.alpha,
+ self.components.2 * self.alpha,
+ ];
+ let end = [
+ other.alpha,
+ other.components.0 * other.alpha,
+ other.components.1 * other.alpha,
+ other.components.2 * other.alpha,
+ ];
+ start
+ .iter()
+ .zip(&end)
+ .map(|(this, other)| this.compute_squared_distance(other))
+ .sum()
+ }
+}
+
+/// An animated value for `<color>`.
+pub type Color = GenericColor<Percentage>;
+
+/// An animated value for `<color-mix>`.
+pub type ColorMix = GenericColorMix<Color, Percentage>;
+
+impl Animate for Color {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (left_weight, right_weight) = procedure.weights();
+ Ok(Self::from_color_mix(ColorMix {
+ interpolation: ColorInterpolationMethod::srgb(),
+ left: self.clone(),
+ left_percentage: Percentage(left_weight as f32),
+ right: other.clone(),
+ right_percentage: Percentage(right_weight as f32),
+ // See https://github.com/w3c/csswg-drafts/issues/7324
+ flags: ColorMixFlags::empty(),
+ }))
+ }
+}
+
+impl ComputeSquaredDistance for Color {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let current_color = AbsoluteColor::TRANSPARENT_BLACK;
+ self.resolve_to_absolute(&current_color)
+ .compute_squared_distance(&other.resolve_to_absolute(&current_color))
+ }
+}
+
+impl ToAnimatedZero for Color {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(Color::Absolute(AbsoluteColor::TRANSPARENT_BLACK))
+ }
+}
diff --git a/servo/components/style/values/animated/effects.rs b/servo/components/style/values/animated/effects.rs
new file mode 100644
index 0000000000..67557e54b7
--- /dev/null
+++ b/servo/components/style/values/animated/effects.rs
@@ -0,0 +1,27 @@
+/* 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/. */
+
+//! Animated types for CSS values related to effects.
+
+use crate::values::animated::color::Color;
+use crate::values::computed::length::Length;
+#[cfg(feature = "gecko")]
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{Angle, Number};
+use crate::values::generics::effects::Filter as GenericFilter;
+use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow;
+#[cfg(not(feature = "gecko"))]
+use crate::values::Impossible;
+
+/// An animated value for the `drop-shadow()` filter.
+pub type AnimatedSimpleShadow = GenericSimpleShadow<Color, Length, Length>;
+
+/// An animated value for a single `filter`.
+#[cfg(feature = "gecko")]
+pub type AnimatedFilter =
+ GenericFilter<Angle, Number, Number, Length, AnimatedSimpleShadow, ComputedUrl>;
+
+/// An animated value for a single `filter`.
+#[cfg(not(feature = "gecko"))]
+pub type AnimatedFilter = GenericFilter<Angle, Number, Number, Length, Impossible, Impossible>;
diff --git a/servo/components/style/values/animated/font.rs b/servo/components/style/values/animated/font.rs
new file mode 100644
index 0000000000..63d4a14b2f
--- /dev/null
+++ b/servo/components/style/values/animated/font.rs
@@ -0,0 +1,37 @@
+/* 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/. */
+
+//! Animation implementation for various font-related types.
+
+use super::{Animate, Procedure, ToAnimatedZero};
+use crate::values::computed::font::FontVariationSettings;
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+
+/// <https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def>
+///
+/// Note that the ComputedValue implementation will already have sorted and de-dup'd
+/// the lists of settings, so we can just iterate over the two lists together and
+/// animate their individual values.
+impl Animate for FontVariationSettings {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let result: Vec<_> =
+ super::lists::by_computed_value::animate(&self.0, &other.0, procedure)?;
+ Ok(Self(result.into_boxed_slice()))
+ }
+}
+
+impl ComputeSquaredDistance for FontVariationSettings {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ super::lists::by_computed_value::squared_distance(&self.0, &other.0)
+ }
+}
+
+impl ToAnimatedZero for FontVariationSettings {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
diff --git a/servo/components/style/values/animated/grid.rs b/servo/components/style/values/animated/grid.rs
new file mode 100644
index 0000000000..04f1a2fcaa
--- /dev/null
+++ b/servo/components/style/values/animated/grid.rs
@@ -0,0 +1,165 @@
+/* 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/. */
+
+//! Animation implementation for various grid-related types.
+
+// Note: we can implement Animate on their generic types directly, but in this case we need to
+// make sure two trait bounds, L: Clone and I: PartialEq, are satisfied on almost all the
+// grid-related types and their other trait implementations because Animate needs them. So in
+// order to avoid adding these two trait bounds (or maybe more..) everywhere, we implement
+// Animate for the computed types, instead of the generic types.
+
+use super::{Animate, Procedure, ToAnimatedZero};
+use crate::values::computed::Integer;
+use crate::values::computed::LengthPercentage;
+use crate::values::computed::{GridTemplateComponent, TrackList, TrackSize};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::grid as generics;
+
+fn discrete<T: Clone>(from: &T, to: &T, procedure: Procedure) -> Result<T, ()> {
+ if let Procedure::Interpolate { progress } = procedure {
+ Ok(if progress < 0.5 {
+ from.clone()
+ } else {
+ to.clone()
+ })
+ } else {
+ // The discrete animation is not additive, so per spec [1] we should use the |from|, which
+ // is the underlying value. However this mismatches our animation mechanism (see
+ // composite_endpoint() in servo/ports/geckolib/glues.rs), which uses the effect value
+ // (i.e. |to| value here) [2]. So in order to match the behavior of other properties and
+ // other browsers, we use |to| value for addition and accumulation, i.e. Vresult = Vb.
+ //
+ // [1] https://drafts.csswg.org/css-values-4/#not-additive
+ // [2] https://github.com/w3c/csswg-drafts/issues/9070
+ Ok(to.clone())
+ }
+}
+
+fn animate_with_discrete_fallback<T: Animate + Clone>(
+ from: &T,
+ to: &T,
+ procedure: Procedure,
+) -> Result<T, ()> {
+ from.animate(to, procedure)
+ .or_else(|_| discrete(from, to, procedure))
+}
+
+impl Animate for TrackSize {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self, other) {
+ (&generics::TrackSize::Breadth(ref from), &generics::TrackSize::Breadth(ref to)) => {
+ animate_with_discrete_fallback(from, to, procedure)
+ .map(generics::TrackSize::Breadth)
+ },
+ (
+ &generics::TrackSize::Minmax(ref from_min, ref from_max),
+ &generics::TrackSize::Minmax(ref to_min, ref to_max),
+ ) => Ok(generics::TrackSize::Minmax(
+ animate_with_discrete_fallback(from_min, to_min, procedure)?,
+ animate_with_discrete_fallback(from_max, to_max, procedure)?,
+ )),
+ (
+ &generics::TrackSize::FitContent(ref from),
+ &generics::TrackSize::FitContent(ref to),
+ ) => animate_with_discrete_fallback(from, to, procedure)
+ .map(generics::TrackSize::FitContent),
+ (_, _) => discrete(self, other, procedure),
+ }
+ }
+}
+
+impl Animate for generics::TrackRepeat<LengthPercentage, Integer> {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ // If the keyword, auto-fit/fill, is the same it can result in different
+ // number of tracks. For both auto-fit/fill, the number of columns isn't
+ // known until you do layout since it depends on the container size, item
+ // placement and other factors, so we cannot do the correct interpolation
+ // by computed values. Therefore, return Err(()) if it's keywords. If it
+ // is Number, we support animation only if the count is the same and the
+ // length of track_sizes is the same.
+ // https://github.com/w3c/csswg-drafts/issues/3503
+ match (&self.count, &other.count) {
+ (&generics::RepeatCount::Number(from), &generics::RepeatCount::Number(to))
+ if from == to =>
+ {
+ ()
+ },
+ (_, _) => return Err(()),
+ }
+
+ let count = self.count;
+ let track_sizes = super::lists::by_computed_value::animate(
+ &self.track_sizes,
+ &other.track_sizes,
+ procedure,
+ )?;
+
+ // The length of |line_names| is always 0 or N+1, where N is the length
+ // of |track_sizes|. Besides, <line-names> is always discrete.
+ let line_names = discrete(&self.line_names, &other.line_names, procedure)?;
+
+ Ok(generics::TrackRepeat {
+ count,
+ line_names,
+ track_sizes,
+ })
+ }
+}
+
+impl Animate for TrackList {
+ // Based on https://github.com/w3c/csswg-drafts/issues/3201:
+ // 1. Check interpolation type per track, so we need to handle discrete animations
+ // in TrackSize, so any Err(()) returned from TrackSize doesn't make all TrackSize
+ // fallback to discrete animation.
+ // 2. line-names is always discrete.
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.values.len() != other.values.len() {
+ return Err(());
+ }
+
+ if self.is_explicit() != other.is_explicit() {
+ return Err(());
+ }
+
+ // For now, repeat(auto-fill/auto-fit, ...) is not animatable.
+ // TrackRepeat will return Err(()) if we use keywords. Therefore, we can
+ // early return here to avoid traversing |values| in <auto-track-list>.
+ // This may be updated in the future.
+ // https://github.com/w3c/csswg-drafts/issues/3503
+ if self.has_auto_repeat() || other.has_auto_repeat() {
+ return Err(());
+ }
+
+ let values =
+ super::lists::by_computed_value::animate(&self.values, &other.values, procedure)?;
+
+ // The length of |line_names| is always 0 or N+1, where N is the length
+ // of |track_sizes|. Besides, <line-names> is always discrete.
+ let line_names = discrete(&self.line_names, &other.line_names, procedure)?;
+
+ Ok(TrackList {
+ values,
+ line_names,
+ auto_repeat_index: self.auto_repeat_index,
+ })
+ }
+}
+
+impl ComputeSquaredDistance for GridTemplateComponent {
+ #[inline]
+ fn compute_squared_distance(&self, _other: &Self) -> Result<SquaredDistance, ()> {
+ // TODO: Bug 1518585, we should implement ComputeSquaredDistance.
+ Err(())
+ }
+}
+
+impl ToAnimatedZero for GridTemplateComponent {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ // It's not clear to get a zero grid track list based on the current definition
+ // of spec, so we return Err(()) directly.
+ Err(())
+ }
+}
diff --git a/servo/components/style/values/animated/lists.rs b/servo/components/style/values/animated/lists.rs
new file mode 100644
index 0000000000..8b3898c497
--- /dev/null
+++ b/servo/components/style/values/animated/lists.rs
@@ -0,0 +1,141 @@
+/* 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/. */
+
+//! Lists have various ways of being animated, this module implements them.
+//!
+//! See https://drafts.csswg.org/web-animations-1/#animating-properties
+
+/// https://drafts.csswg.org/web-animations-1/#by-computed-value
+pub mod by_computed_value {
+ use crate::values::{
+ animated::{Animate, Procedure},
+ distance::{ComputeSquaredDistance, SquaredDistance},
+ };
+ use std::iter::FromIterator;
+
+ #[allow(missing_docs)]
+ pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()>
+ where
+ T: Animate,
+ C: FromIterator<T>,
+ {
+ if left.len() != right.len() {
+ return Err(());
+ }
+ left.iter()
+ .zip(right.iter())
+ .map(|(left, right)| left.animate(right, procedure))
+ .collect()
+ }
+
+ #[allow(missing_docs)]
+ pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()>
+ where
+ T: ComputeSquaredDistance,
+ {
+ if left.len() != right.len() {
+ return Err(());
+ }
+ left.iter()
+ .zip(right.iter())
+ .map(|(left, right)| left.compute_squared_distance(right))
+ .sum()
+ }
+}
+
+/// This is the animation used for some of the types like shadows and filters, where the
+/// interpolation happens with the zero value if one of the sides is not present.
+///
+/// https://drafts.csswg.org/web-animations-1/#animating-shadow-lists
+pub mod with_zero {
+ use crate::values::animated::ToAnimatedZero;
+ use crate::values::{
+ animated::{Animate, Procedure},
+ distance::{ComputeSquaredDistance, SquaredDistance},
+ };
+ use itertools::{EitherOrBoth, Itertools};
+ use std::iter::FromIterator;
+
+ #[allow(missing_docs)]
+ pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()>
+ where
+ T: Animate + Clone + ToAnimatedZero,
+ C: FromIterator<T>,
+ {
+ if procedure == Procedure::Add {
+ return Ok(left.iter().chain(right.iter()).cloned().collect());
+ }
+ left.iter()
+ .zip_longest(right.iter())
+ .map(|it| match it {
+ EitherOrBoth::Both(left, right) => left.animate(right, procedure),
+ EitherOrBoth::Left(left) => left.animate(&left.to_animated_zero()?, procedure),
+ EitherOrBoth::Right(right) => right.to_animated_zero()?.animate(right, procedure),
+ })
+ .collect()
+ }
+
+ #[allow(missing_docs)]
+ pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()>
+ where
+ T: ToAnimatedZero + ComputeSquaredDistance,
+ {
+ left.iter()
+ .zip_longest(right.iter())
+ .map(|it| match it {
+ EitherOrBoth::Both(left, right) => left.compute_squared_distance(right),
+ EitherOrBoth::Left(item) | EitherOrBoth::Right(item) => {
+ item.to_animated_zero()?.compute_squared_distance(item)
+ },
+ })
+ .sum()
+ }
+}
+
+/// https://drafts.csswg.org/web-animations-1/#repeatable-list
+pub mod repeatable_list {
+ use crate::values::{
+ animated::{Animate, Procedure},
+ distance::{ComputeSquaredDistance, SquaredDistance},
+ };
+ use std::iter::FromIterator;
+
+ #[allow(missing_docs)]
+ pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()>
+ where
+ T: Animate,
+ C: FromIterator<T>,
+ {
+ use num_integer::lcm;
+ // If the length of either list is zero, the least common multiple is undefined.
+ if left.is_empty() || right.is_empty() {
+ return Err(());
+ }
+ let len = lcm(left.len(), right.len());
+ left.iter()
+ .cycle()
+ .zip(right.iter().cycle())
+ .take(len)
+ .map(|(left, right)| left.animate(right, procedure))
+ .collect()
+ }
+
+ #[allow(missing_docs)]
+ pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()>
+ where
+ T: ComputeSquaredDistance,
+ {
+ use num_integer::lcm;
+ if left.is_empty() || right.is_empty() {
+ return Err(());
+ }
+ let len = lcm(left.len(), right.len());
+ left.iter()
+ .cycle()
+ .zip(right.iter().cycle())
+ .take(len)
+ .map(|(left, right)| left.compute_squared_distance(right))
+ .sum()
+ }
+}
diff --git a/servo/components/style/values/animated/mod.rs b/servo/components/style/values/animated/mod.rs
new file mode 100644
index 0000000000..31ea206fc0
--- /dev/null
+++ b/servo/components/style/values/animated/mod.rs
@@ -0,0 +1,487 @@
+/* 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/. */
+
+//! Animated values.
+//!
+//! Some values, notably colors, cannot be interpolated directly with their
+//! computed values and need yet another intermediate representation. This
+//! module's raison d'être is to ultimately contain all these types.
+
+use crate::color::AbsoluteColor;
+use crate::properties::PropertyId;
+use crate::values::computed::length::LengthPercentage;
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::Angle as ComputedAngle;
+use crate::values::computed::Image;
+use crate::values::specified::SVGPathData;
+use crate::values::CSSFloat;
+use app_units::Au;
+use smallvec::SmallVec;
+use std::cmp;
+
+pub mod color;
+pub mod effects;
+mod font;
+mod grid;
+pub mod lists;
+mod svg;
+pub mod transform;
+
+/// The category a property falls into for ordering purposes.
+///
+/// https://drafts.csswg.org/web-animations/#calculating-computed-keyframes
+#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
+enum PropertyCategory {
+ Custom,
+ PhysicalLonghand,
+ LogicalLonghand,
+ Shorthand,
+}
+
+impl PropertyCategory {
+ fn of(id: &PropertyId) -> Self {
+ match *id {
+ PropertyId::NonCustom(id) => match id.longhand_or_shorthand() {
+ Ok(id) => if id.is_logical() {
+ PropertyCategory::LogicalLonghand
+ } else {
+ PropertyCategory::PhysicalLonghand
+ },
+ Err(..) => PropertyCategory::Shorthand,
+ },
+ PropertyId::Custom(..) => PropertyCategory::Custom,
+ }
+ }
+}
+
+/// A comparator to sort PropertyIds such that physical longhands are sorted
+/// before logical longhands and shorthands, shorthands with fewer components
+/// are sorted before shorthands with more components, and otherwise shorthands
+/// are sorted by IDL name as defined by [Web Animations][property-order].
+///
+/// Using this allows us to prioritize values specified by longhands (or smaller
+/// shorthand subsets) when longhands and shorthands are both specified on the
+/// one keyframe.
+///
+/// [property-order] https://drafts.csswg.org/web-animations/#calculating-computed-keyframes
+pub fn compare_property_priority(a: &PropertyId, b: &PropertyId) -> cmp::Ordering {
+ let a_category = PropertyCategory::of(a);
+ let b_category = PropertyCategory::of(b);
+
+ if a_category != b_category {
+ return a_category.cmp(&b_category);
+ }
+
+ if a_category != PropertyCategory::Shorthand {
+ return cmp::Ordering::Equal;
+ }
+
+ let a = a.as_shorthand().unwrap();
+ let b = b.as_shorthand().unwrap();
+ // Within shorthands, sort by the number of subproperties, then by IDL
+ // name.
+ let subprop_count_a = a.longhands().count();
+ let subprop_count_b = b.longhands().count();
+ subprop_count_a
+ .cmp(&subprop_count_b)
+ .then_with(|| a.idl_name_sort_order().cmp(&b.idl_name_sort_order()))
+}
+
+/// A helper function to animate two multiplicative factor.
+pub fn animate_multiplicative_factor(
+ this: CSSFloat,
+ other: CSSFloat,
+ procedure: Procedure,
+) -> Result<CSSFloat, ()> {
+ Ok((this - 1.).animate(&(other - 1.), procedure)? + 1.)
+}
+
+/// Animate from one value to another.
+///
+/// This trait is derivable with `#[derive(Animate)]`. The derived
+/// implementation uses a `match` expression with identical patterns for both
+/// `self` and `other`, calling `Animate::animate` on each fields of the values.
+/// If a field is annotated with `#[animation(constant)]`, the two values should
+/// be equal or an error is returned.
+///
+/// If a variant is annotated with `#[animation(error)]`, the corresponding
+/// `match` arm returns an error.
+///
+/// Trait bounds for type parameter `Foo` can be opted out of with
+/// `#[animation(no_bound(Foo))]` on the type definition, trait bounds for
+/// fields can be opted into with `#[animation(field_bound)]` on the field.
+pub trait Animate: Sized {
+ /// Animate a value towards another one, given an animation procedure.
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()>;
+}
+
+/// An animation procedure.
+///
+/// <https://drafts.csswg.org/web-animations/#procedures-for-animating-properties>
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum Procedure {
+ /// <https://drafts.csswg.org/web-animations/#animation-interpolation>
+ Interpolate { progress: f64 },
+ /// <https://drafts.csswg.org/web-animations/#animation-addition>
+ Add,
+ /// <https://drafts.csswg.org/web-animations/#animation-accumulation>
+ Accumulate { count: u64 },
+}
+
+/// Conversion between computed values and intermediate values for animations.
+///
+/// Notably, colors are represented as four floats during animations.
+///
+/// This trait is derivable with `#[derive(ToAnimatedValue)]`.
+pub trait ToAnimatedValue {
+ /// The type of the animated value.
+ type AnimatedValue;
+
+ /// Converts this value to an animated value.
+ fn to_animated_value(self) -> Self::AnimatedValue;
+
+ /// Converts back an animated value into a computed value.
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self;
+}
+
+/// Returns a value similar to `self` that represents zero.
+///
+/// This trait is derivable with `#[derive(ToAnimatedValue)]`. If a field is
+/// annotated with `#[animation(constant)]`, a clone of its value will be used
+/// instead of calling `ToAnimatedZero::to_animated_zero` on it.
+///
+/// If a variant is annotated with `#[animation(error)]`, the corresponding
+/// `match` arm is not generated.
+///
+/// Trait bounds for type parameter `Foo` can be opted out of with
+/// `#[animation(no_bound(Foo))]` on the type definition.
+pub trait ToAnimatedZero: Sized {
+ /// Returns a value that, when added with an underlying value, will produce the underlying
+ /// value. This is used for SMIL animation's "by-animation" where SMIL first interpolates from
+ /// the zero value to the 'by' value, and then adds the result to the underlying value.
+ ///
+ /// This is not the necessarily the same as the initial value of a property. For example, the
+ /// initial value of 'stroke-width' is 1, but the zero value is 0, since adding 1 to the
+ /// underlying value will not produce the underlying value.
+ fn to_animated_zero(&self) -> Result<Self, ()>;
+}
+
+impl Procedure {
+ /// Returns this procedure as a pair of weights.
+ ///
+ /// This is useful for animations that don't animate differently
+ /// depending on the used procedure.
+ #[inline]
+ pub fn weights(self) -> (f64, f64) {
+ match self {
+ Procedure::Interpolate { progress } => (1. - progress, progress),
+ Procedure::Add => (1., 1.),
+ Procedure::Accumulate { count } => (count as f64, 1.),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-transitions/#animtype-number>
+impl Animate for i32 {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(((*self as f64).animate(&(*other as f64), procedure)? + 0.5).floor() as i32)
+ }
+}
+
+/// <https://drafts.csswg.org/css-transitions/#animtype-number>
+impl Animate for f32 {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let ret = (*self as f64).animate(&(*other as f64), procedure)?;
+ Ok(ret.min(f32::MAX as f64).max(f32::MIN as f64) as f32)
+ }
+}
+
+/// <https://drafts.csswg.org/css-transitions/#animtype-number>
+impl Animate for f64 {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (self_weight, other_weight) = procedure.weights();
+
+ let ret = *self * self_weight + *other * other_weight;
+ Ok(ret.min(f64::MAX).max(f64::MIN))
+ }
+}
+
+impl<T> Animate for Option<T>
+where
+ T: Animate,
+{
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self.as_ref(), other.as_ref()) {
+ (Some(ref this), Some(ref other)) => Ok(Some(this.animate(other, procedure)?)),
+ (None, None) => Ok(None),
+ _ => Err(()),
+ }
+ }
+}
+
+impl Animate for Au {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Au::new(self.0.animate(&other.0, procedure)?))
+ }
+}
+
+impl<T: Animate> Animate for Box<T> {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Box::new((**self).animate(&other, procedure)?))
+ }
+}
+
+impl<T> ToAnimatedValue for Option<T>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = Option<<T as ToAnimatedValue>::AnimatedValue>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.map(T::to_animated_value)
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.map(T::from_animated_value)
+ }
+}
+
+impl<T> ToAnimatedValue for Vec<T>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = Vec<<T as ToAnimatedValue>::AnimatedValue>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.into_iter().map(T::to_animated_value).collect()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.into_iter().map(T::from_animated_value).collect()
+ }
+}
+
+impl<T> ToAnimatedValue for Box<T>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = Box<<T as ToAnimatedValue>::AnimatedValue>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ Box::new((*self).to_animated_value())
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Box::new(T::from_animated_value(*animated))
+ }
+}
+
+impl<T> ToAnimatedValue for Box<[T]>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = Box<[<T as ToAnimatedValue>::AnimatedValue]>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.into_vec()
+ .into_iter()
+ .map(T::to_animated_value)
+ .collect::<Vec<_>>()
+ .into_boxed_slice()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated
+ .into_vec()
+ .into_iter()
+ .map(T::from_animated_value)
+ .collect::<Vec<_>>()
+ .into_boxed_slice()
+ }
+}
+
+impl<T> ToAnimatedValue for crate::OwnedSlice<T>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = crate::OwnedSlice<<T as ToAnimatedValue>::AnimatedValue>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.into_box().to_animated_value().into()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Self::from(Box::from_animated_value(animated.into_box()))
+ }
+}
+
+impl<T> ToAnimatedValue for SmallVec<[T; 1]>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = SmallVec<[T::AnimatedValue; 1]>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.into_iter().map(T::to_animated_value).collect()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.into_iter().map(T::from_animated_value).collect()
+ }
+}
+
+macro_rules! trivial_to_animated_value {
+ ($ty:ty) => {
+ impl $crate::values::animated::ToAnimatedValue for $ty {
+ type AnimatedValue = Self;
+
+ #[inline]
+ fn to_animated_value(self) -> Self {
+ self
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated
+ }
+ }
+ };
+}
+
+trivial_to_animated_value!(Au);
+trivial_to_animated_value!(LengthPercentage);
+trivial_to_animated_value!(ComputedAngle);
+trivial_to_animated_value!(ComputedUrl);
+trivial_to_animated_value!(bool);
+trivial_to_animated_value!(f32);
+trivial_to_animated_value!(i32);
+trivial_to_animated_value!(AbsoluteColor);
+trivial_to_animated_value!(crate::values::generics::color::ColorMixFlags);
+// Note: This implementation is for ToAnimatedValue of ShapeSource.
+//
+// SVGPathData uses Box<[T]>. If we want to derive ToAnimatedValue for all the
+// types, we have to do "impl ToAnimatedValue for Box<[T]>" first.
+// However, the general version of "impl ToAnimatedValue for Box<[T]>" needs to
+// clone |T| and convert it into |T::AnimatedValue|. However, for SVGPathData
+// that is unnecessary--moving |T| is sufficient. So here, we implement this
+// trait manually.
+trivial_to_animated_value!(SVGPathData);
+// FIXME: Bug 1514342, Image is not animatable, but we still need to implement
+// this to avoid adding this derive to generic::Image and all its arms. We can
+// drop this after landing Bug 1514342.
+trivial_to_animated_value!(Image);
+
+impl ToAnimatedZero for Au {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(Au(0))
+ }
+}
+
+impl ToAnimatedZero for f32 {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(0.)
+ }
+}
+
+impl ToAnimatedZero for f64 {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(0.)
+ }
+}
+
+impl ToAnimatedZero for i32 {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(0)
+ }
+}
+
+impl<T> ToAnimatedZero for Box<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(Box::new((**self).to_animated_zero()?))
+ }
+}
+
+impl<T> ToAnimatedZero for Option<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ match *self {
+ Some(ref value) => Ok(Some(value.to_animated_zero()?)),
+ None => Ok(None),
+ }
+ }
+}
+
+impl<T> ToAnimatedZero for Vec<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ self.iter().map(|v| v.to_animated_zero()).collect()
+ }
+}
+
+impl<T> ToAnimatedZero for Box<[T]>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ self.iter().map(|v| v.to_animated_zero()).collect()
+ }
+}
+
+impl<T> ToAnimatedZero for crate::OwnedSlice<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ self.iter().map(|v| v.to_animated_zero()).collect()
+ }
+}
+
+impl<T> ToAnimatedZero for crate::ArcSlice<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ let v = self
+ .iter()
+ .map(|v| v.to_animated_zero())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(crate::ArcSlice::from_iter(v.into_iter()))
+ }
+}
diff --git a/servo/components/style/values/animated/svg.rs b/servo/components/style/values/animated/svg.rs
new file mode 100644
index 0000000000..04e35098ad
--- /dev/null
+++ b/servo/components/style/values/animated/svg.rs
@@ -0,0 +1,46 @@
+/* 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/. */
+
+//! Animation implementations for various SVG-related types.
+
+use super::{Animate, Procedure};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::svg::SVGStrokeDashArray;
+
+/// <https://www.w3.org/TR/SVG11/painting.html#StrokeDasharrayProperty>
+impl<L> Animate for SVGStrokeDashArray<L>
+where
+ L: Clone + Animate,
+{
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) {
+ // Non-additive.
+ return Err(());
+ }
+ match (self, other) {
+ (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => {
+ Ok(SVGStrokeDashArray::Values(
+ super::lists::repeatable_list::animate(this, other, procedure)?,
+ ))
+ },
+ _ => Err(()),
+ }
+ }
+}
+
+impl<L> ComputeSquaredDistance for SVGStrokeDashArray<L>
+where
+ L: ComputeSquaredDistance,
+{
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ match (self, other) {
+ (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => {
+ super::lists::repeatable_list::squared_distance(this, other)
+ },
+ _ => Err(()),
+ }
+ }
+}
diff --git a/servo/components/style/values/animated/transform.rs b/servo/components/style/values/animated/transform.rs
new file mode 100644
index 0000000000..b91e3ed8bc
--- /dev/null
+++ b/servo/components/style/values/animated/transform.rs
@@ -0,0 +1,1667 @@
+/* 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/. */
+
+//! Animated types for transform.
+// There are still some implementation on Matrix3D in animated_properties.mako.rs
+// because they still need mako to generate the code.
+
+use super::animate_multiplicative_factor;
+use super::{Animate, Procedure, ToAnimatedZero};
+use crate::values::computed::transform::Rotate as ComputedRotate;
+use crate::values::computed::transform::Scale as ComputedScale;
+use crate::values::computed::transform::Transform as ComputedTransform;
+use crate::values::computed::transform::TransformOperation as ComputedTransformOperation;
+use crate::values::computed::transform::Translate as ComputedTranslate;
+use crate::values::computed::transform::{DirectionVector, Matrix, Matrix3D};
+use crate::values::computed::Angle;
+use crate::values::computed::{Length, LengthPercentage};
+use crate::values::computed::{Number, Percentage};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::transform::{self, Transform, TransformOperation};
+use crate::values::generics::transform::{Rotate, Scale, Translate};
+use crate::values::CSSFloat;
+use crate::Zero;
+use std::cmp;
+use std::ops::Add;
+
+// ------------------------------------
+// Animations for Matrix/Matrix3D.
+// ------------------------------------
+/// A 2d matrix for interpolation.
+#[derive(Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[allow(missing_docs)]
+// FIXME: We use custom derive for ComputeSquaredDistance. However, If possible, we should convert
+// the InnerMatrix2D into types with physical meaning. This custom derive computes the squared
+// distance from each matrix item, and this makes the result different from that in Gecko if we
+// have skew factor in the Matrix3D.
+pub struct InnerMatrix2D {
+ pub m11: CSSFloat,
+ pub m12: CSSFloat,
+ pub m21: CSSFloat,
+ pub m22: CSSFloat,
+}
+
+impl Animate for InnerMatrix2D {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(InnerMatrix2D {
+ m11: animate_multiplicative_factor(self.m11, other.m11, procedure)?,
+ m12: self.m12.animate(&other.m12, procedure)?,
+ m21: self.m21.animate(&other.m21, procedure)?,
+ m22: animate_multiplicative_factor(self.m22, other.m22, procedure)?,
+ })
+ }
+}
+
+/// A 2d translation function.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)]
+pub struct Translate2D(f32, f32);
+
+/// A 2d scale function.
+#[derive(Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Scale2D(f32, f32);
+
+impl Animate for Scale2D {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Scale2D(
+ animate_multiplicative_factor(self.0, other.0, procedure)?,
+ animate_multiplicative_factor(self.1, other.1, procedure)?,
+ ))
+ }
+}
+
+/// A decomposed 2d matrix.
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct MatrixDecomposed2D {
+ /// The translation function.
+ pub translate: Translate2D,
+ /// The scale function.
+ pub scale: Scale2D,
+ /// The rotation angle.
+ pub angle: f32,
+ /// The inner matrix.
+ pub matrix: InnerMatrix2D,
+}
+
+impl Animate for MatrixDecomposed2D {
+ /// <https://drafts.csswg.org/css-transforms/#interpolation-of-decomposed-2d-matrix-values>
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ // If x-axis of one is flipped, and y-axis of the other,
+ // convert to an unflipped rotation.
+ let mut scale = self.scale;
+ let mut angle = self.angle;
+ let mut other_angle = other.angle;
+ if (scale.0 < 0.0 && other.scale.1 < 0.0) || (scale.1 < 0.0 && other.scale.0 < 0.0) {
+ scale.0 = -scale.0;
+ scale.1 = -scale.1;
+ angle += if angle < 0.0 { 180. } else { -180. };
+ }
+
+ // Don't rotate the long way around.
+ if angle == 0.0 {
+ angle = 360.
+ }
+ if other_angle == 0.0 {
+ other_angle = 360.
+ }
+
+ if (angle - other_angle).abs() > 180. {
+ if angle > other_angle {
+ angle -= 360.
+ } else {
+ other_angle -= 360.
+ }
+ }
+
+ // Interpolate all values.
+ let translate = self.translate.animate(&other.translate, procedure)?;
+ let scale = scale.animate(&other.scale, procedure)?;
+ let angle = angle.animate(&other_angle, procedure)?;
+ let matrix = self.matrix.animate(&other.matrix, procedure)?;
+
+ Ok(MatrixDecomposed2D {
+ translate,
+ scale,
+ angle,
+ matrix,
+ })
+ }
+}
+
+impl ComputeSquaredDistance for MatrixDecomposed2D {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ // Use Radian to compute the distance.
+ const RAD_PER_DEG: f64 = std::f64::consts::PI / 180.0;
+ let angle1 = self.angle as f64 * RAD_PER_DEG;
+ let angle2 = other.angle as f64 * RAD_PER_DEG;
+ Ok(self.translate.compute_squared_distance(&other.translate)? +
+ self.scale.compute_squared_distance(&other.scale)? +
+ angle1.compute_squared_distance(&angle2)? +
+ self.matrix.compute_squared_distance(&other.matrix)?)
+ }
+}
+
+impl From<Matrix3D> for MatrixDecomposed2D {
+ /// Decompose a 2D matrix.
+ /// <https://drafts.csswg.org/css-transforms/#decomposing-a-2d-matrix>
+ fn from(matrix: Matrix3D) -> MatrixDecomposed2D {
+ let mut row0x = matrix.m11;
+ let mut row0y = matrix.m12;
+ let mut row1x = matrix.m21;
+ let mut row1y = matrix.m22;
+
+ let translate = Translate2D(matrix.m41, matrix.m42);
+ let mut scale = Scale2D(
+ (row0x * row0x + row0y * row0y).sqrt(),
+ (row1x * row1x + row1y * row1y).sqrt(),
+ );
+
+ // If determinant is negative, one axis was flipped.
+ let determinant = row0x * row1y - row0y * row1x;
+ if determinant < 0. {
+ if row0x < row1y {
+ scale.0 = -scale.0;
+ } else {
+ scale.1 = -scale.1;
+ }
+ }
+
+ // Renormalize matrix to remove scale.
+ if scale.0 != 0.0 {
+ row0x *= 1. / scale.0;
+ row0y *= 1. / scale.0;
+ }
+ if scale.1 != 0.0 {
+ row1x *= 1. / scale.1;
+ row1y *= 1. / scale.1;
+ }
+
+ // Compute rotation and renormalize matrix.
+ let mut angle = row0y.atan2(row0x);
+ if angle != 0.0 {
+ let sn = -row0y;
+ let cs = row0x;
+ let m11 = row0x;
+ let m12 = row0y;
+ let m21 = row1x;
+ let m22 = row1y;
+ row0x = cs * m11 + sn * m21;
+ row0y = cs * m12 + sn * m22;
+ row1x = -sn * m11 + cs * m21;
+ row1y = -sn * m12 + cs * m22;
+ }
+
+ let m = InnerMatrix2D {
+ m11: row0x,
+ m12: row0y,
+ m21: row1x,
+ m22: row1y,
+ };
+
+ // Convert into degrees because our rotation functions expect it.
+ angle = angle.to_degrees();
+ MatrixDecomposed2D {
+ translate: translate,
+ scale: scale,
+ angle: angle,
+ matrix: m,
+ }
+ }
+}
+
+impl From<MatrixDecomposed2D> for Matrix3D {
+ /// Recompose a 2D matrix.
+ /// <https://drafts.csswg.org/css-transforms/#recomposing-to-a-2d-matrix>
+ fn from(decomposed: MatrixDecomposed2D) -> Matrix3D {
+ let mut computed_matrix = Matrix3D::identity();
+ computed_matrix.m11 = decomposed.matrix.m11;
+ computed_matrix.m12 = decomposed.matrix.m12;
+ computed_matrix.m21 = decomposed.matrix.m21;
+ computed_matrix.m22 = decomposed.matrix.m22;
+
+ // Translate matrix.
+ computed_matrix.m41 = decomposed.translate.0;
+ computed_matrix.m42 = decomposed.translate.1;
+
+ // Rotate matrix.
+ let angle = decomposed.angle.to_radians();
+ let cos_angle = angle.cos();
+ let sin_angle = angle.sin();
+
+ let mut rotate_matrix = Matrix3D::identity();
+ rotate_matrix.m11 = cos_angle;
+ rotate_matrix.m12 = sin_angle;
+ rotate_matrix.m21 = -sin_angle;
+ rotate_matrix.m22 = cos_angle;
+
+ // Multiplication of computed_matrix and rotate_matrix
+ computed_matrix = rotate_matrix.multiply(&computed_matrix);
+
+ // Scale matrix.
+ computed_matrix.m11 *= decomposed.scale.0;
+ computed_matrix.m12 *= decomposed.scale.0;
+ computed_matrix.m21 *= decomposed.scale.1;
+ computed_matrix.m22 *= decomposed.scale.1;
+ computed_matrix
+ }
+}
+
+impl Animate for Matrix {
+ #[cfg(feature = "servo")]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let this = Matrix3D::from(*self);
+ let other = Matrix3D::from(*other);
+ let this = MatrixDecomposed2D::from(this);
+ let other = MatrixDecomposed2D::from(other);
+ Matrix3D::from(this.animate(&other, procedure)?).into_2d()
+ }
+
+ #[cfg(feature = "gecko")]
+ // Gecko doesn't exactly follow the spec here; we use a different procedure
+ // to match it
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let this = Matrix3D::from(*self);
+ let other = Matrix3D::from(*other);
+ let from = decompose_2d_matrix(&this)?;
+ let to = decompose_2d_matrix(&other)?;
+ Matrix3D::from(from.animate(&to, procedure)?).into_2d()
+ }
+}
+
+/// A 3d translation.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)]
+pub struct Translate3D(pub f32, pub f32, pub f32);
+
+/// A 3d scale function.
+#[derive(Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Scale3D(pub f32, pub f32, pub f32);
+
+impl Scale3D {
+ /// Negate self.
+ fn negate(&mut self) {
+ self.0 *= -1.0;
+ self.1 *= -1.0;
+ self.2 *= -1.0;
+ }
+}
+
+impl Animate for Scale3D {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Scale3D(
+ animate_multiplicative_factor(self.0, other.0, procedure)?,
+ animate_multiplicative_factor(self.1, other.1, procedure)?,
+ animate_multiplicative_factor(self.2, other.2, procedure)?,
+ ))
+ }
+}
+
+/// A 3d skew function.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[derive(Animate, Clone, Copy, Debug)]
+pub struct Skew(f32, f32, f32);
+
+impl ComputeSquaredDistance for Skew {
+ // We have to use atan() to convert the skew factors into skew angles, so implement
+ // ComputeSquaredDistance manually.
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(self.0.atan().compute_squared_distance(&other.0.atan())? +
+ self.1.atan().compute_squared_distance(&other.1.atan())? +
+ self.2.atan().compute_squared_distance(&other.2.atan())?)
+ }
+}
+
+/// A 3d perspective transformation.
+#[derive(Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Perspective(pub f32, pub f32, pub f32, pub f32);
+
+impl Animate for Perspective {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Perspective(
+ self.0.animate(&other.0, procedure)?,
+ self.1.animate(&other.1, procedure)?,
+ self.2.animate(&other.2, procedure)?,
+ animate_multiplicative_factor(self.3, other.3, procedure)?,
+ ))
+ }
+}
+
+/// A quaternion used to represent a rotation.
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Quaternion(f64, f64, f64, f64);
+
+impl Quaternion {
+ /// Return a quaternion from a unit direction vector and angle (unit: radian).
+ #[inline]
+ fn from_direction_and_angle(vector: &DirectionVector, angle: f64) -> Self {
+ debug_assert!(
+ (vector.length() - 1.).abs() < 0.0001,
+ "Only accept an unit direction vector to create a quaternion"
+ );
+
+ // Quaternions between the range [360, 720] will treated as rotations at the other
+ // direction: [-360, 0]. And quaternions between the range [720*k, 720*(k+1)] will be
+ // treated as rotations [0, 720]. So it does not make sense to use quaternions to rotate
+ // the element more than ±360deg. Therefore, we have to make sure its range is (-360, 360).
+ let half_angle = angle
+ .abs()
+ .rem_euclid(std::f64::consts::TAU)
+ .copysign(angle) /
+ 2.;
+
+ // Reference:
+ // https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
+ //
+ // if the direction axis is (x, y, z) = xi + yj + zk,
+ // and the angle is |theta|, this formula can be done using
+ // an extension of Euler's formula:
+ // q = cos(theta/2) + (xi + yj + zk)(sin(theta/2))
+ // = cos(theta/2) +
+ // x*sin(theta/2)i + y*sin(theta/2)j + z*sin(theta/2)k
+ Quaternion(
+ vector.x as f64 * half_angle.sin(),
+ vector.y as f64 * half_angle.sin(),
+ vector.z as f64 * half_angle.sin(),
+ half_angle.cos(),
+ )
+ }
+
+ /// Calculate the dot product.
+ #[inline]
+ fn dot(&self, other: &Self) -> f64 {
+ self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3
+ }
+
+ /// Return the scaled quaternion by a factor.
+ #[inline]
+ fn scale(&self, factor: f64) -> Self {
+ Quaternion(
+ self.0 * factor,
+ self.1 * factor,
+ self.2 * factor,
+ self.3 * factor,
+ )
+ }
+}
+
+impl Add for Quaternion {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self(
+ self.0 + other.0,
+ self.1 + other.1,
+ self.2 + other.2,
+ self.3 + other.3,
+ )
+ }
+}
+
+impl Animate for Quaternion {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (this_weight, other_weight) = procedure.weights();
+ debug_assert!(
+ // Doule EPSILON since both this_weight and other_weght have calculation errors
+ // which are approximately equal to EPSILON.
+ (this_weight + other_weight - 1.0f64).abs() <= f64::EPSILON * 2.0 ||
+ other_weight == 1.0f64 ||
+ other_weight == 0.0f64,
+ "animate should only be used for interpolating or accumulating transforms"
+ );
+
+ // We take a specialized code path for accumulation (where other_weight
+ // is 1).
+ if let Procedure::Accumulate { .. } = procedure {
+ debug_assert_eq!(other_weight, 1.0);
+ if this_weight == 0.0 {
+ return Ok(*other);
+ }
+
+ let clamped_w = self.3.min(1.0).max(-1.0);
+
+ // Determine the scale factor.
+ let mut theta = clamped_w.acos();
+ let mut scale = if theta == 0.0 { 0.0 } else { 1.0 / theta.sin() };
+ theta *= this_weight;
+ scale *= theta.sin();
+
+ // Scale the self matrix by this_weight.
+ let mut scaled_self = *self;
+ scaled_self.0 *= scale;
+ scaled_self.1 *= scale;
+ scaled_self.2 *= scale;
+ scaled_self.3 = theta.cos();
+
+ // Multiply scaled-self by other.
+ let a = &scaled_self;
+ let b = other;
+ return Ok(Quaternion(
+ a.3 * b.0 + a.0 * b.3 + a.1 * b.2 - a.2 * b.1,
+ a.3 * b.1 - a.0 * b.2 + a.1 * b.3 + a.2 * b.0,
+ a.3 * b.2 + a.0 * b.1 - a.1 * b.0 + a.2 * b.3,
+ a.3 * b.3 - a.0 * b.0 - a.1 * b.1 - a.2 * b.2,
+ ));
+ }
+
+ // https://drafts.csswg.org/css-transforms-2/#interpolation-of-decomposed-3d-matrix-values
+ //
+ // Dot product, clamped between -1 and 1.
+ let cos_half_theta =
+ (self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3)
+ .min(1.0)
+ .max(-1.0);
+
+ if cos_half_theta.abs() == 1.0 {
+ return Ok(*self);
+ }
+
+ let half_theta = cos_half_theta.acos();
+ let sin_half_theta = (1.0 - cos_half_theta * cos_half_theta).sqrt();
+
+ let right_weight = (other_weight * half_theta).sin() / sin_half_theta;
+ // The spec would like to use
+ // "(other_weight * half_theta).cos() - cos_half_theta * right_weight". However, this
+ // formula may produce some precision issues of floating-point number calculation, e.g.
+ // when the progress is 100% (i.e. |other_weight| is 1), the |left_weight| may not be
+ // perfectly equal to 0. It could be something like -2.22e-16, which is approximately equal
+ // to zero, in the test. And after we recompose the Matrix3D, these approximated zeros
+ // make us failed to treat this Matrix3D as a Matrix2D, when serializating it.
+ //
+ // Therefore, we use another formula to calculate |left_weight| here. Blink and WebKit also
+ // use this formula, which is defined in:
+ // https://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/index.htm
+ // https://github.com/w3c/csswg-drafts/issues/9338
+ let left_weight = (this_weight * half_theta).sin() / sin_half_theta;
+
+ Ok(self.scale(left_weight) + other.scale(right_weight))
+ }
+}
+
+impl ComputeSquaredDistance for Quaternion {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ // Use quaternion vectors to get the angle difference. Both q1 and q2 are unit vectors,
+ // so we can get their angle difference by:
+ // cos(theta/2) = (q1 dot q2) / (|q1| * |q2|) = q1 dot q2.
+ let distance = self.dot(other).max(-1.0).min(1.0).acos() * 2.0;
+ Ok(SquaredDistance::from_sqrt(distance))
+ }
+}
+
+/// A decomposed 3d matrix.
+#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct MatrixDecomposed3D {
+ /// A translation function.
+ pub translate: Translate3D,
+ /// A scale function.
+ pub scale: Scale3D,
+ /// The skew component of the transformation.
+ pub skew: Skew,
+ /// The perspective component of the transformation.
+ pub perspective: Perspective,
+ /// The quaternion used to represent the rotation.
+ pub quaternion: Quaternion,
+}
+
+impl From<MatrixDecomposed3D> for Matrix3D {
+ /// Recompose a 3D matrix.
+ /// <https://drafts.csswg.org/css-transforms/#recomposing-to-a-3d-matrix>
+ fn from(decomposed: MatrixDecomposed3D) -> Matrix3D {
+ let mut matrix = Matrix3D::identity();
+
+ // Apply perspective
+ matrix.set_perspective(&decomposed.perspective);
+
+ // Apply translation
+ matrix.apply_translate(&decomposed.translate);
+
+ // Apply rotation
+ {
+ let x = decomposed.quaternion.0;
+ let y = decomposed.quaternion.1;
+ let z = decomposed.quaternion.2;
+ let w = decomposed.quaternion.3;
+
+ // Construct a composite rotation matrix from the quaternion values
+ // rotationMatrix is a identity 4x4 matrix initially
+ let mut rotation_matrix = Matrix3D::identity();
+ rotation_matrix.m11 = 1.0 - 2.0 * (y * y + z * z) as f32;
+ rotation_matrix.m12 = 2.0 * (x * y + z * w) as f32;
+ rotation_matrix.m13 = 2.0 * (x * z - y * w) as f32;
+ rotation_matrix.m21 = 2.0 * (x * y - z * w) as f32;
+ rotation_matrix.m22 = 1.0 - 2.0 * (x * x + z * z) as f32;
+ rotation_matrix.m23 = 2.0 * (y * z + x * w) as f32;
+ rotation_matrix.m31 = 2.0 * (x * z + y * w) as f32;
+ rotation_matrix.m32 = 2.0 * (y * z - x * w) as f32;
+ rotation_matrix.m33 = 1.0 - 2.0 * (x * x + y * y) as f32;
+
+ matrix = rotation_matrix.multiply(&matrix);
+ }
+
+ // Apply skew
+ {
+ let mut temp = Matrix3D::identity();
+ if decomposed.skew.2 != 0.0 {
+ temp.m32 = decomposed.skew.2;
+ matrix = temp.multiply(&matrix);
+ temp.m32 = 0.0;
+ }
+
+ if decomposed.skew.1 != 0.0 {
+ temp.m31 = decomposed.skew.1;
+ matrix = temp.multiply(&matrix);
+ temp.m31 = 0.0;
+ }
+
+ if decomposed.skew.0 != 0.0 {
+ temp.m21 = decomposed.skew.0;
+ matrix = temp.multiply(&matrix);
+ }
+ }
+
+ // Apply scale
+ matrix.apply_scale(&decomposed.scale);
+
+ matrix
+ }
+}
+
+/// Decompose a 3D matrix.
+/// https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix
+/// http://www.realtimerendering.com/resources/GraphicsGems/gemsii/unmatrix.c
+fn decompose_3d_matrix(mut matrix: Matrix3D) -> Result<MatrixDecomposed3D, ()> {
+ // Combine 2 point.
+ let combine = |a: [f32; 3], b: [f32; 3], ascl: f32, bscl: f32| {
+ [
+ (ascl * a[0]) + (bscl * b[0]),
+ (ascl * a[1]) + (bscl * b[1]),
+ (ascl * a[2]) + (bscl * b[2]),
+ ]
+ };
+ // Dot product.
+ let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ // Cross product.
+ let cross = |row1: [f32; 3], row2: [f32; 3]| {
+ [
+ row1[1] * row2[2] - row1[2] * row2[1],
+ row1[2] * row2[0] - row1[0] * row2[2],
+ row1[0] * row2[1] - row1[1] * row2[0],
+ ]
+ };
+
+ if matrix.m44 == 0.0 {
+ return Err(());
+ }
+
+ let scaling_factor = matrix.m44;
+
+ // Normalize the matrix.
+ matrix.scale_by_factor(1.0 / scaling_factor);
+
+ // perspective_matrix is used to solve for perspective, but it also provides
+ // an easy way to test for singularity of the upper 3x3 component.
+ let mut perspective_matrix = matrix;
+
+ perspective_matrix.m14 = 0.0;
+ perspective_matrix.m24 = 0.0;
+ perspective_matrix.m34 = 0.0;
+ perspective_matrix.m44 = 1.0;
+
+ if perspective_matrix.determinant() == 0.0 {
+ return Err(());
+ }
+
+ // First, isolate perspective.
+ let perspective = if matrix.m14 != 0.0 || matrix.m24 != 0.0 || matrix.m34 != 0.0 {
+ let right_hand_side: [f32; 4] = [matrix.m14, matrix.m24, matrix.m34, matrix.m44];
+
+ perspective_matrix = perspective_matrix.inverse().unwrap().transpose();
+ let perspective = perspective_matrix.pre_mul_point4(&right_hand_side);
+ // NOTE(emilio): Even though the reference algorithm clears the
+ // fourth column here (matrix.m14..matrix.m44), they're not used below
+ // so it's not really needed.
+ Perspective(
+ perspective[0],
+ perspective[1],
+ perspective[2],
+ perspective[3],
+ )
+ } else {
+ Perspective(0.0, 0.0, 0.0, 1.0)
+ };
+
+ // Next take care of translation (easy).
+ let translate = Translate3D(matrix.m41, matrix.m42, matrix.m43);
+
+ // Now get scale and shear. 'row' is a 3 element array of 3 component vectors
+ let mut row = matrix.get_matrix_3x3_part();
+
+ // Compute X scale factor and normalize first row.
+ let row0len = (row[0][0] * row[0][0] + row[0][1] * row[0][1] + row[0][2] * row[0][2]).sqrt();
+ let mut scale = Scale3D(row0len, 0.0, 0.0);
+ row[0] = [
+ row[0][0] / row0len,
+ row[0][1] / row0len,
+ row[0][2] / row0len,
+ ];
+
+ // Compute XY shear factor and make 2nd row orthogonal to 1st.
+ let mut skew = Skew(dot(row[0], row[1]), 0.0, 0.0);
+ row[1] = combine(row[1], row[0], 1.0, -skew.0);
+
+ // Now, compute Y scale and normalize 2nd row.
+ let row1len = (row[1][0] * row[1][0] + row[1][1] * row[1][1] + row[1][2] * row[1][2]).sqrt();
+ scale.1 = row1len;
+ row[1] = [
+ row[1][0] / row1len,
+ row[1][1] / row1len,
+ row[1][2] / row1len,
+ ];
+ skew.0 /= scale.1;
+
+ // Compute XZ and YZ shears, orthogonalize 3rd row
+ skew.1 = dot(row[0], row[2]);
+ row[2] = combine(row[2], row[0], 1.0, -skew.1);
+ skew.2 = dot(row[1], row[2]);
+ row[2] = combine(row[2], row[1], 1.0, -skew.2);
+
+ // Next, get Z scale and normalize 3rd row.
+ let row2len = (row[2][0] * row[2][0] + row[2][1] * row[2][1] + row[2][2] * row[2][2]).sqrt();
+ scale.2 = row2len;
+ row[2] = [
+ row[2][0] / row2len,
+ row[2][1] / row2len,
+ row[2][2] / row2len,
+ ];
+ skew.1 /= scale.2;
+ skew.2 /= scale.2;
+
+ // At this point, the matrix (in rows) is orthonormal.
+ // Check for a coordinate system flip. If the determinant
+ // is -1, then negate the matrix and the scaling factors.
+ if dot(row[0], cross(row[1], row[2])) < 0.0 {
+ scale.negate();
+ for i in 0..3 {
+ row[i][0] *= -1.0;
+ row[i][1] *= -1.0;
+ row[i][2] *= -1.0;
+ }
+ }
+
+ // Now, get the rotations out.
+ let mut quaternion = Quaternion(
+ 0.5 * ((1.0 + row[0][0] - row[1][1] - row[2][2]).max(0.0) as f64).sqrt(),
+ 0.5 * ((1.0 - row[0][0] + row[1][1] - row[2][2]).max(0.0) as f64).sqrt(),
+ 0.5 * ((1.0 - row[0][0] - row[1][1] + row[2][2]).max(0.0) as f64).sqrt(),
+ 0.5 * ((1.0 + row[0][0] + row[1][1] + row[2][2]).max(0.0) as f64).sqrt(),
+ );
+
+ if row[2][1] > row[1][2] {
+ quaternion.0 = -quaternion.0
+ }
+ if row[0][2] > row[2][0] {
+ quaternion.1 = -quaternion.1
+ }
+ if row[1][0] > row[0][1] {
+ quaternion.2 = -quaternion.2
+ }
+
+ Ok(MatrixDecomposed3D {
+ translate,
+ scale,
+ skew,
+ perspective,
+ quaternion,
+ })
+}
+
+/**
+ * The relevant section of the transitions specification:
+ * https://drafts.csswg.org/web-animations-1/#animation-types
+ * http://dev.w3.org/csswg/css3-transitions/#animation-of-property-types-
+ * defers all of the details to the 2-D and 3-D transforms specifications.
+ * For the 2-D transforms specification (all that's relevant for us, right
+ * now), the relevant section is:
+ * https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms
+ * This, in turn, refers to the unmatrix program in Graphics Gems,
+ * available from http://graphicsgems.org/ , and in
+ * particular as the file GraphicsGems/gemsii/unmatrix.c
+ * in http://graphicsgems.org/AllGems.tar.gz
+ *
+ * The unmatrix reference is for general 3-D transform matrices (any of the
+ * 16 components can have any value).
+ *
+ * For CSS 2-D transforms, we have a 2-D matrix with the bottom row constant:
+ *
+ * [ A C E ]
+ * [ B D F ]
+ * [ 0 0 1 ]
+ *
+ * For that case, I believe the algorithm in unmatrix reduces to:
+ *
+ * (1) If A * D - B * C == 0, the matrix is singular. Fail.
+ *
+ * (2) Set translation components (Tx and Ty) to the translation parts of
+ * the matrix (E and F) and then ignore them for the rest of the time.
+ * (For us, E and F each actually consist of three constants: a
+ * length, a multiplier for the width, and a multiplier for the
+ * height. This actually requires its own decomposition, but I'll
+ * keep that separate.)
+ *
+ * (3) Let the X scale (Sx) be sqrt(A^2 + B^2). Then divide both A and B
+ * by it.
+ *
+ * (4) Let the XY shear (K) be A * C + B * D. From C, subtract A times
+ * the XY shear. From D, subtract B times the XY shear.
+ *
+ * (5) Let the Y scale (Sy) be sqrt(C^2 + D^2). Divide C, D, and the XY
+ * shear (K) by it.
+ *
+ * (6) At this point, A * D - B * C is either 1 or -1. If it is -1,
+ * negate the XY shear (K), the X scale (Sx), and A, B, C, and D.
+ * (Alternatively, we could negate the XY shear (K) and the Y scale
+ * (Sy).)
+ *
+ * (7) Let the rotation be R = atan2(B, A).
+ *
+ * Then the resulting decomposed transformation is:
+ *
+ * translate(Tx, Ty) rotate(R) skewX(atan(K)) scale(Sx, Sy)
+ *
+ * An interesting result of this is that all of the simple transform
+ * functions (i.e., all functions other than matrix()), in isolation,
+ * decompose back to themselves except for:
+ * 'skewY(φ)', which is 'matrix(1, tan(φ), 0, 1, 0, 0)', which decomposes
+ * to 'rotate(φ) skewX(φ) scale(sec(φ), cos(φ))' since (ignoring the
+ * alternate sign possibilities that would get fixed in step 6):
+ * In step 3, the X scale factor is sqrt(1+tan²(φ)) = sqrt(sec²(φ)) =
+ * sec(φ). Thus, after step 3, A = 1/sec(φ) = cos(φ) and B = tan(φ) / sec(φ) =
+ * sin(φ). In step 4, the XY shear is sin(φ). Thus, after step 4, C =
+ * -cos(φ)sin(φ) and D = 1 - sin²(φ) = cos²(φ). Thus, in step 5, the Y scale is
+ * sqrt(cos²(φ)(sin²(φ) + cos²(φ)) = cos(φ). Thus, after step 5, C = -sin(φ), D
+ * = cos(φ), and the XY shear is tan(φ). Thus, in step 6, A * D - B * C =
+ * cos²(φ) + sin²(φ) = 1. In step 7, the rotation is thus φ.
+ *
+ * skew(θ, φ), which is matrix(1, tan(φ), tan(θ), 1, 0, 0), which decomposes
+ * to 'rotate(φ) skewX(θ + φ) scale(sec(φ), cos(φ))' since (ignoring
+ * the alternate sign possibilities that would get fixed in step 6):
+ * In step 3, the X scale factor is sqrt(1+tan²(φ)) = sqrt(sec²(φ)) =
+ * sec(φ). Thus, after step 3, A = 1/sec(φ) = cos(φ) and B = tan(φ) / sec(φ) =
+ * sin(φ). In step 4, the XY shear is cos(φ)tan(θ) + sin(φ). Thus, after step 4,
+ * C = tan(θ) - cos(φ)(cos(φ)tan(θ) + sin(φ)) = tan(θ)sin²(φ) - cos(φ)sin(φ)
+ * D = 1 - sin(φ)(cos(φ)tan(θ) + sin(φ)) = cos²(φ) - sin(φ)cos(φ)tan(θ)
+ * Thus, in step 5, the Y scale is sqrt(C² + D²) =
+ * sqrt(tan²(θ)(sin⁴(φ) + sin²(φ)cos²(φ)) -
+ * 2 tan(θ)(sin³(φ)cos(φ) + sin(φ)cos³(φ)) +
+ * (sin²(φ)cos²(φ) + cos⁴(φ))) =
+ * sqrt(tan²(θ)sin²(φ) - 2 tan(θ)sin(φ)cos(φ) + cos²(φ)) =
+ * cos(φ) - tan(θ)sin(φ) (taking the negative of the obvious solution so
+ * we avoid flipping in step 6).
+ * After step 5, C = -sin(φ) and D = cos(φ), and the XY shear is
+ * (cos(φ)tan(θ) + sin(φ)) / (cos(φ) - tan(θ)sin(φ)) =
+ * (dividing both numerator and denominator by cos(φ))
+ * (tan(θ) + tan(φ)) / (1 - tan(θ)tan(φ)) = tan(θ + φ).
+ * (See http://en.wikipedia.org/wiki/List_of_trigonometric_identities .)
+ * Thus, in step 6, A * D - B * C = cos²(φ) + sin²(φ) = 1.
+ * In step 7, the rotation is thus φ.
+ *
+ * To check this result, we can multiply things back together:
+ *
+ * [ cos(φ) -sin(φ) ] [ 1 tan(θ + φ) ] [ sec(φ) 0 ]
+ * [ sin(φ) cos(φ) ] [ 0 1 ] [ 0 cos(φ) ]
+ *
+ * [ cos(φ) cos(φ)tan(θ + φ) - sin(φ) ] [ sec(φ) 0 ]
+ * [ sin(φ) sin(φ)tan(θ + φ) + cos(φ) ] [ 0 cos(φ) ]
+ *
+ * but since tan(θ + φ) = (tan(θ) + tan(φ)) / (1 - tan(θ)tan(φ)),
+ * cos(φ)tan(θ + φ) - sin(φ)
+ * = cos(φ)(tan(θ) + tan(φ)) - sin(φ) + sin(φ)tan(θ)tan(φ)
+ * = cos(φ)tan(θ) + sin(φ) - sin(φ) + sin(φ)tan(θ)tan(φ)
+ * = cos(φ)tan(θ) + sin(φ)tan(θ)tan(φ)
+ * = tan(θ) (cos(φ) + sin(φ)tan(φ))
+ * = tan(θ) sec(φ) (cos²(φ) + sin²(φ))
+ * = tan(θ) sec(φ)
+ * and
+ * sin(φ)tan(θ + φ) + cos(φ)
+ * = sin(φ)(tan(θ) + tan(φ)) + cos(φ) - cos(φ)tan(θ)tan(φ)
+ * = tan(θ) (sin(φ) - sin(φ)) + sin(φ)tan(φ) + cos(φ)
+ * = sec(φ) (sin²(φ) + cos²(φ))
+ * = sec(φ)
+ * so the above is:
+ * [ cos(φ) tan(θ) sec(φ) ] [ sec(φ) 0 ]
+ * [ sin(φ) sec(φ) ] [ 0 cos(φ) ]
+ *
+ * [ 1 tan(θ) ]
+ * [ tan(φ) 1 ]
+ */
+
+/// Decompose a 2D matrix for Gecko. This implements the above decomposition algorithm.
+#[cfg(feature = "gecko")]
+fn decompose_2d_matrix(matrix: &Matrix3D) -> Result<MatrixDecomposed3D, ()> {
+ // The index is column-major, so the equivalent transform matrix is:
+ // | m11 m21 0 m41 | => | m11 m21 | and translate(m41, m42)
+ // | m12 m22 0 m42 | | m12 m22 |
+ // | 0 0 1 0 |
+ // | 0 0 0 1 |
+ let (mut m11, mut m12) = (matrix.m11, matrix.m12);
+ let (mut m21, mut m22) = (matrix.m21, matrix.m22);
+ // Check if this is a singular matrix.
+ if m11 * m22 == m12 * m21 {
+ return Err(());
+ }
+
+ let mut scale_x = (m11 * m11 + m12 * m12).sqrt();
+ m11 /= scale_x;
+ m12 /= scale_x;
+
+ let mut shear_xy = m11 * m21 + m12 * m22;
+ m21 -= m11 * shear_xy;
+ m22 -= m12 * shear_xy;
+
+ let scale_y = (m21 * m21 + m22 * m22).sqrt();
+ m21 /= scale_y;
+ m22 /= scale_y;
+ shear_xy /= scale_y;
+
+ let determinant = m11 * m22 - m12 * m21;
+ // Determinant should now be 1 or -1.
+ if 0.99 > determinant.abs() || determinant.abs() > 1.01 {
+ return Err(());
+ }
+
+ if determinant < 0. {
+ m11 = -m11;
+ m12 = -m12;
+ shear_xy = -shear_xy;
+ scale_x = -scale_x;
+ }
+
+ Ok(MatrixDecomposed3D {
+ translate: Translate3D(matrix.m41, matrix.m42, 0.),
+ scale: Scale3D(scale_x, scale_y, 1.),
+ skew: Skew(shear_xy, 0., 0.),
+ perspective: Perspective(0., 0., 0., 1.),
+ quaternion: Quaternion::from_direction_and_angle(
+ &DirectionVector::new(0., 0., 1.),
+ m12.atan2(m11) as f64,
+ ),
+ })
+}
+
+impl Animate for Matrix3D {
+ #[cfg(feature = "servo")]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.is_3d() || other.is_3d() {
+ let decomposed_from = decompose_3d_matrix(*self);
+ let decomposed_to = decompose_3d_matrix(*other);
+ match (decomposed_from, decomposed_to) {
+ (Ok(this), Ok(other)) => Ok(Matrix3D::from(this.animate(&other, procedure)?)),
+ // Matrices can be undecomposable due to couple reasons, e.g.,
+ // non-invertible matrices. In this case, we should report Err
+ // here, and let the caller do the fallback procedure.
+ _ => Err(()),
+ }
+ } else {
+ let this = MatrixDecomposed2D::from(*self);
+ let other = MatrixDecomposed2D::from(*other);
+ Ok(Matrix3D::from(this.animate(&other, procedure)?))
+ }
+ }
+
+ #[cfg(feature = "gecko")]
+ // Gecko doesn't exactly follow the spec here; we use a different procedure
+ // to match it
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (from, to) = if self.is_3d() || other.is_3d() {
+ (decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?)
+ } else {
+ (decompose_2d_matrix(self)?, decompose_2d_matrix(other)?)
+ };
+ // Matrices can be undecomposable due to couple reasons, e.g.,
+ // non-invertible matrices. In this case, we should report Err here,
+ // and let the caller do the fallback procedure.
+ Ok(Matrix3D::from(from.animate(&to, procedure)?))
+ }
+}
+
+impl ComputeSquaredDistance for Matrix3D {
+ #[inline]
+ #[cfg(feature = "servo")]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ if self.is_3d() || other.is_3d() {
+ let from = decompose_3d_matrix(*self)?;
+ let to = decompose_3d_matrix(*other)?;
+ from.compute_squared_distance(&to)
+ } else {
+ let from = MatrixDecomposed2D::from(*self);
+ let to = MatrixDecomposed2D::from(*other);
+ from.compute_squared_distance(&to)
+ }
+ }
+
+ #[inline]
+ #[cfg(feature = "gecko")]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let (from, to) = if self.is_3d() || other.is_3d() {
+ (decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?)
+ } else {
+ (decompose_2d_matrix(self)?, decompose_2d_matrix(other)?)
+ };
+ from.compute_squared_distance(&to)
+ }
+}
+
+// ------------------------------------
+// Animation for Transform list.
+// ------------------------------------
+fn is_matched_operation(
+ first: &ComputedTransformOperation,
+ second: &ComputedTransformOperation,
+) -> bool {
+ match (first, second) {
+ (&TransformOperation::Matrix(..), &TransformOperation::Matrix(..)) |
+ (&TransformOperation::Matrix3D(..), &TransformOperation::Matrix3D(..)) |
+ (&TransformOperation::Skew(..), &TransformOperation::Skew(..)) |
+ (&TransformOperation::SkewX(..), &TransformOperation::SkewX(..)) |
+ (&TransformOperation::SkewY(..), &TransformOperation::SkewY(..)) |
+ (&TransformOperation::Rotate(..), &TransformOperation::Rotate(..)) |
+ (&TransformOperation::Rotate3D(..), &TransformOperation::Rotate3D(..)) |
+ (&TransformOperation::RotateX(..), &TransformOperation::RotateX(..)) |
+ (&TransformOperation::RotateY(..), &TransformOperation::RotateY(..)) |
+ (&TransformOperation::RotateZ(..), &TransformOperation::RotateZ(..)) |
+ (&TransformOperation::Perspective(..), &TransformOperation::Perspective(..)) => true,
+ // Match functions that have the same primitive transform function
+ (a, b) if a.is_translate() && b.is_translate() => true,
+ (a, b) if a.is_scale() && b.is_scale() => true,
+ (a, b) if a.is_rotate() && b.is_rotate() => true,
+ // InterpolateMatrix and AccumulateMatrix are for mismatched transforms
+ _ => false,
+ }
+}
+
+/// <https://drafts.csswg.org/css-transforms/#interpolation-of-transforms>
+impl Animate for ComputedTransform {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ use std::borrow::Cow;
+
+ // Addition for transforms simply means appending to the list of
+ // transform functions. This is different to how we handle the other
+ // animation procedures so we treat it separately here rather than
+ // handling it in TransformOperation.
+ if procedure == Procedure::Add {
+ let result = self.0.iter().chain(&*other.0).cloned().collect();
+ return Ok(Transform(result));
+ }
+
+ let this = Cow::Borrowed(&self.0);
+ let other = Cow::Borrowed(&other.0);
+
+ // Interpolate the common prefix
+ let mut result = this
+ .iter()
+ .zip(other.iter())
+ .take_while(|(this, other)| is_matched_operation(this, other))
+ .map(|(this, other)| this.animate(other, procedure))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ // Deal with the remainders
+ let this_remainder = if this.len() > result.len() {
+ Some(&this[result.len()..])
+ } else {
+ None
+ };
+ let other_remainder = if other.len() > result.len() {
+ Some(&other[result.len()..])
+ } else {
+ None
+ };
+
+ match (this_remainder, other_remainder) {
+ // If there is a remainder from *both* lists we must have had mismatched functions.
+ // => Add the remainders to a suitable ___Matrix function.
+ (Some(this_remainder), Some(other_remainder)) => {
+ result.push(TransformOperation::animate_mismatched_transforms(
+ this_remainder,
+ other_remainder,
+ procedure,
+ )?);
+ },
+ // If there is a remainder from just one list, then one list must be shorter but
+ // completely match the type of the corresponding functions in the longer list.
+ // => Interpolate the remainder with identity transforms.
+ (Some(remainder), None) | (None, Some(remainder)) => {
+ let fill_right = this_remainder.is_some();
+ result.append(
+ &mut remainder
+ .iter()
+ .map(|transform| {
+ let identity = transform.to_animated_zero().unwrap();
+
+ match transform {
+ TransformOperation::AccumulateMatrix { .. } |
+ TransformOperation::InterpolateMatrix { .. } => {
+ let (from, to) = if fill_right {
+ (transform, &identity)
+ } else {
+ (&identity, transform)
+ };
+
+ TransformOperation::animate_mismatched_transforms(
+ &[from.clone()],
+ &[to.clone()],
+ procedure,
+ )
+ },
+ _ => {
+ let (lhs, rhs) = if fill_right {
+ (transform, &identity)
+ } else {
+ (&identity, transform)
+ };
+ lhs.animate(rhs, procedure)
+ },
+ }
+ })
+ .collect::<Result<Vec<_>, _>>()?,
+ );
+ },
+ (None, None) => {},
+ }
+
+ Ok(Transform(result.into()))
+ }
+}
+
+impl ComputeSquaredDistance for ComputedTransform {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let squared_dist = super::lists::with_zero::squared_distance(&self.0, &other.0);
+
+ // Roll back to matrix interpolation if there is any Err(()) in the
+ // transform lists, such as mismatched transform functions.
+ //
+ // FIXME: Using a zero size here seems a bit sketchy but matches the
+ // previous behavior.
+ if squared_dist.is_err() {
+ let rect = euclid::Rect::zero();
+ let matrix1: Matrix3D = self.to_transform_3d_matrix(Some(&rect))?.0.into();
+ let matrix2: Matrix3D = other.to_transform_3d_matrix(Some(&rect))?.0.into();
+ return matrix1.compute_squared_distance(&matrix2);
+ }
+
+ squared_dist
+ }
+}
+
+/// <http://dev.w3.org/csswg/css-transforms/#interpolation-of-transforms>
+impl Animate for ComputedTransformOperation {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self, other) {
+ (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => {
+ Ok(TransformOperation::Matrix3D(
+ this.animate(other, procedure)?,
+ ))
+ },
+ (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => {
+ Ok(TransformOperation::Matrix(this.animate(other, procedure)?))
+ },
+ (
+ &TransformOperation::Skew(ref fx, ref fy),
+ &TransformOperation::Skew(ref tx, ref ty),
+ ) => Ok(TransformOperation::Skew(
+ fx.animate(tx, procedure)?,
+ fy.animate(ty, procedure)?,
+ )),
+ (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) => {
+ Ok(TransformOperation::SkewX(f.animate(t, procedure)?))
+ },
+ (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => {
+ Ok(TransformOperation::SkewY(f.animate(t, procedure)?))
+ },
+ (
+ &TransformOperation::Translate3D(ref fx, ref fy, ref fz),
+ &TransformOperation::Translate3D(ref tx, ref ty, ref tz),
+ ) => Ok(TransformOperation::Translate3D(
+ fx.animate(tx, procedure)?,
+ fy.animate(ty, procedure)?,
+ fz.animate(tz, procedure)?,
+ )),
+ (
+ &TransformOperation::Translate(ref fx, ref fy),
+ &TransformOperation::Translate(ref tx, ref ty),
+ ) => Ok(TransformOperation::Translate(
+ fx.animate(tx, procedure)?,
+ fy.animate(ty, procedure)?,
+ )),
+ (&TransformOperation::TranslateX(ref f), &TransformOperation::TranslateX(ref t)) => {
+ Ok(TransformOperation::TranslateX(f.animate(t, procedure)?))
+ },
+ (&TransformOperation::TranslateY(ref f), &TransformOperation::TranslateY(ref t)) => {
+ Ok(TransformOperation::TranslateY(f.animate(t, procedure)?))
+ },
+ (&TransformOperation::TranslateZ(ref f), &TransformOperation::TranslateZ(ref t)) => {
+ Ok(TransformOperation::TranslateZ(f.animate(t, procedure)?))
+ },
+ (
+ &TransformOperation::Scale3D(ref fx, ref fy, ref fz),
+ &TransformOperation::Scale3D(ref tx, ref ty, ref tz),
+ ) => Ok(TransformOperation::Scale3D(
+ animate_multiplicative_factor(*fx, *tx, procedure)?,
+ animate_multiplicative_factor(*fy, *ty, procedure)?,
+ animate_multiplicative_factor(*fz, *tz, procedure)?,
+ )),
+ (&TransformOperation::ScaleX(ref f), &TransformOperation::ScaleX(ref t)) => Ok(
+ TransformOperation::ScaleX(animate_multiplicative_factor(*f, *t, procedure)?),
+ ),
+ (&TransformOperation::ScaleY(ref f), &TransformOperation::ScaleY(ref t)) => Ok(
+ TransformOperation::ScaleY(animate_multiplicative_factor(*f, *t, procedure)?),
+ ),
+ (&TransformOperation::ScaleZ(ref f), &TransformOperation::ScaleZ(ref t)) => Ok(
+ TransformOperation::ScaleZ(animate_multiplicative_factor(*f, *t, procedure)?),
+ ),
+ (
+ &TransformOperation::Scale(ref fx, ref fy),
+ &TransformOperation::Scale(ref tx, ref ty),
+ ) => Ok(TransformOperation::Scale(
+ animate_multiplicative_factor(*fx, *tx, procedure)?,
+ animate_multiplicative_factor(*fy, *ty, procedure)?,
+ )),
+ (
+ &TransformOperation::Rotate3D(fx, fy, fz, fa),
+ &TransformOperation::Rotate3D(tx, ty, tz, ta),
+ ) => {
+ let animated = Rotate::Rotate3D(fx, fy, fz, fa)
+ .animate(&Rotate::Rotate3D(tx, ty, tz, ta), procedure)?;
+ let (fx, fy, fz, fa) = ComputedRotate::resolve(&animated);
+ Ok(TransformOperation::Rotate3D(fx, fy, fz, fa))
+ },
+ (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) => {
+ Ok(TransformOperation::RotateX(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) => {
+ Ok(TransformOperation::RotateY(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) => {
+ Ok(TransformOperation::RotateZ(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => {
+ Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::Rotate(fa), &TransformOperation::RotateZ(ta)) => {
+ Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::RotateZ(fa), &TransformOperation::Rotate(ta)) => {
+ Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?))
+ },
+ (
+ &TransformOperation::Perspective(ref fd),
+ &TransformOperation::Perspective(ref td),
+ ) => {
+ use crate::values::computed::CSSPixelLength;
+ use crate::values::generics::transform::create_perspective_matrix;
+
+ // From https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions:
+ //
+ // The transform functions matrix(), matrix3d() and
+ // perspective() get converted into 4x4 matrices first and
+ // interpolated as defined in section Interpolation of
+ // Matrices afterwards.
+ //
+ let from = create_perspective_matrix(fd.infinity_or(|l| l.px()));
+ let to = create_perspective_matrix(td.infinity_or(|l| l.px()));
+
+ let interpolated = Matrix3D::from(from).animate(&Matrix3D::from(to), procedure)?;
+
+ let decomposed = decompose_3d_matrix(interpolated)?;
+ let perspective_z = decomposed.perspective.2;
+ // Clamp results outside of the -1 to 0 range so that we get perspective
+ // function values between 1 and infinity.
+ let used_value = if perspective_z >= 0. {
+ transform::PerspectiveFunction::None
+ } else {
+ transform::PerspectiveFunction::Length(CSSPixelLength::new(
+ if perspective_z <= -1. {
+ 1.
+ } else {
+ -1. / perspective_z
+ },
+ ))
+ };
+ Ok(TransformOperation::Perspective(used_value))
+ },
+ _ if self.is_translate() && other.is_translate() => self
+ .to_translate_3d()
+ .animate(&other.to_translate_3d(), procedure),
+ _ if self.is_scale() && other.is_scale() => {
+ self.to_scale_3d().animate(&other.to_scale_3d(), procedure)
+ },
+ _ if self.is_rotate() && other.is_rotate() => self
+ .to_rotate_3d()
+ .animate(&other.to_rotate_3d(), procedure),
+ _ => Err(()),
+ }
+ }
+}
+
+impl ComputedTransformOperation {
+ /// If there are no size dependencies, we try to animate in-place, to avoid
+ /// creating deeply nested Interpolate* operations.
+ fn try_animate_mismatched_transforms_in_place(
+ left: &[Self],
+ right: &[Self],
+ procedure: Procedure,
+ ) -> Result<Self, ()> {
+ let (left, _left_3d) = Transform::components_to_transform_3d_matrix(left, None)?;
+ let (right, _right_3d) = Transform::components_to_transform_3d_matrix(right, None)?;
+ Ok(Self::Matrix3D(
+ Matrix3D::from(left).animate(&Matrix3D::from(right), procedure)?,
+ ))
+ }
+
+ fn animate_mismatched_transforms(
+ left: &[Self],
+ right: &[Self],
+ procedure: Procedure,
+ ) -> Result<Self, ()> {
+ if let Ok(op) = Self::try_animate_mismatched_transforms_in_place(left, right, procedure) {
+ return Ok(op);
+ }
+ let from_list = Transform(left.to_vec().into());
+ let to_list = Transform(right.to_vec().into());
+ Ok(match procedure {
+ Procedure::Add => {
+ debug_assert!(false, "Addition should've been handled earlier");
+ return Err(());
+ },
+ Procedure::Interpolate { progress } => Self::InterpolateMatrix {
+ from_list,
+ to_list,
+ progress: Percentage(progress as f32),
+ },
+ Procedure::Accumulate { count } => Self::AccumulateMatrix {
+ from_list,
+ to_list,
+ count: cmp::min(count, i32::max_value() as u64) as i32,
+ },
+ })
+ }
+}
+
+// This might not be the most useful definition of distance. It might be better, for example,
+// to trace the distance travelled by a point as its transform is interpolated between the two
+// lists. That, however, proves to be quite complicated so we take a simple approach for now.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1318591#c0.
+impl ComputeSquaredDistance for ComputedTransformOperation {
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ match (self, other) {
+ (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => {
+ this.compute_squared_distance(other)
+ },
+ (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => {
+ let this: Matrix3D = (*this).into();
+ let other: Matrix3D = (*other).into();
+ this.compute_squared_distance(&other)
+ },
+ (
+ &TransformOperation::Skew(ref fx, ref fy),
+ &TransformOperation::Skew(ref tx, ref ty),
+ ) => Ok(fx.compute_squared_distance(&tx)? + fy.compute_squared_distance(&ty)?),
+ (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) |
+ (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => {
+ f.compute_squared_distance(&t)
+ },
+ (
+ &TransformOperation::Translate3D(ref fx, ref fy, ref fz),
+ &TransformOperation::Translate3D(ref tx, ref ty, ref tz),
+ ) => {
+ // For translate, We don't want to require doing layout in order
+ // to calculate the result, so drop the percentage part.
+ //
+ // However, dropping percentage makes us impossible to compute
+ // the distance for the percentage-percentage case, but Gecko
+ // uses the same formula, so it's fine for now.
+ let basis = Length::new(0.);
+ let fx = fx.resolve(basis).px();
+ let fy = fy.resolve(basis).px();
+ let tx = tx.resolve(basis).px();
+ let ty = ty.resolve(basis).px();
+
+ Ok(fx.compute_squared_distance(&tx)? +
+ fy.compute_squared_distance(&ty)? +
+ fz.compute_squared_distance(&tz)?)
+ },
+ (
+ &TransformOperation::Scale3D(ref fx, ref fy, ref fz),
+ &TransformOperation::Scale3D(ref tx, ref ty, ref tz),
+ ) => Ok(fx.compute_squared_distance(&tx)? +
+ fy.compute_squared_distance(&ty)? +
+ fz.compute_squared_distance(&tz)?),
+ (
+ &TransformOperation::Rotate3D(fx, fy, fz, fa),
+ &TransformOperation::Rotate3D(tx, ty, tz, ta),
+ ) => Rotate::Rotate3D(fx, fy, fz, fa)
+ .compute_squared_distance(&Rotate::Rotate3D(tx, ty, tz, ta)),
+ (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) |
+ (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) |
+ (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) |
+ (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => {
+ fa.compute_squared_distance(&ta)
+ },
+ (
+ &TransformOperation::Perspective(ref fd),
+ &TransformOperation::Perspective(ref td),
+ ) => fd
+ .infinity_or(|l| l.px())
+ .compute_squared_distance(&td.infinity_or(|l| l.px())),
+ (&TransformOperation::Perspective(ref p), &TransformOperation::Matrix3D(ref m)) |
+ (&TransformOperation::Matrix3D(ref m), &TransformOperation::Perspective(ref p)) => {
+ // FIXME(emilio): Is this right? Why interpolating this with
+ // Perspective but not with anything else?
+ let mut p_matrix = Matrix3D::identity();
+ let p = p.infinity_or(|p| p.px());
+ if p >= 0. {
+ p_matrix.m34 = -1. / p.max(1.);
+ }
+ p_matrix.compute_squared_distance(&m)
+ },
+ // Gecko cross-interpolates amongst all translate and all scale
+ // functions (See ToPrimitive in layout/style/StyleAnimationValue.cpp)
+ // without falling back to InterpolateMatrix
+ _ if self.is_translate() && other.is_translate() => self
+ .to_translate_3d()
+ .compute_squared_distance(&other.to_translate_3d()),
+ _ if self.is_scale() && other.is_scale() => self
+ .to_scale_3d()
+ .compute_squared_distance(&other.to_scale_3d()),
+ _ if self.is_rotate() && other.is_rotate() => self
+ .to_rotate_3d()
+ .compute_squared_distance(&other.to_rotate_3d()),
+ _ => Err(()),
+ }
+ }
+}
+
+// ------------------------------------
+// Individual transforms.
+// ------------------------------------
+/// <https://drafts.csswg.org/css-transforms-2/#propdef-rotate>
+impl ComputedRotate {
+ fn resolve(&self) -> (Number, Number, Number, Angle) {
+ // According to the spec:
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ //
+ // If the axis is unspecified, it defaults to "0 0 1"
+ match *self {
+ Rotate::None => (0., 0., 1., Angle::zero()),
+ Rotate::Rotate3D(rx, ry, rz, angle) => (rx, ry, rz, angle),
+ Rotate::Rotate(angle) => (0., 0., 1., angle),
+ }
+ }
+}
+
+impl Animate for ComputedRotate {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ use euclid::approxeq::ApproxEq;
+ match (self, other) {
+ (&Rotate::None, &Rotate::None) => Ok(Rotate::None),
+ (&Rotate::Rotate3D(fx, fy, fz, fa), &Rotate::None) => {
+ // We always normalize direction vector for rotate3d() first, so we should also
+ // apply the same rule for rotate property. In other words, we promote none into
+ // a 3d rotate, and normalize both direction vector first, and then do
+ // interpolation.
+ let (fx, fy, fz, fa) = transform::get_normalized_vector_and_angle(fx, fy, fz, fa);
+ Ok(Rotate::Rotate3D(
+ fx,
+ fy,
+ fz,
+ fa.animate(&Angle::zero(), procedure)?,
+ ))
+ },
+ (&Rotate::None, &Rotate::Rotate3D(tx, ty, tz, ta)) => {
+ // Normalize direction vector first.
+ let (tx, ty, tz, ta) = transform::get_normalized_vector_and_angle(tx, ty, tz, ta);
+ Ok(Rotate::Rotate3D(
+ tx,
+ ty,
+ tz,
+ Angle::zero().animate(&ta, procedure)?,
+ ))
+ },
+ (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => {
+ // https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
+
+ let (from, to) = (self.resolve(), other.resolve());
+ // For interpolations with the primitive rotate3d(), the direction vectors of the
+ // transform functions get normalized first.
+ let (fx, fy, fz, fa) =
+ transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3);
+ let (tx, ty, tz, ta) =
+ transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3);
+
+ // The rotation angle gets interpolated numerically and the rotation vector of the
+ // non-zero angle is used or (0, 0, 1) if both angles are zero.
+ //
+ // Note: the normalization may get two different vectors because of the
+ // floating-point precision, so we have to use approx_eq to compare two
+ // vectors.
+ let fv = DirectionVector::new(fx, fy, fz);
+ let tv = DirectionVector::new(tx, ty, tz);
+ if fa.is_zero() || ta.is_zero() || fv.approx_eq(&tv) {
+ let (x, y, z) = if fa.is_zero() && ta.is_zero() {
+ (0., 0., 1.)
+ } else if fa.is_zero() {
+ (tx, ty, tz)
+ } else {
+ // ta.is_zero() or both vectors are equal.
+ (fx, fy, fz)
+ };
+ return Ok(Rotate::Rotate3D(x, y, z, fa.animate(&ta, procedure)?));
+ }
+
+ // Slerp algorithm doesn't work well for Procedure::Add, which makes both
+ // |this_weight| and |other_weight| be 1.0, and this may make the cosine value of
+ // the angle be out of the range (i.e. the 4th component of the quaternion vector).
+ // (See Quaternion::animate() for more details about the Slerp formula.)
+ // Therefore, if the cosine value is out of range, we get an NaN after applying
+ // acos() on it, and so the result is invalid.
+ // Note: This is specialized for `rotate` property. The addition of `transform`
+ // property has been handled in `ComputedTransform::animate()` by merging two list
+ // directly.
+ let rq = if procedure == Procedure::Add {
+ // In Transform::animate(), it converts two rotations into transform matrices,
+ // and do matrix multiplication. This match the spec definition for the
+ // addition.
+ // https://drafts.csswg.org/css-transforms-2/#combining-transform-lists
+ let f = ComputedTransformOperation::Rotate3D(fx, fy, fz, fa);
+ let t = ComputedTransformOperation::Rotate3D(tx, ty, tz, ta);
+ let v =
+ Transform(vec![f].into()).animate(&Transform(vec![t].into()), procedure)?;
+ let (m, _) = v.to_transform_3d_matrix(None)?;
+ // Decompose the matrix and retrive the quaternion vector.
+ decompose_3d_matrix(Matrix3D::from(m))?.quaternion
+ } else {
+ // If the normalized vectors are not equal and both rotation angles are
+ // non-zero the transform functions get converted into 4x4 matrices first and
+ // interpolated as defined in section Interpolation of Matrices afterwards.
+ // However, per the spec issue [1], we prefer to converting the rotate3D into
+ // quaternion vectors directly, and then apply Slerp algorithm.
+ //
+ // Both ways should be identical, and converting rotate3D into quaternion
+ // vectors directly can avoid redundant math operations, e.g. the generation of
+ // the equivalent matrix3D and the unnecessary decomposition parts of
+ // translation, scale, skew, and persepctive in the matrix3D.
+ //
+ // [1] https://github.com/w3c/csswg-drafts/issues/9278
+ let fq = Quaternion::from_direction_and_angle(&fv, fa.radians64());
+ let tq = Quaternion::from_direction_and_angle(&tv, ta.radians64());
+ Quaternion::animate(&fq, &tq, procedure)?
+ };
+
+ debug_assert!(rq.3 <= 1.0 && rq.3 >= -1.0, "Invalid cosine value");
+ let (x, y, z, angle) = transform::get_normalized_vector_and_angle(
+ rq.0 as f32,
+ rq.1 as f32,
+ rq.2 as f32,
+ rq.3.acos() as f32 * 2.0,
+ );
+
+ Ok(Rotate::Rotate3D(x, y, z, Angle::from_radians(angle)))
+ },
+ (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => {
+ // If this is a 2D rotation, we just animate the <angle>
+ let (from, to) = (self.resolve().3, other.resolve().3);
+ Ok(Rotate::Rotate(from.animate(&to, procedure)?))
+ },
+ }
+ }
+}
+
+impl ComputeSquaredDistance for ComputedRotate {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ use euclid::approxeq::ApproxEq;
+ match (self, other) {
+ (&Rotate::None, &Rotate::None) => Ok(SquaredDistance::from_sqrt(0.)),
+ (&Rotate::Rotate3D(_, _, _, a), &Rotate::None) |
+ (&Rotate::None, &Rotate::Rotate3D(_, _, _, a)) => {
+ a.compute_squared_distance(&Angle::zero())
+ },
+ (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => {
+ let (from, to) = (self.resolve(), other.resolve());
+ let (mut fx, mut fy, mut fz, angle1) =
+ transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3);
+ let (mut tx, mut ty, mut tz, angle2) =
+ transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3);
+
+ if angle1.is_zero() && angle2.is_zero() {
+ (fx, fy, fz) = (0., 0., 1.);
+ (tx, ty, tz) = (0., 0., 1.);
+ } else if angle1.is_zero() {
+ (fx, fy, fz) = (tx, ty, tz);
+ } else if angle2.is_zero() {
+ (tx, ty, tz) = (fx, fy, fz);
+ }
+
+ let v1 = DirectionVector::new(fx, fy, fz);
+ let v2 = DirectionVector::new(tx, ty, tz);
+ if v1.approx_eq(&v2) {
+ angle1.compute_squared_distance(&angle2)
+ } else {
+ let q1 = Quaternion::from_direction_and_angle(&v1, angle1.radians64());
+ let q2 = Quaternion::from_direction_and_angle(&v2, angle2.radians64());
+ q1.compute_squared_distance(&q2)
+ }
+ },
+ (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => self
+ .resolve()
+ .3
+ .compute_squared_distance(&other.resolve().3),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-transforms-2/#propdef-translate>
+impl ComputedTranslate {
+ fn resolve(&self) -> (LengthPercentage, LengthPercentage, Length) {
+ // According to the spec:
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ //
+ // Unspecified translations default to 0px
+ match *self {
+ Translate::None => (
+ LengthPercentage::zero(),
+ LengthPercentage::zero(),
+ Length::zero(),
+ ),
+ Translate::Translate(ref tx, ref ty, ref tz) => (tx.clone(), ty.clone(), tz.clone()),
+ }
+ }
+}
+
+impl Animate for ComputedTranslate {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self, other) {
+ (&Translate::None, &Translate::None) => Ok(Translate::None),
+ (&Translate::Translate(_, ..), _) | (_, &Translate::Translate(_, ..)) => {
+ let (from, to) = (self.resolve(), other.resolve());
+ Ok(Translate::Translate(
+ from.0.animate(&to.0, procedure)?,
+ from.1.animate(&to.1, procedure)?,
+ from.2.animate(&to.2, procedure)?,
+ ))
+ },
+ }
+ }
+}
+
+impl ComputeSquaredDistance for ComputedTranslate {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let (from, to) = (self.resolve(), other.resolve());
+ Ok(from.0.compute_squared_distance(&to.0)? +
+ from.1.compute_squared_distance(&to.1)? +
+ from.2.compute_squared_distance(&to.2)?)
+ }
+}
+
+/// <https://drafts.csswg.org/css-transforms-2/#propdef-scale>
+impl ComputedScale {
+ fn resolve(&self) -> (Number, Number, Number) {
+ // According to the spec:
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ //
+ // Unspecified scales default to 1
+ match *self {
+ Scale::None => (1.0, 1.0, 1.0),
+ Scale::Scale(sx, sy, sz) => (sx, sy, sz),
+ }
+ }
+}
+
+impl Animate for ComputedScale {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self, other) {
+ (&Scale::None, &Scale::None) => Ok(Scale::None),
+ (&Scale::Scale(_, ..), _) | (_, &Scale::Scale(_, ..)) => {
+ let (from, to) = (self.resolve(), other.resolve());
+ // For transform lists, we add by appending to the list of
+ // transform functions. However, ComputedScale cannot be
+ // simply concatenated, so we have to calculate the additive
+ // result here.
+ if procedure == Procedure::Add {
+ // scale(x1,y1,z1)*scale(x2,y2,z2) = scale(x1*x2, y1*y2, z1*z2)
+ return Ok(Scale::Scale(from.0 * to.0, from.1 * to.1, from.2 * to.2));
+ }
+ Ok(Scale::Scale(
+ animate_multiplicative_factor(from.0, to.0, procedure)?,
+ animate_multiplicative_factor(from.1, to.1, procedure)?,
+ animate_multiplicative_factor(from.2, to.2, procedure)?,
+ ))
+ },
+ }
+ }
+}
+
+impl ComputeSquaredDistance for ComputedScale {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let (from, to) = (self.resolve(), other.resolve());
+ Ok(from.0.compute_squared_distance(&to.0)? +
+ from.1.compute_squared_distance(&to.1)? +
+ from.2.compute_squared_distance(&to.2)?)
+ }
+}
diff --git a/servo/components/style/values/computed/align.rs b/servo/components/style/values/computed/align.rs
new file mode 100644
index 0000000000..94847fd80f
--- /dev/null
+++ b/servo/components/style/values/computed/align.rs
@@ -0,0 +1,91 @@
+/* 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/. */
+
+//! Values for CSS Box Alignment properties
+//!
+//! https://drafts.csswg.org/css-align/
+
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::specified;
+
+pub use super::specified::{
+ AlignContent, AlignItems, AlignTracks, ContentDistribution, JustifyContent, JustifyTracks,
+ SelfAlignment,
+};
+pub use super::specified::{AlignSelf, JustifySelf};
+
+/// The computed value for the `justify-items` property.
+///
+/// Need to carry around both the specified and computed value to handle the
+/// special legacy keyword without destroying style sharing.
+///
+/// In particular, `justify-items` is a reset property, so we ought to be able
+/// to share its computed representation across elements as long as they match
+/// the same rules. Except that it's not true if the specified value for
+/// `justify-items` is `legacy` and the computed value of the parent has the
+/// `legacy` modifier.
+///
+/// So instead of computing `legacy` "normally" looking at get_parent_position(),
+/// marking it as uncacheable, we carry the specified value around and handle
+/// the special case in `StyleAdjuster` instead, only when the result of the
+/// computation would vary.
+///
+/// Note that we also need to special-case this property in matching.rs, in
+/// order to properly handle changes to the legacy keyword... This all kinda
+/// sucks :(.
+///
+/// See the discussion in https://bugzil.la/1384542.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue)]
+#[repr(C)]
+pub struct ComputedJustifyItems {
+ /// The specified value for the property. Can contain the bare `legacy`
+ /// keyword.
+ #[css(skip)]
+ pub specified: specified::JustifyItems,
+ /// The computed value for the property. Cannot contain the bare `legacy`
+ /// keyword, but note that it could contain it in combination with other
+ /// keywords like `left`, `right` or `center`.
+ pub computed: specified::JustifyItems,
+}
+
+pub use self::ComputedJustifyItems as JustifyItems;
+
+impl JustifyItems {
+ /// Returns the `legacy` value.
+ pub fn legacy() -> Self {
+ Self {
+ specified: specified::JustifyItems::legacy(),
+ computed: specified::JustifyItems::normal(),
+ }
+ }
+}
+
+impl ToComputedValue for specified::JustifyItems {
+ type ComputedValue = JustifyItems;
+
+ /// <https://drafts.csswg.org/css-align/#valdef-justify-items-legacy>
+ fn to_computed_value(&self, _context: &Context) -> JustifyItems {
+ use crate::values::specified::align;
+ let specified = *self;
+ let computed = if self.0 != align::AlignFlags::LEGACY {
+ *self
+ } else {
+ // If the inherited value of `justify-items` includes the
+ // `legacy` keyword, `legacy` computes to the inherited value, but
+ // we assume it computes to `normal`, and handle that special-case
+ // in StyleAdjuster.
+ Self::normal()
+ };
+
+ JustifyItems {
+ specified,
+ computed,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &JustifyItems) -> Self {
+ computed.specified
+ }
+}
diff --git a/servo/components/style/values/computed/angle.rs b/servo/components/style/values/computed/angle.rs
new file mode 100644
index 0000000000..ea321d2233
--- /dev/null
+++ b/servo/components/style/values/computed/angle.rs
@@ -0,0 +1,101 @@
+/* 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/. */
+
+//! Computed angles.
+
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::CSSFloat;
+use crate::Zero;
+use std::f64::consts::PI;
+use std::fmt::{self, Write};
+use std::{f32, f64};
+use style_traits::{CssWriter, ToCss};
+
+/// A computed angle in degrees.
+#[derive(
+ Add,
+ Animate,
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+)]
+#[repr(C)]
+pub struct Angle(CSSFloat);
+
+impl ToCss for Angle {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.degrees().to_css(dest)?;
+ dest.write_str("deg")
+ }
+}
+
+const RAD_PER_DEG: f64 = PI / 180.0;
+
+impl Angle {
+ /// Creates a computed `Angle` value from a radian amount.
+ pub fn from_radians(radians: CSSFloat) -> Self {
+ Angle(radians / RAD_PER_DEG as f32)
+ }
+
+ /// Creates a computed `Angle` value from a degrees amount.
+ #[inline]
+ pub fn from_degrees(degrees: CSSFloat) -> Self {
+ Angle(degrees)
+ }
+
+ /// Returns the amount of radians this angle represents.
+ #[inline]
+ pub fn radians(&self) -> CSSFloat {
+ self.radians64().min(f32::MAX as f64).max(f32::MIN as f64) as f32
+ }
+
+ /// Returns the amount of radians this angle represents as a `f64`.
+ ///
+ /// Gecko stores angles as singles, but does this computation using doubles.
+ ///
+ /// This is significant enough to mess up rounding to the nearest
+ /// quarter-turn for 225 degrees, for example.
+ #[inline]
+ pub fn radians64(&self) -> f64 {
+ self.0 as f64 * RAD_PER_DEG
+ }
+
+ /// Return the value in degrees.
+ #[inline]
+ pub fn degrees(&self) -> CSSFloat {
+ self.0
+ }
+}
+
+impl Zero for Angle {
+ #[inline]
+ fn zero() -> Self {
+ Angle(0.0)
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.0 == 0.
+ }
+}
+
+impl ComputeSquaredDistance for Angle {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ // Use the formula for calculating the distance between angles defined in SVG:
+ // https://www.w3.org/TR/SVG/animate.html#complexDistances
+ self.radians64()
+ .compute_squared_distance(&other.radians64())
+ }
+}
diff --git a/servo/components/style/values/computed/animation.rs b/servo/components/style/values/computed/animation.rs
new file mode 100644
index 0000000000..626dbe5347
--- /dev/null
+++ b/servo/components/style/values/computed/animation.rs
@@ -0,0 +1,70 @@
+/* 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/. */
+
+//! Computed values for properties related to animations and transitions
+
+use crate::values::computed::{Context, LengthPercentage, ToComputedValue};
+use crate::values::generics::animation as generics;
+use crate::values::specified::animation as specified;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+pub use crate::values::specified::animation::{
+ AnimationName, ScrollAxis, ScrollTimelineName, TransitionProperty, AnimationComposition,
+ AnimationDirection, AnimationFillMode, AnimationPlayState,
+};
+
+/// A computed value for the `animation-iteration-count` property.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub struct AnimationIterationCount(pub f32);
+
+impl ToComputedValue for specified::AnimationIterationCount {
+ type ComputedValue = AnimationIterationCount;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ AnimationIterationCount(match *self {
+ specified::AnimationIterationCount::Number(n) => n.to_computed_value(context).0,
+ specified::AnimationIterationCount::Infinite => f32::INFINITY,
+ })
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ use crate::values::specified::NonNegativeNumber;
+ if computed.0.is_infinite() {
+ specified::AnimationIterationCount::Infinite
+ } else {
+ specified::AnimationIterationCount::Number(NonNegativeNumber::new(computed.0))
+ }
+ }
+}
+
+impl AnimationIterationCount {
+ /// Returns the value `1.0`.
+ #[inline]
+ pub fn one() -> Self {
+ Self(1.0)
+ }
+}
+
+impl ToCss for AnimationIterationCount {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.0.is_infinite() {
+ dest.write_str("infinite")
+ } else {
+ self.0.to_css(dest)
+ }
+ }
+}
+
+/// A computed value for the `animation-timeline` property.
+pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>;
+
+/// A computed value for the `view-timeline-inset` property.
+pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>;
diff --git a/servo/components/style/values/computed/background.rs b/servo/components/style/values/computed/background.rs
new file mode 100644
index 0000000000..e2a58f8b74
--- /dev/null
+++ b/servo/components/style/values/computed/background.rs
@@ -0,0 +1,13 @@
+/* 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/. */
+
+//! Computed types for CSS values related to backgrounds.
+
+use crate::values::computed::length::NonNegativeLengthPercentage;
+use crate::values::generics::background::BackgroundSize as GenericBackgroundSize;
+
+pub use crate::values::specified::background::BackgroundRepeat;
+
+/// A computed value for the `background-size` property.
+pub type BackgroundSize = GenericBackgroundSize<NonNegativeLengthPercentage>;
diff --git a/servo/components/style/values/computed/basic_shape.rs b/servo/components/style/values/computed/basic_shape.rs
new file mode 100644
index 0000000000..d39110ec1c
--- /dev/null
+++ b/servo/components/style/values/computed/basic_shape.rs
@@ -0,0 +1,37 @@
+/* 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/. */
+
+//! CSS handling for the computed value of
+//! [`basic-shape`][basic-shape]s
+//!
+//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape
+
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{Image, LengthPercentage, NonNegativeLengthPercentage, Position};
+use crate::values::generics::basic_shape as generic;
+
+/// A computed alias for FillRule.
+pub use crate::values::generics::basic_shape::FillRule;
+
+/// A computed `clip-path` value.
+pub type ClipPath = generic::GenericClipPath<BasicShape, ComputedUrl>;
+
+/// A computed `shape-outside` value.
+pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;
+
+/// A computed basic shape.
+pub type BasicShape =
+ generic::GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercentage, InsetRect>;
+
+/// The computed value of `inset()`.
+pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>;
+
+/// A computed circle.
+pub type Circle = generic::Circle<Position, NonNegativeLengthPercentage>;
+
+/// A computed ellipse.
+pub type Ellipse = generic::Ellipse<Position, NonNegativeLengthPercentage>;
+
+/// The computed value of `ShapeRadius`.
+pub type ShapeRadius = generic::GenericShapeRadius<NonNegativeLengthPercentage>;
diff --git a/servo/components/style/values/computed/border.rs b/servo/components/style/values/computed/border.rs
new file mode 100644
index 0000000000..e073f671b3
--- /dev/null
+++ b/servo/components/style/values/computed/border.rs
@@ -0,0 +1,84 @@
+/* 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/. */
+
+//! Computed types for CSS values related to borders.
+
+use crate::values::computed::length::{NonNegativeLength, NonNegativeLengthPercentage};
+use crate::values::computed::{NonNegativeNumber, NonNegativeNumberOrPercentage};
+use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius;
+use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice;
+use crate::values::generics::border::BorderRadius as GenericBorderRadius;
+use crate::values::generics::border::BorderSpacing as GenericBorderSpacing;
+use crate::values::generics::border::GenericBorderImageSideWidth;
+use crate::values::generics::rect::Rect;
+use crate::values::generics::size::Size2D;
+use crate::values::generics::NonNegative;
+use crate::Zero;
+use app_units::Au;
+
+pub use crate::values::specified::border::BorderImageRepeat;
+
+/// A computed value for -webkit-text-stroke-width.
+pub type LineWidth = Au;
+
+/// A computed value for border-width (and the like).
+pub type BorderSideWidth = Au;
+
+/// A computed value for the `border-image-width` property.
+pub type BorderImageWidth = Rect<BorderImageSideWidth>;
+
+/// A computed value for a single side of a `border-image-width` property.
+pub type BorderImageSideWidth =
+ GenericBorderImageSideWidth<NonNegativeLengthPercentage, NonNegativeNumber>;
+
+/// A computed value for the `border-image-slice` property.
+pub type BorderImageSlice = GenericBorderImageSlice<NonNegativeNumberOrPercentage>;
+
+/// A computed value for the `border-radius` property.
+pub type BorderRadius = GenericBorderRadius<NonNegativeLengthPercentage>;
+
+/// A computed value for the `border-*-radius` longhand properties.
+pub type BorderCornerRadius = GenericBorderCornerRadius<NonNegativeLengthPercentage>;
+
+/// A computed value for the `border-spacing` longhand property.
+pub type BorderSpacing = GenericBorderSpacing<NonNegativeLength>;
+
+impl BorderImageSideWidth {
+ /// Returns `1`.
+ #[inline]
+ pub fn one() -> Self {
+ GenericBorderImageSideWidth::Number(NonNegative(1.))
+ }
+}
+
+impl BorderImageSlice {
+ /// Returns the `100%` value.
+ #[inline]
+ pub fn hundred_percent() -> Self {
+ GenericBorderImageSlice {
+ offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()),
+ fill: false,
+ }
+ }
+}
+
+impl BorderSpacing {
+ /// Returns `0 0`.
+ pub fn zero() -> Self {
+ GenericBorderSpacing(Size2D::new(
+ NonNegativeLength::zero(),
+ NonNegativeLength::zero(),
+ ))
+ }
+
+ /// Returns the horizontal spacing.
+ pub fn horizontal(&self) -> Au {
+ Au::from(*self.0.width())
+ }
+
+ /// Returns the vertical spacing.
+ pub fn vertical(&self) -> Au {
+ Au::from(*self.0.height())
+ }
+}
diff --git a/servo/components/style/values/computed/box.rs b/servo/components/style/values/computed/box.rs
new file mode 100644
index 0000000000..62811d9851
--- /dev/null
+++ b/servo/components/style/values/computed/box.rs
@@ -0,0 +1,388 @@
+/* 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/. */
+
+//! Computed types for box properties.
+
+use crate::values::animated::{Animate, Procedure, ToAnimatedValue};
+use crate::values::computed::font::FixedPoint;
+use crate::values::computed::length::{LengthPercentage, NonNegativeLength};
+use crate::values::computed::{Context, Integer, Number, ToComputedValue};
+use crate::values::generics::box_::{
+ GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign,
+};
+use crate::values::specified::box_ as specified;
+use std::fmt;
+use style_traits::{CssWriter, ToCss};
+
+pub use crate::values::specified::box_::{
+ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear as SpecifiedClear, Contain,
+ ContainerName, ContainerType, ContentVisibility, Display, Float as SpecifiedFloat, Overflow,
+ OverflowAnchor, OverflowClipBox, OverscrollBehavior, ScrollSnapAlign, ScrollSnapAxis,
+ ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, WillChange,
+};
+
+/// A computed value for the `vertical-align` property.
+pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>;
+
+/// A computed value for the `contain-intrinsic-size` property.
+pub type ContainIntrinsicSize = GenericContainIntrinsicSize<NonNegativeLength>;
+
+impl ContainIntrinsicSize {
+ /// Converts contain-intrinsic-size to auto style.
+ pub fn add_auto_if_needed(&self) -> Option<Self> {
+ Some(match *self {
+ Self::None => Self::AutoNone,
+ Self::Length(ref l) => Self::AutoLength(*l),
+ Self::AutoNone | Self::AutoLength(..) => return None,
+ })
+ }
+}
+
+/// A computed value for the `line-clamp` property.
+pub type LineClamp = GenericLineClamp<Integer>;
+
+impl Animate for LineClamp {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.is_none() != other.is_none() {
+ return Err(());
+ }
+ if self.is_none() {
+ return Ok(Self::none());
+ }
+ Ok(Self(self.0.animate(&other.0, procedure)?.max(1)))
+ }
+}
+
+/// A computed value for the `perspective` property.
+pub type Perspective = GenericPerspective<NonNegativeLength>;
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToResolvedValue,
+)]
+#[repr(u8)]
+/// A computed value for the `float` property.
+pub enum Float {
+ Left,
+ Right,
+ None,
+}
+
+impl Float {
+ /// Returns true if `self` is not `None`.
+ pub fn is_floating(self) -> bool {
+ self != Self::None
+ }
+}
+
+impl ToComputedValue for SpecifiedFloat {
+ type ComputedValue = Float;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let ltr = context.style().writing_mode.is_bidi_ltr();
+ // https://drafts.csswg.org/css-logical-props/#float-clear
+ match *self {
+ SpecifiedFloat::InlineStart => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if ltr {
+ Float::Left
+ } else {
+ Float::Right
+ }
+ },
+ SpecifiedFloat::InlineEnd => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if ltr {
+ Float::Right
+ } else {
+ Float::Left
+ }
+ },
+ SpecifiedFloat::Left => Float::Left,
+ SpecifiedFloat::Right => Float::Right,
+ SpecifiedFloat::None => Float::None,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> SpecifiedFloat {
+ match *computed {
+ Float::Left => SpecifiedFloat::Left,
+ Float::Right => SpecifiedFloat::Right,
+ Float::None => SpecifiedFloat::None,
+ }
+ }
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToResolvedValue,
+)]
+/// A computed value for the `clear` property.
+#[repr(u8)]
+pub enum Clear {
+ None,
+ Left,
+ Right,
+ Both,
+}
+
+impl ToComputedValue for SpecifiedClear {
+ type ComputedValue = Clear;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let ltr = context.style().writing_mode.is_bidi_ltr();
+ // https://drafts.csswg.org/css-logical-props/#float-clear
+ match *self {
+ SpecifiedClear::InlineStart => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if ltr {
+ Clear::Left
+ } else {
+ Clear::Right
+ }
+ },
+ SpecifiedClear::InlineEnd => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if ltr {
+ Clear::Right
+ } else {
+ Clear::Left
+ }
+ },
+ SpecifiedClear::None => Clear::None,
+ SpecifiedClear::Left => Clear::Left,
+ SpecifiedClear::Right => Clear::Right,
+ SpecifiedClear::Both => Clear::Both,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> SpecifiedClear {
+ match *computed {
+ Clear::None => SpecifiedClear::None,
+ Clear::Left => SpecifiedClear::Left,
+ Clear::Right => SpecifiedClear::Right,
+ Clear::Both => SpecifiedClear::Both,
+ }
+ }
+}
+
+/// A computed value for the `resize` property.
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, ToCss, ToResolvedValue)]
+#[repr(u8)]
+pub enum Resize {
+ None,
+ Both,
+ Horizontal,
+ Vertical,
+}
+
+impl ToComputedValue for specified::Resize {
+ type ComputedValue = Resize;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Resize {
+ let is_vertical = context.style().writing_mode.is_vertical();
+ match self {
+ specified::Resize::Inline => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if is_vertical {
+ Resize::Vertical
+ } else {
+ Resize::Horizontal
+ }
+ },
+ specified::Resize::Block => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if is_vertical {
+ Resize::Horizontal
+ } else {
+ Resize::Vertical
+ }
+ },
+ specified::Resize::None => Resize::None,
+ specified::Resize::Both => Resize::Both,
+ specified::Resize::Horizontal => Resize::Horizontal,
+ specified::Resize::Vertical => Resize::Vertical,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Resize) -> specified::Resize {
+ match computed {
+ Resize::None => specified::Resize::None,
+ Resize::Both => specified::Resize::Both,
+ Resize::Horizontal => specified::Resize::Horizontal,
+ Resize::Vertical => specified::Resize::Vertical,
+ }
+ }
+}
+
+/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375).
+pub const ZOOM_FRACTION_BITS: u16 = 6;
+
+/// This is an alias which is useful mostly as a cbindgen / C++ inference workaround.
+pub type ZoomFixedPoint = FixedPoint<u16, ZOOM_FRACTION_BITS>;
+
+/// The computed `zoom` property value. We store it as a 16-bit fixed point because we need to
+/// store it efficiently in the ComputedStyle representation. The assumption being that zooms over
+/// 1000 aren't quite useful.
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ ToResolvedValue,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(C)]
+pub struct Zoom(ZoomFixedPoint);
+
+impl ToComputedValue for specified::Zoom {
+ type ComputedValue = Zoom;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ let n = match *self {
+ Self::Normal => return Zoom::ONE,
+ Self::Document => return Zoom::DOCUMENT,
+ Self::Value(ref n) => n.0.to_number().get(),
+ };
+ if n == 0.0 {
+ // For legacy reasons, zoom: 0 (and 0%) computes to 1. ¯\_(ツ)_/¯
+ return Zoom::ONE;
+ }
+ Zoom(ZoomFixedPoint::from_float(n))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::new_number(computed.value())
+ }
+}
+
+impl ToCss for Zoom {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use std::fmt::Write;
+ if *self == Self::DOCUMENT {
+ return dest.write_str("document");
+ }
+ self.value().to_css(dest)
+ }
+}
+
+impl ToAnimatedValue for Zoom {
+ type AnimatedValue = Number;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.value()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Zoom(ZoomFixedPoint::from_float(animated.max(0.0)))
+ }
+}
+
+impl Zoom {
+ /// The value 1. This is by far the most common value.
+ pub const ONE: Zoom = Zoom(ZoomFixedPoint {
+ value: 1 << ZOOM_FRACTION_BITS,
+ });
+
+ /// The `document` value. This can appear in the computed zoom property value, but not in the
+ /// `effective_zoom` field.
+ pub const DOCUMENT: Zoom = Zoom(ZoomFixedPoint { value: 0 });
+
+ /// Returns whether we're the number 1.
+ #[inline]
+ pub fn is_one(self) -> bool {
+ self == Self::ONE
+ }
+
+ /// Returns the value as a float.
+ #[inline]
+ pub fn value(&self) -> f32 {
+ self.0.to_float()
+ }
+
+ /// Computes the effective zoom for a given new zoom value in rhs.
+ pub fn compute_effective(self, specified: Self) -> Self {
+ if specified == Self::DOCUMENT {
+ return Self::ONE;
+ }
+ if self == Self::ONE {
+ return specified;
+ }
+ if specified == Self::ONE {
+ return self;
+ }
+ Zoom(self.0 * specified.0)
+ }
+
+ /// Returns the zoomed value.
+ #[inline]
+ pub fn zoom(self, value: f32) -> f32 {
+ if self == Self::ONE {
+ return value;
+ }
+ self.value() * value
+ }
+}
diff --git a/servo/components/style/values/computed/color.rs b/servo/components/style/values/computed/color.rs
new file mode 100644
index 0000000000..9b5185d923
--- /dev/null
+++ b/servo/components/style/values/computed/color.rs
@@ -0,0 +1,95 @@
+/* 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/. */
+
+//! Computed color values.
+
+use crate::color::parsing::Color as CSSParserColor;
+use crate::color::AbsoluteColor;
+use crate::values::animated::ToAnimatedZero;
+use crate::values::computed::percentage::Percentage;
+use crate::values::generics::color::{
+ GenericCaretColor, GenericColor, GenericColorMix, GenericColorOrAuto,
+};
+use std::fmt;
+use style_traits::{CssWriter, ToCss};
+
+pub use crate::values::specified::color::{ColorScheme, ForcedColorAdjust, PrintColorAdjust};
+
+/// The computed value of the `color` property.
+pub type ColorPropertyValue = AbsoluteColor;
+
+/// A computed value for `<color>`.
+pub type Color = GenericColor<Percentage>;
+
+/// A computed color-mix().
+pub type ColorMix = GenericColorMix<Color, Percentage>;
+
+impl ToCss for Color {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ Self::Absolute(ref c) => c.to_css(dest),
+ Self::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest),
+ Self::ColorMix(ref m) => m.to_css(dest),
+ }
+ }
+}
+
+impl Color {
+ /// A fully transparent color.
+ pub const TRANSPARENT_BLACK: Self = Self::Absolute(AbsoluteColor::TRANSPARENT_BLACK);
+
+ /// An opaque black color.
+ pub const BLACK: Self = Self::Absolute(AbsoluteColor::BLACK);
+
+ /// An opaque white color.
+ pub const WHITE: Self = Self::Absolute(AbsoluteColor::WHITE);
+
+ /// Create a new computed [`Color`] from a given color-mix, simplifying it to an absolute color
+ /// if possible.
+ pub fn from_color_mix(color_mix: ColorMix) -> Self {
+ if let Some(absolute) = color_mix.mix_to_absolute() {
+ Self::Absolute(absolute)
+ } else {
+ Self::ColorMix(Box::new(color_mix))
+ }
+ }
+
+ /// Combine this complex color with the given foreground color into an
+ /// absolute color.
+ pub fn resolve_to_absolute(&self, current_color: &AbsoluteColor) -> AbsoluteColor {
+ use crate::values::specified::percentage::ToPercentage;
+
+ match *self {
+ Self::Absolute(c) => c,
+ Self::CurrentColor => *current_color,
+ Self::ColorMix(ref mix) => {
+ let left = mix.left.resolve_to_absolute(current_color);
+ let right = mix.right.resolve_to_absolute(current_color);
+ crate::color::mix::mix(
+ mix.interpolation,
+ &left,
+ mix.left_percentage.to_percentage(),
+ &right,
+ mix.right_percentage.to_percentage(),
+ mix.flags,
+ )
+ },
+ }
+ }
+}
+
+impl ToAnimatedZero for AbsoluteColor {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(Self::TRANSPARENT_BLACK)
+ }
+}
+
+/// auto | <color>
+pub type ColorOrAuto = GenericColorOrAuto<Color>;
+
+/// caret-color
+pub type CaretColor = GenericCaretColor<Color>;
diff --git a/servo/components/style/values/computed/column.rs b/servo/components/style/values/computed/column.rs
new file mode 100644
index 0000000000..38437ea110
--- /dev/null
+++ b/servo/components/style/values/computed/column.rs
@@ -0,0 +1,11 @@
+/* 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/. */
+
+//! Computed types for the column properties.
+
+use crate::values::computed::PositiveInteger;
+use crate::values::generics::column::ColumnCount as GenericColumnCount;
+
+/// A computed type for `column-count` values.
+pub type ColumnCount = GenericColumnCount<PositiveInteger>;
diff --git a/servo/components/style/values/computed/counters.rs b/servo/components/style/values/computed/counters.rs
new file mode 100644
index 0000000000..fd5e915c4a
--- /dev/null
+++ b/servo/components/style/values/computed/counters.rs
@@ -0,0 +1,26 @@
+/* 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/. */
+
+//! Computed values for counter properties
+
+use crate::values::computed::image::Image;
+use crate::values::generics::counters as generics;
+use crate::values::generics::counters::CounterIncrement as GenericCounterIncrement;
+use crate::values::generics::counters::CounterReset as GenericCounterReset;
+use crate::values::generics::counters::CounterSet as GenericCounterSet;
+
+/// A computed value for the `counter-increment` property.
+pub type CounterIncrement = GenericCounterIncrement<i32>;
+
+/// A computed value for the `counter-reset` property.
+pub type CounterReset = GenericCounterReset<i32>;
+
+/// A computed value for the `counter-set` property.
+pub type CounterSet = GenericCounterSet<i32>;
+
+/// A computed value for the `content` property.
+pub type Content = generics::GenericContent<Image>;
+
+/// A computed content item.
+pub type ContentItem = generics::GenericContentItem<Image>;
diff --git a/servo/components/style/values/computed/easing.rs b/servo/components/style/values/computed/easing.rs
new file mode 100644
index 0000000000..d351b3c71d
--- /dev/null
+++ b/servo/components/style/values/computed/easing.rs
@@ -0,0 +1,109 @@
+/* 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/. */
+
+//! Computed types for CSS Easing functions.
+
+use euclid::approxeq::ApproxEq;
+
+use crate::bezier::Bezier;
+use crate::piecewise_linear::PiecewiseLinearFunction;
+use crate::values::computed::{Integer, Number};
+use crate::values::generics::easing::{self, BeforeFlag, StepPosition, TimingKeyword};
+
+/// A computed timing function.
+pub type ComputedTimingFunction = easing::TimingFunction<Integer, Number, PiecewiseLinearFunction>;
+
+/// An alias of the computed timing function.
+pub type TimingFunction = ComputedTimingFunction;
+
+impl ComputedTimingFunction {
+ fn calculate_step_output(
+ steps: i32,
+ pos: StepPosition,
+ progress: f64,
+ before_flag: BeforeFlag,
+ ) -> f64 {
+ // User specified values can cause overflow (bug 1706157). Increments/decrements
+ // should be gravefully handled.
+ let mut current_step = (progress * (steps as f64)).floor() as i32;
+
+ // Increment current step if it is jump-start or start.
+ if pos == StepPosition::Start ||
+ pos == StepPosition::JumpStart ||
+ pos == StepPosition::JumpBoth
+ {
+ current_step = current_step.checked_add(1).unwrap_or(current_step);
+ }
+
+ // If the "before flag" is set and we are at a transition point,
+ // drop back a step
+ if before_flag == BeforeFlag::Set &&
+ (progress * steps as f64).rem_euclid(1.0).approx_eq(&0.0)
+ {
+ current_step = current_step.checked_sub(1).unwrap_or(current_step);
+ }
+
+ // We should not produce a result outside [0, 1] unless we have an
+ // input outside that range. This takes care of steps that would otherwise
+ // occur at boundaries.
+ if progress >= 0.0 && current_step < 0 {
+ current_step = 0;
+ }
+
+ // |jumps| should always be in [1, i32::MAX].
+ let jumps = if pos == StepPosition::JumpBoth {
+ steps.checked_add(1).unwrap_or(steps)
+ } else if pos == StepPosition::JumpNone {
+ steps.checked_sub(1).unwrap_or(steps)
+ } else {
+ steps
+ };
+
+ if progress <= 1.0 && current_step > jumps {
+ current_step = jumps;
+ }
+
+ (current_step as f64) / (jumps as f64)
+ }
+
+ /// The output of the timing function given the progress ratio of this animation.
+ pub fn calculate_output(&self, progress: f64, before_flag: BeforeFlag, epsilon: f64) -> f64 {
+ let progress = match self {
+ TimingFunction::CubicBezier { x1, y1, x2, y2 } => {
+ Bezier::calculate_bezier_output(progress, epsilon, *x1, *y1, *x2, *y2)
+ },
+ TimingFunction::Steps(steps, pos) => {
+ Self::calculate_step_output(*steps, *pos, progress, before_flag)
+ },
+ TimingFunction::LinearFunction(function) => function.at(progress as f32).into(),
+ TimingFunction::Keyword(keyword) => match keyword {
+ TimingKeyword::Linear => progress,
+ TimingKeyword::Ease => {
+ Bezier::calculate_bezier_output(progress, epsilon, 0.25, 0.1, 0.25, 1.)
+ },
+ TimingKeyword::EaseIn => {
+ Bezier::calculate_bezier_output(progress, epsilon, 0.42, 0., 1., 1.)
+ },
+ TimingKeyword::EaseOut => {
+ Bezier::calculate_bezier_output(progress, epsilon, 0., 0., 0.58, 1.)
+ },
+ TimingKeyword::EaseInOut => {
+ Bezier::calculate_bezier_output(progress, epsilon, 0.42, 0., 0.58, 1.)
+ },
+ },
+ };
+
+ // The output progress value of an easing function is a real number in the range:
+ // [-inf, inf].
+ // https://drafts.csswg.org/css-easing-1/#output-progress-value
+ //
+ // However, we expect to use the finite progress for interpolation and web-animations
+ // https://drafts.csswg.org/css-values-4/#interpolation
+ // https://drafts.csswg.org/web-animations-1/#dom-computedeffecttiming-progress
+ //
+ // So we clamp the infinite progress, per the spec issue:
+ // https://github.com/w3c/csswg-drafts/issues/8344
+ progress.min(f64::MAX).max(f64::MIN)
+ }
+}
diff --git a/servo/components/style/values/computed/effects.rs b/servo/components/style/values/computed/effects.rs
new file mode 100644
index 0000000000..b0a92024ca
--- /dev/null
+++ b/servo/components/style/values/computed/effects.rs
@@ -0,0 +1,44 @@
+/* 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/. */
+
+//! Computed types for CSS values related to effects.
+
+use crate::values::computed::color::Color;
+use crate::values::computed::length::{Length, NonNegativeLength};
+#[cfg(feature = "gecko")]
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{Angle, NonNegativeNumber, ZeroToOneNumber};
+use crate::values::generics::effects::BoxShadow as GenericBoxShadow;
+use crate::values::generics::effects::Filter as GenericFilter;
+use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow;
+#[cfg(not(feature = "gecko"))]
+use crate::values::Impossible;
+
+/// A computed value for a single shadow of the `box-shadow` property.
+pub type BoxShadow = GenericBoxShadow<Color, Length, NonNegativeLength, Length>;
+
+/// A computed value for a single `filter`.
+#[cfg(feature = "gecko")]
+pub type Filter = GenericFilter<
+ Angle,
+ NonNegativeNumber,
+ ZeroToOneNumber,
+ NonNegativeLength,
+ SimpleShadow,
+ ComputedUrl,
+>;
+
+/// A computed value for a single `filter`.
+#[cfg(feature = "servo")]
+pub type Filter = GenericFilter<
+ Angle,
+ NonNegativeNumber,
+ ZeroToOneNumber,
+ NonNegativeLength,
+ Impossible,
+ Impossible,
+>;
+
+/// A computed value for the `drop-shadow()` filter.
+pub type SimpleShadow = GenericSimpleShadow<Color, Length, NonNegativeLength>;
diff --git a/servo/components/style/values/computed/flex.rs b/servo/components/style/values/computed/flex.rs
new file mode 100644
index 0000000000..95c497ecf6
--- /dev/null
+++ b/servo/components/style/values/computed/flex.rs
@@ -0,0 +1,19 @@
+/* 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/. */
+
+//! Computed types for CSS values related to flexbox.
+
+use crate::values::computed::Size;
+use crate::values::generics::flex::FlexBasis as GenericFlexBasis;
+
+/// A computed value for the `flex-basis` property.
+pub type FlexBasis = GenericFlexBasis<Size>;
+
+impl FlexBasis {
+ /// `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ GenericFlexBasis::Size(Size::auto())
+ }
+}
diff --git a/servo/components/style/values/computed/font.rs b/servo/components/style/values/computed/font.rs
new file mode 100644
index 0000000000..de0a5e372b
--- /dev/null
+++ b/servo/components/style/values/computed/font.rs
@@ -0,0 +1,1369 @@
+/* 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/. */
+
+//! Computed values for font properties
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::animated::ToAnimatedValue;
+use crate::values::computed::{
+ Angle, Context, Integer, Length, NonNegativeLength, NonNegativeNumber, Number, Percentage,
+ ToComputedValue,
+};
+use crate::values::generics::font::{
+ FeatureTagValue, FontSettings, TaggedFontValue, VariationValue,
+};
+use crate::values::generics::{font as generics, NonNegative};
+use crate::values::resolved::{Context as ResolvedContext, ToResolvedValue};
+use crate::values::specified::font::{
+ self as specified, KeywordInfo, MAX_FONT_WEIGHT, MIN_FONT_WEIGHT,
+};
+use crate::values::specified::length::{FontBaseSize, LineHeightBase, NoCalcLength};
+use crate::Atom;
+use cssparser::{serialize_identifier, CssStringWriter, Parser};
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use num_traits::abs;
+use num_traits::cast::AsPrimitive;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+pub use crate::values::computed::Length as MozScriptMinSize;
+pub use crate::values::specified::font::MozScriptSizeMultiplier;
+pub use crate::values::specified::font::{FontPalette, FontSynthesis};
+pub use crate::values::specified::font::{
+ FontVariantAlternates, FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric, XLang,
+ XTextScale,
+};
+pub use crate::values::specified::Integer as SpecifiedInteger;
+pub use crate::values::specified::Number as SpecifiedNumber;
+
+/// Generic template for font property type classes that use a fixed-point
+/// internal representation with `FRACTION_BITS` for the fractional part.
+///
+/// Values are constructed from and exposed as floating-point, but stored
+/// internally as fixed point, so there will be a quantization effect on
+/// fractional values, depending on the number of fractional bits used.
+///
+/// Using (16-bit) fixed-point types rather than floats for these style
+/// attributes reduces the memory footprint of gfxFontEntry and gfxFontStyle; it
+/// will also tend to reduce the number of distinct font instances that get
+/// created, particularly when styles are animated or set to arbitrary values
+/// (e.g. by sliders in the UI), which should reduce pressure on graphics
+/// resources and improve cache hit rates.
+///
+/// cbindgen:derive-lt
+/// cbindgen:derive-lte
+/// cbindgen:derive-gt
+/// cbindgen:derive-gte
+#[repr(C)]
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ ToResolvedValue,
+)]
+pub struct FixedPoint<T, const FRACTION_BITS: u16> {
+ /// The actual representation.
+ pub value: T,
+}
+
+impl<T, const FRACTION_BITS: u16> FixedPoint<T, FRACTION_BITS>
+where
+ T: AsPrimitive<f32>,
+ f32: AsPrimitive<T>,
+ u16: AsPrimitive<T>,
+{
+ const SCALE: u16 = 1 << FRACTION_BITS;
+ const INVERSE_SCALE: f32 = 1.0 / Self::SCALE as f32;
+
+ /// Returns a fixed-point bit from a floating-point context.
+ pub fn from_float(v: f32) -> Self {
+ Self {
+ value: (v * Self::SCALE as f32).round().as_(),
+ }
+ }
+
+ /// Returns the floating-point representation.
+ pub fn to_float(&self) -> f32 {
+ self.value.as_() * Self::INVERSE_SCALE
+ }
+}
+
+// We implement this and mul below only for u16 types, because u32 types might need more care about
+// overflow. But it's not hard to implement in either case.
+impl<const FRACTION_BITS: u16> std::ops::Div for FixedPoint<u16, FRACTION_BITS> {
+ type Output = Self;
+ fn div(self, rhs: Self) -> Self {
+ Self {
+ value: (((self.value as u32) << (FRACTION_BITS as u32)) / (rhs.value as u32)) as u16,
+ }
+ }
+}
+impl<const FRACTION_BITS: u16> std::ops::Mul for FixedPoint<u16, FRACTION_BITS> {
+ type Output = Self;
+ fn mul(self, rhs: Self) -> Self {
+ Self {
+ value: (((self.value as u32) * (rhs.value as u32)) >> (FRACTION_BITS as u32)) as u16,
+ }
+ }
+}
+
+/// font-weight: range 1..1000, fractional values permitted; keywords
+/// 'normal', 'bold' aliased to 400, 700 respectively.
+///
+/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375)
+pub const FONT_WEIGHT_FRACTION_BITS: u16 = 6;
+
+/// This is an alias which is useful mostly as a cbindgen / C++ inference
+/// workaround.
+pub type FontWeightFixedPoint = FixedPoint<u16, FONT_WEIGHT_FRACTION_BITS>;
+
+/// A value for the font-weight property per:
+///
+/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight
+///
+/// cbindgen:derive-lt
+/// cbindgen:derive-lte
+/// cbindgen:derive-gt
+/// cbindgen:derive-gte
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ ToResolvedValue,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(C)]
+pub struct FontWeight(FontWeightFixedPoint);
+impl ToAnimatedValue for FontWeight {
+ type AnimatedValue = Number;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.value()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ FontWeight::from_float(animated)
+ }
+}
+
+impl ToCss for FontWeight {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.value().to_css(dest)
+ }
+}
+
+impl FontWeight {
+ /// The `normal` keyword.
+ pub const NORMAL: FontWeight = FontWeight(FontWeightFixedPoint {
+ value: 400 << FONT_WEIGHT_FRACTION_BITS,
+ });
+
+ /// The `bold` value.
+ pub const BOLD: FontWeight = FontWeight(FontWeightFixedPoint {
+ value: 700 << FONT_WEIGHT_FRACTION_BITS,
+ });
+
+ /// The threshold from which we consider a font bold.
+ pub const BOLD_THRESHOLD: FontWeight = FontWeight(FontWeightFixedPoint {
+ value: 600 << FONT_WEIGHT_FRACTION_BITS,
+ });
+
+ /// Returns the `normal` keyword value.
+ pub fn normal() -> Self {
+ Self::NORMAL
+ }
+
+ /// Weither this weight is bold
+ pub fn is_bold(&self) -> bool {
+ *self >= Self::BOLD_THRESHOLD
+ }
+
+ /// Returns the value as a float.
+ pub fn value(&self) -> f32 {
+ self.0.to_float()
+ }
+
+ /// Construct a valid weight from a float value.
+ pub fn from_float(v: f32) -> Self {
+ Self(FixedPoint::from_float(
+ v.max(MIN_FONT_WEIGHT).min(MAX_FONT_WEIGHT),
+ ))
+ }
+
+ /// Return the bolder weight.
+ ///
+ /// See the table in:
+ /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values
+ pub fn bolder(self) -> Self {
+ let value = self.value();
+ if value < 350. {
+ return Self::NORMAL;
+ }
+ if value < 550. {
+ return Self::BOLD;
+ }
+ Self::from_float(value.max(900.))
+ }
+
+ /// Return the lighter weight.
+ ///
+ /// See the table in:
+ /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values
+ pub fn lighter(self) -> Self {
+ let value = self.value();
+ if value < 550. {
+ return Self::from_float(value.min(100.));
+ }
+ if value < 750. {
+ return Self::NORMAL;
+ }
+ Self::BOLD
+ }
+}
+
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))]
+/// The computed value of font-size
+pub struct FontSize {
+ /// The computed size, that we use to compute ems etc. This accounts for
+ /// e.g., text-zoom.
+ pub computed_size: NonNegativeLength,
+ /// The actual used size. This is the computed font size, potentially
+ /// constrained by other factors like minimum font-size settings and so on.
+ #[css(skip)]
+ pub used_size: NonNegativeLength,
+ /// If derived from a keyword, the keyword and additional transformations applied to it
+ #[css(skip)]
+ pub keyword_info: KeywordInfo,
+}
+
+impl FontSize {
+ /// The actual computed font size.
+ #[inline]
+ pub fn computed_size(&self) -> Length {
+ self.computed_size.0
+ }
+
+ /// The actual used font size.
+ #[inline]
+ pub fn used_size(&self) -> Length {
+ self.used_size.0
+ }
+
+ #[inline]
+ /// Get default value of font size.
+ pub fn medium() -> Self {
+ Self {
+ computed_size: NonNegative(Length::new(specified::FONT_MEDIUM_PX)),
+ used_size: NonNegative(Length::new(specified::FONT_MEDIUM_PX)),
+ keyword_info: KeywordInfo::medium(),
+ }
+ }
+}
+
+impl ToAnimatedValue for FontSize {
+ type AnimatedValue = Length;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.computed_size.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ FontSize {
+ computed_size: NonNegative(animated.clamp_to_non_negative()),
+ used_size: NonNegative(animated.clamp_to_non_negative()),
+ keyword_info: KeywordInfo::none(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, ToComputedValue, ToResolvedValue)]
+#[cfg_attr(feature = "servo", derive(Hash, MallocSizeOf, Serialize, Deserialize))]
+/// Specifies a prioritized list of font family names or generic family names.
+#[repr(C)]
+pub struct FontFamily {
+ /// The actual list of family names.
+ pub families: FontFamilyList,
+ /// Whether this font-family came from a specified system-font.
+ pub is_system_font: bool,
+ /// Whether this is the initial font-family that might react to language
+ /// changes.
+ pub is_initial: bool,
+}
+
+macro_rules! static_font_family {
+ ($ident:ident, $family:expr) => {
+ lazy_static! {
+ static ref $ident: FontFamily = FontFamily {
+ families: FontFamilyList {
+ list: crate::ArcSlice::from_iter_leaked(std::iter::once($family)),
+ },
+ is_system_font: false,
+ is_initial: false,
+ };
+ }
+ };
+}
+
+impl FontFamily {
+ #[inline]
+ /// Get default font family as `serif` which is a generic font-family
+ pub fn serif() -> Self {
+ Self::generic(GenericFontFamily::Serif).clone()
+ }
+
+ /// Returns the font family for `-moz-bullet-font`.
+ pub(crate) fn moz_bullet() -> &'static Self {
+ static_font_family!(
+ MOZ_BULLET,
+ SingleFontFamily::FamilyName(FamilyName {
+ name: atom!("-moz-bullet-font"),
+ syntax: FontFamilyNameSyntax::Identifiers,
+ })
+ );
+
+ &*MOZ_BULLET
+ }
+
+ /// Returns a font family for a single system font.
+ pub fn for_system_font(name: &str) -> Self {
+ Self {
+ families: FontFamilyList {
+ list: crate::ArcSlice::from_iter(std::iter::once(SingleFontFamily::FamilyName(
+ FamilyName {
+ name: Atom::from(name),
+ syntax: FontFamilyNameSyntax::Identifiers,
+ },
+ ))),
+ },
+ is_system_font: true,
+ is_initial: false,
+ }
+ }
+
+ /// Returns a generic font family.
+ pub fn generic(generic: GenericFontFamily) -> &'static Self {
+ macro_rules! generic_font_family {
+ ($ident:ident, $family:ident) => {
+ static_font_family!(
+ $ident,
+ SingleFontFamily::Generic(GenericFontFamily::$family)
+ )
+ };
+ }
+
+ generic_font_family!(SERIF, Serif);
+ generic_font_family!(SANS_SERIF, SansSerif);
+ generic_font_family!(MONOSPACE, Monospace);
+ generic_font_family!(CURSIVE, Cursive);
+ generic_font_family!(FANTASY, Fantasy);
+ generic_font_family!(MOZ_EMOJI, MozEmoji);
+ generic_font_family!(SYSTEM_UI, SystemUi);
+
+ let family = match generic {
+ GenericFontFamily::None => {
+ debug_assert!(false, "Bogus caller!");
+ &*SERIF
+ },
+ GenericFontFamily::Serif => &*SERIF,
+ GenericFontFamily::SansSerif => &*SANS_SERIF,
+ GenericFontFamily::Monospace => &*MONOSPACE,
+ GenericFontFamily::Cursive => &*CURSIVE,
+ GenericFontFamily::Fantasy => &*FANTASY,
+ GenericFontFamily::MozEmoji => &*MOZ_EMOJI,
+ GenericFontFamily::SystemUi => &*SYSTEM_UI,
+ };
+ debug_assert_eq!(
+ *family.families.iter().next().unwrap(),
+ SingleFontFamily::Generic(generic)
+ );
+ family
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl MallocSizeOf for FontFamily {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ use malloc_size_of::MallocUnconditionalSizeOf;
+ // SharedFontList objects are generally measured from the pointer stored
+ // in the specified value. So only count this if the SharedFontList is
+ // unshared.
+ let shared_font_list = &self.families.list;
+ if shared_font_list.is_unique() {
+ shared_font_list.unconditional_size_of(ops)
+ } else {
+ 0
+ }
+ }
+}
+
+impl ToCss for FontFamily {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let mut iter = self.families.iter();
+ match iter.next() {
+ Some(f) => f.to_css(dest)?,
+ None => return Ok(()),
+ }
+ for family in iter {
+ dest.write_str(", ")?;
+ family.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// The name of a font family of choice.
+#[derive(
+ Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(C)]
+pub struct FamilyName {
+ /// Name of the font family.
+ pub name: Atom,
+ /// Syntax of the font family.
+ pub syntax: FontFamilyNameSyntax,
+}
+
+impl FamilyName {
+ fn is_known_icon_font_family(&self) -> bool {
+ use crate::gecko_bindings::bindings;
+ unsafe { bindings::Gecko_IsKnownIconFontFamily(self.name.as_ptr()) }
+ }
+}
+
+impl ToCss for FamilyName {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match self.syntax {
+ FontFamilyNameSyntax::Quoted => {
+ dest.write_char('"')?;
+ write!(CssStringWriter::new(dest), "{}", self.name)?;
+ dest.write_char('"')
+ },
+ FontFamilyNameSyntax::Identifiers => {
+ let mut first = true;
+ for ident in self.name.to_string().split(' ') {
+ if first {
+ first = false;
+ } else {
+ dest.write_char(' ')?;
+ }
+ debug_assert!(
+ !ident.is_empty(),
+ "Family name with leading, \
+ trailing, or consecutive white spaces should \
+ have been marked quoted by the parser"
+ );
+ serialize_identifier(ident, dest)?;
+ }
+ Ok(())
+ },
+ }
+ }
+}
+
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+/// Font family names must either be given quoted as strings,
+/// or unquoted as a sequence of one or more identifiers.
+#[repr(u8)]
+pub enum FontFamilyNameSyntax {
+ /// The family name was specified in a quoted form, e.g. "Font Name"
+ /// or 'Font Name'.
+ Quoted,
+
+ /// The family name was specified in an unquoted form as a sequence of
+ /// identifiers.
+ Identifiers,
+}
+
+/// A set of faces that vary in weight, width or slope.
+/// cbindgen:derive-mut-casts=true
+#[derive(
+ Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize, Hash))]
+#[repr(u8)]
+pub enum SingleFontFamily {
+ /// The name of a font family of choice.
+ FamilyName(FamilyName),
+ /// Generic family name.
+ Generic(GenericFontFamily),
+}
+
+fn system_ui_enabled(_: &ParserContext) -> bool {
+ static_prefs::pref!("layout.css.system-ui.enabled")
+}
+
+/// A generic font-family name.
+///
+/// The order here is important, if you change it make sure that
+/// `gfxPlatformFontList.h`s ranged array and `gfxFontFamilyList`'s
+/// sSingleGenerics are updated as well.
+///
+/// NOTE(emilio): Should be u8, but it's a u32 because of ABI issues between GCC
+/// and LLVM see https://bugs.llvm.org/show_bug.cgi?id=44228 / bug 1600735 /
+/// bug 1726515.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(u32)]
+#[allow(missing_docs)]
+pub enum GenericFontFamily {
+ /// No generic family specified, only for internal usage.
+ ///
+ /// NOTE(emilio): Gecko code relies on this variant being zero.
+ #[css(skip)]
+ None = 0,
+ Serif,
+ SansSerif,
+ #[parse(aliases = "-moz-fixed")]
+ Monospace,
+ Cursive,
+ Fantasy,
+ #[parse(condition = "system_ui_enabled")]
+ SystemUi,
+ /// An internal value for emoji font selection.
+ #[css(skip)]
+ #[cfg(feature = "gecko")]
+ MozEmoji,
+}
+
+impl GenericFontFamily {
+ /// When we disallow websites to override fonts, we ignore some generic
+ /// families that the website might specify, since they're not configured by
+ /// the user. See bug 789788 and bug 1730098.
+ pub(crate) fn valid_for_user_font_prioritization(self) -> bool {
+ match self {
+ Self::None | Self::Fantasy | Self::Cursive | Self::SystemUi | Self::MozEmoji => false,
+
+ Self::Serif | Self::SansSerif | Self::Monospace => true,
+ }
+ }
+}
+
+impl Parse for SingleFontFamily {
+ /// Parse a font-family value.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) {
+ return Ok(SingleFontFamily::FamilyName(FamilyName {
+ name: Atom::from(&*value),
+ syntax: FontFamilyNameSyntax::Quoted,
+ }));
+ }
+
+ if let Ok(generic) = input.try_parse(|i| GenericFontFamily::parse(context, i)) {
+ return Ok(SingleFontFamily::Generic(generic));
+ }
+
+ let first_ident = input.expect_ident_cloned()?;
+ let reserved = match_ignore_ascii_case! { &first_ident,
+ // https://drafts.csswg.org/css-fonts/#propdef-font-family
+ // "Font family names that happen to be the same as a keyword value
+ // (`inherit`, `serif`, `sans-serif`, `monospace`, `fantasy`, and `cursive`)
+ // must be quoted to prevent confusion with the keywords with the same names.
+ // The keywords ‘initial’ and ‘default’ are reserved for future use
+ // and must also be quoted when used as font names.
+ // UAs must not consider these keywords as matching the <family-name> type."
+ "inherit" | "initial" | "unset" | "revert" | "default" => true,
+ _ => false,
+ };
+
+ let mut value = first_ident.as_ref().to_owned();
+ let mut serialize_quoted = value.contains(' ');
+
+ // These keywords are not allowed by themselves.
+ // The only way this value can be valid with with another keyword.
+ if reserved {
+ let ident = input.expect_ident()?;
+ serialize_quoted = serialize_quoted || ident.contains(' ');
+ value.push(' ');
+ value.push_str(&ident);
+ }
+ while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
+ serialize_quoted = serialize_quoted || ident.contains(' ');
+ value.push(' ');
+ value.push_str(&ident);
+ }
+ let syntax = if serialize_quoted {
+ // For font family names which contains special white spaces, e.g.
+ // `font-family: \ a\ \ b\ \ c\ ;`, it is tricky to serialize them
+ // as identifiers correctly. Just mark them quoted so we don't need
+ // to worry about them in serialization code.
+ FontFamilyNameSyntax::Quoted
+ } else {
+ FontFamilyNameSyntax::Identifiers
+ };
+ Ok(SingleFontFamily::FamilyName(FamilyName {
+ name: Atom::from(value),
+ syntax,
+ }))
+ }
+}
+
+#[cfg(feature = "servo")]
+impl SingleFontFamily {
+ /// Get the corresponding font-family with Atom
+ pub fn from_atom(input: Atom) -> SingleFontFamily {
+ match input {
+ atom!("serif") => return SingleFontFamily::Generic(GenericFontFamily::Serif),
+ atom!("sans-serif") => return SingleFontFamily::Generic(GenericFontFamily::SansSerif),
+ atom!("cursive") => return SingleFontFamily::Generic(GenericFontFamily::Cursive),
+ atom!("fantasy") => return SingleFontFamily::Generic(GenericFontFamily::Fantasy),
+ atom!("monospace") => return SingleFontFamily::Generic(GenericFontFamily::Monospace),
+ _ => {},
+ }
+
+ match_ignore_ascii_case! { &input,
+ "serif" => return SingleFontFamily::Generic(GenericFontFamily::Serif),
+ "sans-serif" => return SingleFontFamily::Generic(GenericFontFamily::SansSerif),
+ "cursive" => return SingleFontFamily::Generic(GenericFontFamily::Cursive),
+ "fantasy" => return SingleFontFamily::Generic(GenericFontFamily::Fantasy),
+ "monospace" => return SingleFontFamily::Generic(GenericFontFamily::Monospace),
+ _ => {}
+ }
+
+ // We don't know if it's quoted or not. So we set it to
+ // quoted by default.
+ SingleFontFamily::FamilyName(FamilyName {
+ name: input,
+ syntax: FontFamilyNameSyntax::Quoted,
+ })
+ }
+}
+
+/// A list of font families.
+#[derive(Clone, Debug, ToComputedValue, ToResolvedValue, ToShmem, PartialEq, Eq)]
+#[repr(C)]
+pub struct FontFamilyList {
+ /// The actual list of font families specified.
+ pub list: crate::ArcSlice<SingleFontFamily>,
+}
+
+impl FontFamilyList {
+ /// Return iterator of SingleFontFamily
+ pub fn iter(&self) -> impl Iterator<Item = &SingleFontFamily> {
+ self.list.iter()
+ }
+
+ /// If there's a generic font family on the list which is suitable for user
+ /// font prioritization, then move it ahead of the other families in the list,
+ /// except for any families known to be ligature-based icon fonts, where using a
+ /// generic instead of the site's specified font may cause substantial breakage.
+ /// If no suitable generic is found in the list, insert the default generic ahead
+ /// of all the listed families except for known ligature-based icon fonts.
+ pub(crate) fn prioritize_first_generic_or_prepend(&mut self, generic: GenericFontFamily) {
+ let mut index_of_first_generic = None;
+ let mut target_index = None;
+
+ for (i, f) in self.iter().enumerate() {
+ match &*f {
+ SingleFontFamily::Generic(f) => {
+ if index_of_first_generic.is_none() && f.valid_for_user_font_prioritization() {
+ // If we haven't found a target position, there's nothing to do;
+ // this entry is already ahead of everything except any whitelisted
+ // icon fonts.
+ if target_index.is_none() {
+ return;
+ }
+ index_of_first_generic = Some(i);
+ break;
+ }
+ // A non-prioritized generic (e.g. cursive, fantasy) becomes the target
+ // position for prioritization, just like arbitrary named families.
+ if target_index.is_none() {
+ target_index = Some(i);
+ }
+ },
+ SingleFontFamily::FamilyName(fam) => {
+ // Target position for the first generic is in front of the first
+ // non-whitelisted icon font family we find.
+ if target_index.is_none() && !fam.is_known_icon_font_family() {
+ target_index = Some(i);
+ }
+ },
+ }
+ }
+
+ let mut new_list = self.list.iter().cloned().collect::<Vec<_>>();
+ let first_generic = match index_of_first_generic {
+ Some(i) => new_list.remove(i),
+ None => SingleFontFamily::Generic(generic),
+ };
+
+ if let Some(i) = target_index {
+ new_list.insert(i, first_generic);
+ } else {
+ new_list.push(first_generic);
+ }
+ self.list = crate::ArcSlice::from_iter(new_list.into_iter());
+ }
+
+ /// Returns whether we need to prioritize user fonts.
+ pub(crate) fn needs_user_font_prioritization(&self) -> bool {
+ self.iter().next().map_or(true, |f| match f {
+ SingleFontFamily::Generic(f) => !f.valid_for_user_font_prioritization(),
+ _ => true,
+ })
+ }
+
+ /// Return the generic ID if it is a single generic font
+ pub fn single_generic(&self) -> Option<GenericFontFamily> {
+ let mut iter = self.iter();
+ if let Some(SingleFontFamily::Generic(f)) = iter.next() {
+ if iter.next().is_none() {
+ return Some(*f);
+ }
+ }
+ None
+ }
+}
+
+/// Preserve the readability of text when font fallback occurs.
+pub type FontSizeAdjust = generics::GenericFontSizeAdjust<NonNegativeNumber>;
+
+impl FontSizeAdjust {
+ #[inline]
+ /// Default value of font-size-adjust
+ pub fn none() -> Self {
+ FontSizeAdjust::None
+ }
+}
+
+impl ToComputedValue for specified::FontSizeAdjust {
+ type ComputedValue = FontSizeAdjust;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ use crate::font_metrics::FontMetricsOrientation;
+
+ let font_metrics = |vertical| {
+ let orient = if vertical {
+ FontMetricsOrientation::MatchContextPreferVertical
+ } else {
+ FontMetricsOrientation::Horizontal
+ };
+ let metrics = context.query_font_metrics(FontBaseSize::CurrentStyle, orient, false);
+ let font_size = context.style().get_font().clone_font_size().used_size.0;
+ (metrics, font_size)
+ };
+
+ // Macro to resolve a from-font value using the given metric field. If not present,
+ // returns the fallback value, or if that is negative, resolves using ascent instead
+ // of the missing field (this is the fallback for cap-height).
+ macro_rules! resolve {
+ ($basis:ident, $value:expr, $vertical:expr, $field:ident, $fallback:expr) => {{
+ match $value {
+ specified::FontSizeAdjustFactor::Number(f) => {
+ FontSizeAdjust::$basis(f.to_computed_value(context))
+ },
+ specified::FontSizeAdjustFactor::FromFont => {
+ let (metrics, font_size) = font_metrics($vertical);
+ let ratio = if let Some(metric) = metrics.$field {
+ metric / font_size
+ } else if $fallback >= 0.0 {
+ $fallback
+ } else {
+ metrics.ascent / font_size
+ };
+ if ratio.is_nan() {
+ FontSizeAdjust::$basis(NonNegative(abs($fallback)))
+ } else {
+ FontSizeAdjust::$basis(NonNegative(ratio))
+ }
+ },
+ }
+ }};
+ }
+
+ match *self {
+ Self::None => FontSizeAdjust::None,
+ Self::ExHeight(val) => resolve!(ExHeight, val, false, x_height, 0.5),
+ Self::CapHeight(val) => {
+ resolve!(CapHeight, val, false, cap_height, -1.0 /* fall back to ascent */)
+ },
+ Self::ChWidth(val) => resolve!(ChWidth, val, false, zero_advance_measure, 0.5),
+ Self::IcWidth(val) => resolve!(IcWidth, val, false, ic_width, 1.0),
+ Self::IcHeight(val) => resolve!(IcHeight, val, true, ic_width, 1.0),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ macro_rules! case {
+ ($basis:ident, $val:expr) => {
+ Self::$basis(specified::FontSizeAdjustFactor::Number(
+ ToComputedValue::from_computed_value($val),
+ ))
+ };
+ }
+ match *computed {
+ FontSizeAdjust::None => Self::None,
+ FontSizeAdjust::ExHeight(ref val) => case!(ExHeight, val),
+ FontSizeAdjust::CapHeight(ref val) => case!(CapHeight, val),
+ FontSizeAdjust::ChWidth(ref val) => case!(ChWidth, val),
+ FontSizeAdjust::IcWidth(ref val) => case!(IcWidth, val),
+ FontSizeAdjust::IcHeight(ref val) => case!(IcHeight, val),
+ }
+ }
+}
+
+/// Use FontSettings as computed type of FontFeatureSettings.
+pub type FontFeatureSettings = FontSettings<FeatureTagValue<Integer>>;
+
+/// The computed value for font-variation-settings.
+pub type FontVariationSettings = FontSettings<VariationValue<Number>>;
+
+// The computed value of font-{feature,variation}-settings discards values
+// with duplicate tags, keeping only the last occurrence of each tag.
+fn dedup_font_settings<T>(settings_list: &mut Vec<T>)
+where
+ T: TaggedFontValue,
+{
+ if settings_list.len() > 1 {
+ settings_list.sort_by_key(|k| k.tag().0);
+ // dedup() keeps the first of any duplicates, but we want the last,
+ // so we implement it manually here.
+ let mut prev_tag = settings_list.last().unwrap().tag();
+ for i in (0..settings_list.len() - 1).rev() {
+ let cur_tag = settings_list[i].tag();
+ if cur_tag == prev_tag {
+ settings_list.remove(i);
+ }
+ prev_tag = cur_tag;
+ }
+ }
+}
+
+impl<T> ToComputedValue for FontSettings<T>
+where
+ T: ToComputedValue,
+ <T as ToComputedValue>::ComputedValue: TaggedFontValue,
+{
+ type ComputedValue = FontSettings<T::ComputedValue>;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let mut v = self
+ .0
+ .iter()
+ .map(|item| item.to_computed_value(context))
+ .collect::<Vec<_>>();
+ dedup_font_settings(&mut v);
+ FontSettings(v.into_boxed_slice())
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self(
+ computed
+ .0
+ .iter()
+ .map(T::from_computed_value)
+ .collect::<Vec<_>>()
+ .into_boxed_slice(),
+ )
+ }
+}
+
+/// font-language-override can only have a single 1-4 ASCII character
+/// OpenType "language system" tag, so we should be able to compute
+/// it and store it as a 32-bit integer
+/// (see http://www.microsoft.com/typography/otspec/languagetags.htm).
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[value_info(other_values = "normal")]
+pub struct FontLanguageOverride(pub u32);
+
+impl FontLanguageOverride {
+ #[inline]
+ /// Get computed default value of `font-language-override` with 0
+ pub fn normal() -> FontLanguageOverride {
+ FontLanguageOverride(0)
+ }
+
+ /// Returns this value as a `&str`, backed by `storage`.
+ #[inline]
+ pub(crate) fn to_str(self, storage: &mut [u8; 4]) -> &str {
+ *storage = u32::to_be_bytes(self.0);
+ // Safe because we ensure it's ASCII during parsing
+ let slice = if cfg!(debug_assertions) {
+ std::str::from_utf8(&storage[..]).unwrap()
+ } else {
+ unsafe { std::str::from_utf8_unchecked(&storage[..]) }
+ };
+ slice.trim_end()
+ }
+
+ /// Unsafe because `Self::to_str` requires the value to represent a UTF-8
+ /// string.
+ #[inline]
+ pub unsafe fn from_u32(value: u32) -> Self {
+ Self(value)
+ }
+}
+
+impl ToCss for FontLanguageOverride {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ if self.0 == 0 {
+ return dest.write_str("normal");
+ }
+ self.to_str(&mut [0; 4]).to_css(dest)
+ }
+}
+
+// FIXME(emilio): Make Gecko use the cbindgen'd fontLanguageOverride, then
+// remove this.
+#[cfg(feature = "gecko")]
+impl From<u32> for FontLanguageOverride {
+ fn from(v: u32) -> Self {
+ unsafe { Self::from_u32(v) }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl From<FontLanguageOverride> for u32 {
+ fn from(v: FontLanguageOverride) -> u32 {
+ v.0
+ }
+}
+
+impl ToComputedValue for specified::MozScriptMinSize {
+ type ComputedValue = MozScriptMinSize;
+
+ fn to_computed_value(&self, cx: &Context) -> MozScriptMinSize {
+ // this value is used in the computation of font-size, so
+ // we use the parent size
+ let base_size = FontBaseSize::InheritedStyle;
+ let line_height_base = LineHeightBase::InheritedStyle;
+ match self.0 {
+ NoCalcLength::FontRelative(value) => {
+ value.to_computed_value(cx, base_size, line_height_base)
+ },
+ NoCalcLength::ServoCharacterWidth(value) => {
+ value.to_computed_value(base_size.resolve(cx).computed_size())
+ },
+ ref l => l.to_computed_value(cx),
+ }
+ }
+
+ fn from_computed_value(other: &MozScriptMinSize) -> Self {
+ specified::MozScriptMinSize(ToComputedValue::from_computed_value(other))
+ }
+}
+
+/// The computed value of the math-depth property.
+pub type MathDepth = i8;
+
+#[cfg(feature = "gecko")]
+impl ToComputedValue for specified::MathDepth {
+ type ComputedValue = MathDepth;
+
+ fn to_computed_value(&self, cx: &Context) -> i8 {
+ use crate::properties::longhands::math_style::SpecifiedValue as MathStyleValue;
+ use std::{cmp, i8};
+
+ let int = match *self {
+ specified::MathDepth::AutoAdd => {
+ let parent = cx.builder.get_parent_font().clone_math_depth() as i32;
+ let style = cx.builder.get_parent_font().clone_math_style();
+ if style == MathStyleValue::Compact {
+ parent.saturating_add(1)
+ } else {
+ parent
+ }
+ },
+ specified::MathDepth::Add(rel) => {
+ let parent = cx.builder.get_parent_font().clone_math_depth();
+ (parent as i32).saturating_add(rel.to_computed_value(cx))
+ },
+ specified::MathDepth::Absolute(abs) => abs.to_computed_value(cx),
+ };
+ cmp::min(int, i8::MAX as i32) as i8
+ }
+
+ fn from_computed_value(other: &i8) -> Self {
+ let computed_value = *other as i32;
+ specified::MathDepth::Absolute(SpecifiedInteger::from_computed_value(&computed_value))
+ }
+}
+
+/// - Use a signed 8.8 fixed-point value (representable range -128.0..128)
+///
+/// Values of <angle> below -90 or above 90 not permitted, so we use out of
+/// range values to represent normal | oblique
+pub const FONT_STYLE_FRACTION_BITS: u16 = 8;
+
+/// This is an alias which is useful mostly as a cbindgen / C++ inference
+/// workaround.
+pub type FontStyleFixedPoint = FixedPoint<i16, FONT_STYLE_FRACTION_BITS>;
+
+/// The computed value of `font-style`.
+///
+/// - Define out of range values min value (-128.0) as meaning 'normal'
+/// - Define max value (127.99609375) as 'italic'
+/// - Other values represent 'oblique <angle>'
+/// - Note that 'oblique 0deg' is distinct from 'normal' (should it be?)
+///
+/// cbindgen:derive-lt
+/// cbindgen:derive-lte
+/// cbindgen:derive-gt
+/// cbindgen:derive-gte
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ ToResolvedValue,
+)]
+#[repr(C)]
+pub struct FontStyle(FontStyleFixedPoint);
+
+impl FontStyle {
+ /// The normal keyword.
+ pub const NORMAL: FontStyle = FontStyle(FontStyleFixedPoint {
+ value: 100 << FONT_STYLE_FRACTION_BITS,
+ });
+ /// The italic keyword.
+ pub const ITALIC: FontStyle = FontStyle(FontStyleFixedPoint {
+ value: 101 << FONT_STYLE_FRACTION_BITS,
+ });
+
+ /// The default angle for `font-style: oblique`.
+ /// See also https://github.com/w3c/csswg-drafts/issues/2295
+ pub const DEFAULT_OBLIQUE_DEGREES: i16 = 14;
+
+ /// The `oblique` keyword with the default degrees.
+ pub const OBLIQUE: FontStyle = FontStyle(FontStyleFixedPoint {
+ value: Self::DEFAULT_OBLIQUE_DEGREES << FONT_STYLE_FRACTION_BITS,
+ });
+
+ /// The `normal` value.
+ #[inline]
+ pub fn normal() -> Self {
+ Self::NORMAL
+ }
+
+ /// Returns the oblique angle for this style.
+ pub fn oblique(degrees: f32) -> Self {
+ Self(FixedPoint::from_float(
+ degrees
+ .max(specified::FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES)
+ .min(specified::FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES),
+ ))
+ }
+
+ /// Returns the oblique angle for this style.
+ pub fn oblique_degrees(&self) -> f32 {
+ debug_assert_ne!(*self, Self::NORMAL);
+ debug_assert_ne!(*self, Self::ITALIC);
+ self.0.to_float()
+ }
+}
+
+impl ToCss for FontStyle {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ if *self == Self::NORMAL {
+ return dest.write_str("normal");
+ }
+ if *self == Self::ITALIC {
+ return dest.write_str("italic");
+ }
+ if *self == Self::OBLIQUE {
+ return dest.write_str("oblique");
+ }
+ dest.write_str("oblique ")?;
+ let angle = Angle::from_degrees(self.oblique_degrees());
+ angle.to_css(dest)?;
+ Ok(())
+ }
+}
+
+impl ToAnimatedValue for FontStyle {
+ type AnimatedValue = generics::FontStyle<Angle>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ if self == Self::NORMAL {
+ // This allows us to animate between normal and oblique values. Per spec,
+ // https://drafts.csswg.org/css-fonts-4/#font-style-prop:
+ // Animation type: by computed value type; 'normal' animates as 'oblique 0deg'
+ return generics::FontStyle::Oblique(Angle::from_degrees(0.0));
+ }
+ if self == Self::ITALIC {
+ return generics::FontStyle::Italic;
+ }
+ generics::FontStyle::Oblique(Angle::from_degrees(self.oblique_degrees()))
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ match animated {
+ generics::FontStyle::Normal => Self::NORMAL,
+ generics::FontStyle::Italic => Self::ITALIC,
+ generics::FontStyle::Oblique(ref angle) => {
+ if angle.degrees() == 0.0 {
+ // Reverse the conversion done in to_animated_value()
+ Self::NORMAL
+ } else {
+ Self::oblique(angle.degrees())
+ }
+ },
+ }
+ }
+}
+
+/// font-stretch is a percentage relative to normal.
+///
+/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375)
+///
+/// We arbitrarily limit here to 1000%. (If that becomes a problem, we could
+/// reduce the number of fractional bits and increase the limit.)
+pub const FONT_STRETCH_FRACTION_BITS: u16 = 6;
+
+/// This is an alias which is useful mostly as a cbindgen / C++ inference
+/// workaround.
+pub type FontStretchFixedPoint = FixedPoint<u16, FONT_STRETCH_FRACTION_BITS>;
+
+/// A value for the font-stretch property per:
+///
+/// https://drafts.csswg.org/css-fonts-4/#propdef-font-stretch
+///
+/// cbindgen:derive-lt
+/// cbindgen:derive-lte
+/// cbindgen:derive-gt
+/// cbindgen:derive-gte
+#[derive(
+ Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToResolvedValue,
+)]
+#[repr(C)]
+pub struct FontStretch(pub FontStretchFixedPoint);
+
+impl FontStretch {
+ /// The fraction bits, as an easy-to-access-constant.
+ pub const FRACTION_BITS: u16 = FONT_STRETCH_FRACTION_BITS;
+ /// 0.5 in our floating point representation.
+ pub const HALF: u16 = 1 << (Self::FRACTION_BITS - 1);
+
+ /// The `ultra-condensed` keyword.
+ pub const ULTRA_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 50 << Self::FRACTION_BITS,
+ });
+ /// The `extra-condensed` keyword.
+ pub const EXTRA_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: (62 << Self::FRACTION_BITS) + Self::HALF,
+ });
+ /// The `condensed` keyword.
+ pub const CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 75 << Self::FRACTION_BITS,
+ });
+ /// The `semi-condensed` keyword.
+ pub const SEMI_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: (87 << Self::FRACTION_BITS) + Self::HALF,
+ });
+ /// The `normal` keyword.
+ pub const NORMAL: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 100 << Self::FRACTION_BITS,
+ });
+ /// The `semi-expanded` keyword.
+ pub const SEMI_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: (112 << Self::FRACTION_BITS) + Self::HALF,
+ });
+ /// The `expanded` keyword.
+ pub const EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 125 << Self::FRACTION_BITS,
+ });
+ /// The `extra-expanded` keyword.
+ pub const EXTRA_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 150 << Self::FRACTION_BITS,
+ });
+ /// The `ultra-expanded` keyword.
+ pub const ULTRA_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 200 << Self::FRACTION_BITS,
+ });
+
+ /// 100%
+ pub fn hundred() -> Self {
+ Self::NORMAL
+ }
+
+ /// Converts to a computed percentage.
+ #[inline]
+ pub fn to_percentage(&self) -> Percentage {
+ Percentage(self.0.to_float() / 100.0)
+ }
+
+ /// Converts from a computed percentage value.
+ pub fn from_percentage(p: f32) -> Self {
+ Self(FixedPoint::from_float((p * 100.).max(0.0).min(1000.0)))
+ }
+
+ /// Returns a relevant stretch value from a keyword.
+ /// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop
+ pub fn from_keyword(kw: specified::FontStretchKeyword) -> Self {
+ use specified::FontStretchKeyword::*;
+ match kw {
+ UltraCondensed => Self::ULTRA_CONDENSED,
+ ExtraCondensed => Self::EXTRA_CONDENSED,
+ Condensed => Self::CONDENSED,
+ SemiCondensed => Self::SEMI_CONDENSED,
+ Normal => Self::NORMAL,
+ SemiExpanded => Self::SEMI_EXPANDED,
+ Expanded => Self::EXPANDED,
+ ExtraExpanded => Self::EXTRA_EXPANDED,
+ UltraExpanded => Self::ULTRA_EXPANDED,
+ }
+ }
+
+ /// Returns the stretch keyword if we map to one of the relevant values.
+ pub fn as_keyword(&self) -> Option<specified::FontStretchKeyword> {
+ use specified::FontStretchKeyword::*;
+ // TODO: Can we use match here?
+ if *self == Self::ULTRA_CONDENSED {
+ return Some(UltraCondensed);
+ }
+ if *self == Self::EXTRA_CONDENSED {
+ return Some(ExtraCondensed);
+ }
+ if *self == Self::CONDENSED {
+ return Some(Condensed);
+ }
+ if *self == Self::SEMI_CONDENSED {
+ return Some(SemiCondensed);
+ }
+ if *self == Self::NORMAL {
+ return Some(Normal);
+ }
+ if *self == Self::SEMI_EXPANDED {
+ return Some(SemiExpanded);
+ }
+ if *self == Self::EXPANDED {
+ return Some(Expanded);
+ }
+ if *self == Self::EXTRA_EXPANDED {
+ return Some(ExtraExpanded);
+ }
+ if *self == Self::ULTRA_EXPANDED {
+ return Some(UltraExpanded);
+ }
+ None
+ }
+}
+
+impl ToCss for FontStretch {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.to_percentage().to_css(dest)
+ }
+}
+
+impl ToAnimatedValue for FontStretch {
+ type AnimatedValue = Percentage;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.to_percentage()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Self::from_percentage(animated.0)
+ }
+}
+
+/// A computed value for the `line-height` property.
+pub type LineHeight = generics::GenericLineHeight<NonNegativeNumber, NonNegativeLength>;
+
+impl ToResolvedValue for LineHeight {
+ type ResolvedValue = Self;
+
+ fn to_resolved_value(self, context: &ResolvedContext) -> Self::ResolvedValue {
+ // Resolve <number> to an absolute <length> based on font size.
+ if matches!(self, Self::Normal | Self::MozBlockHeight) {
+ return self;
+ }
+ let wm = context.style.writing_mode;
+ Self::Length(context.device.calc_line_height(
+ context.style.get_font(),
+ wm,
+ Some(context.element_info.element),
+ ))
+ }
+
+ #[inline]
+ fn from_resolved_value(value: Self::ResolvedValue) -> Self {
+ value
+ }
+}
diff --git a/servo/components/style/values/computed/image.rs b/servo/components/style/values/computed/image.rs
new file mode 100644
index 0000000000..8a91d95313
--- /dev/null
+++ b/servo/components/style/values/computed/image.rs
@@ -0,0 +1,205 @@
+/* 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/. */
+
+//! CSS handling for the computed value of
+//! [`image`][image]s
+//!
+//! [image]: https://drafts.csswg.org/css-images/#image-values
+
+use crate::values::computed::percentage::Percentage;
+use crate::values::computed::position::Position;
+use crate::values::computed::url::ComputedImageUrl;
+use crate::values::computed::{Angle, Color, Context};
+use crate::values::computed::{
+ AngleOrPercentage, LengthPercentage, NonNegativeLength, NonNegativeLengthPercentage,
+ Resolution, ToComputedValue,
+};
+use crate::values::generics::image::{self as generic, GradientCompatMode};
+use crate::values::specified::image as specified;
+use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
+use std::f32::consts::PI;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+pub use specified::ImageRendering;
+
+/// Computed values for an image according to CSS-IMAGES.
+/// <https://drafts.csswg.org/css-images/#image-values>
+pub type Image = generic::GenericImage<Gradient, ComputedImageUrl, Color, Percentage, Resolution>;
+
+// Images should remain small, see https://github.com/servo/servo/pull/18430
+size_of_test!(Image, 16);
+
+/// Computed values for a CSS gradient.
+/// <https://drafts.csswg.org/css-images/#gradients>
+pub type Gradient = generic::GenericGradient<
+ LineDirection,
+ LengthPercentage,
+ NonNegativeLength,
+ NonNegativeLengthPercentage,
+ Position,
+ Angle,
+ AngleOrPercentage,
+ Color,
+>;
+
+/// Computed values for CSS cross-fade
+/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
+pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;
+
+/// A computed radial gradient ending shape.
+pub type EndingShape = generic::GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage>;
+
+/// A computed gradient line direction.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToResolvedValue)]
+#[repr(C, u8)]
+pub enum LineDirection {
+ /// An angle.
+ Angle(Angle),
+ /// A horizontal direction.
+ Horizontal(HorizontalPositionKeyword),
+ /// A vertical direction.
+ Vertical(VerticalPositionKeyword),
+ /// A corner.
+ Corner(HorizontalPositionKeyword, VerticalPositionKeyword),
+}
+
+/// The computed value for an `image-set()` image.
+pub type ImageSet = generic::GenericImageSet<Image, Resolution>;
+
+impl ToComputedValue for specified::ImageSet {
+ type ComputedValue = ImageSet;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let items = self.items.to_computed_value(context);
+ let dpr = context.device().device_pixel_ratio().get();
+
+ let mut supported_image = false;
+ let mut selected_index = std::usize::MAX;
+ let mut selected_resolution = 0.0;
+
+ for (i, item) in items.iter().enumerate() {
+ if item.has_mime_type && !context.device().is_supported_mime_type(&item.mime_type) {
+ // If the MIME type is not supported, we discard the ImageSetItem.
+ continue;
+ }
+
+ let candidate_resolution = item.resolution.dppx();
+ debug_assert!(
+ candidate_resolution >= 0.0,
+ "Resolutions should be non-negative"
+ );
+ if candidate_resolution == 0.0 {
+ // If the resolution is 0, we also treat it as an invalid image.
+ continue;
+ }
+
+ // https://drafts.csswg.org/css-images-4/#image-set-notation:
+ //
+ // Make a UA-specific choice of which to load, based on whatever criteria deemed
+ // relevant (such as the resolution of the display, connection speed, etc).
+ //
+ // For now, select the lowest resolution greater than display density, otherwise the
+ // greatest resolution available.
+ let better_candidate = || {
+ if selected_resolution < dpr && candidate_resolution > selected_resolution {
+ return true;
+ }
+ if candidate_resolution < selected_resolution && candidate_resolution >= dpr {
+ return true;
+ }
+ false
+ };
+
+ // The first item with a supported MIME type is obviously the current best candidate
+ if !supported_image || better_candidate() {
+ supported_image = true;
+ selected_index = i;
+ selected_resolution = candidate_resolution;
+ }
+ }
+
+ ImageSet {
+ selected_index,
+ items,
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self {
+ selected_index: std::usize::MAX,
+ items: ToComputedValue::from_computed_value(&computed.items),
+ }
+ }
+}
+
+impl generic::LineDirection for LineDirection {
+ fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool {
+ match *self {
+ LineDirection::Angle(angle) => angle.radians() == PI,
+ LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
+ compat_mode == GradientCompatMode::Modern
+ },
+ LineDirection::Vertical(VerticalPositionKeyword::Top) => {
+ compat_mode != GradientCompatMode::Modern
+ },
+ _ => false,
+ }
+ }
+
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ LineDirection::Angle(ref angle) => angle.to_css(dest),
+ LineDirection::Horizontal(x) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ x.to_css(dest)
+ },
+ LineDirection::Vertical(y) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ y.to_css(dest)
+ },
+ LineDirection::Corner(x, y) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ x.to_css(dest)?;
+ dest.write_char(' ')?;
+ y.to_css(dest)
+ },
+ }
+ }
+}
+
+impl ToComputedValue for specified::LineDirection {
+ type ComputedValue = LineDirection;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ specified::LineDirection::Angle(ref angle) => {
+ LineDirection::Angle(angle.to_computed_value(context))
+ },
+ specified::LineDirection::Horizontal(x) => LineDirection::Horizontal(x),
+ specified::LineDirection::Vertical(y) => LineDirection::Vertical(y),
+ specified::LineDirection::Corner(x, y) => LineDirection::Corner(x, y),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match *computed {
+ LineDirection::Angle(ref angle) => {
+ specified::LineDirection::Angle(ToComputedValue::from_computed_value(angle))
+ },
+ LineDirection::Horizontal(x) => specified::LineDirection::Horizontal(x),
+ LineDirection::Vertical(y) => specified::LineDirection::Vertical(y),
+ LineDirection::Corner(x, y) => specified::LineDirection::Corner(x, y),
+ }
+ }
+}
diff --git a/servo/components/style/values/computed/length.rs b/servo/components/style/values/computed/length.rs
new file mode 100644
index 0000000000..e75676a76d
--- /dev/null
+++ b/servo/components/style/values/computed/length.rs
@@ -0,0 +1,531 @@
+/* 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/. */
+
+//! `<length>` computed values, and related ones.
+
+use super::{Context, Number, ToComputedValue};
+use crate::values::animated::ToAnimatedValue;
+use crate::values::computed::NonNegativeNumber;
+use crate::values::generics::length as generics;
+use crate::values::generics::length::{
+ GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize,
+};
+use crate::values::generics::NonNegative;
+use crate::values::specified::length::{AbsoluteLength, FontBaseSize, LineHeightBase};
+use crate::values::{specified, CSSFloat};
+use crate::Zero;
+use app_units::Au;
+use std::fmt::{self, Write};
+use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub};
+use style_traits::{CSSPixel, CssWriter, ToCss};
+
+pub use super::image::Image;
+pub use super::length_percentage::{LengthPercentage, NonNegativeLengthPercentage};
+pub use crate::values::specified::url::UrlOrNone;
+pub use crate::values::specified::{Angle, BorderStyle, Time};
+
+impl ToComputedValue for specified::NoCalcLength {
+ type ComputedValue = Length;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.to_computed_value_with_base_size(
+ context,
+ FontBaseSize::CurrentStyle,
+ LineHeightBase::CurrentStyle,
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::Absolute(AbsoluteLength::Px(computed.px()))
+ }
+}
+
+impl specified::NoCalcLength {
+ /// Computes a length with a given font-relative base size.
+ pub fn to_computed_value_with_base_size(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> Length {
+ match *self {
+ Self::Absolute(length) => length.to_computed_value(context),
+ Self::FontRelative(length) => {
+ length.to_computed_value(context, base_size, line_height_base)
+ },
+ Self::ViewportPercentage(length) => length.to_computed_value(context),
+ Self::ContainerRelative(length) => length.to_computed_value(context),
+ Self::ServoCharacterWidth(length) => length
+ .to_computed_value(context.style().get_font().clone_font_size().computed_size()),
+ }
+ }
+}
+
+impl ToComputedValue for specified::Length {
+ type ComputedValue = Length;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ Self::NoCalc(l) => l.to_computed_value(context),
+ Self::Calc(ref calc) => {
+ let result = calc.to_computed_value(context);
+ debug_assert!(
+ result.to_length().is_some(),
+ "{:?} didn't resolve to a length: {:?}",
+ calc,
+ result,
+ );
+ result.to_length().unwrap_or_else(Length::zero)
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::NoCalc(specified::NoCalcLength::from_computed_value(computed))
+ }
+}
+
+/// Some boilerplate to share between negative and non-negative
+/// length-percentage or auto.
+macro_rules! computed_length_percentage_or_auto {
+ ($inner:ty) => {
+ /// Returns the used value.
+ #[inline]
+ pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> {
+ match *self {
+ Self::Auto => None,
+ Self::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)),
+ }
+ }
+
+ /// Returns true if the computed value is absolute 0 or 0%.
+ #[inline]
+ pub fn is_definitely_zero(&self) -> bool {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match *self {
+ LengthPercentage(ref l) => l.is_definitely_zero(),
+ Auto => false,
+ }
+ }
+ };
+}
+
+/// A computed type for `<length-percentage> | auto`.
+pub type LengthPercentageOrAuto = generics::GenericLengthPercentageOrAuto<LengthPercentage>;
+
+impl LengthPercentageOrAuto {
+ /// Clamps the value to a non-negative value.
+ pub fn clamp_to_non_negative(self) -> Self {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match self {
+ LengthPercentage(l) => LengthPercentage(l.clamp_to_non_negative()),
+ Auto => Auto,
+ }
+ }
+
+ /// Convert to have a borrow inside the enum
+ pub fn as_ref(&self) -> generics::GenericLengthPercentageOrAuto<&LengthPercentage> {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match *self {
+ LengthPercentage(ref lp) => LengthPercentage(lp),
+ Auto => Auto,
+ }
+ }
+
+ computed_length_percentage_or_auto!(LengthPercentage);
+}
+
+impl generics::GenericLengthPercentageOrAuto<&LengthPercentage> {
+ /// Resolves the percentage.
+ #[inline]
+ pub fn percentage_relative_to(&self, basis: Length) -> LengthOrAuto {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match self {
+ LengthPercentage(length_percentage) => {
+ LengthPercentage(length_percentage.percentage_relative_to(basis))
+ },
+ Auto => Auto,
+ }
+ }
+
+ /// Maybe resolves the percentage.
+ #[inline]
+ pub fn maybe_percentage_relative_to(&self, basis: Option<Length>) -> LengthOrAuto {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match self {
+ LengthPercentage(length_percentage) => length_percentage
+ .maybe_percentage_relative_to(basis)
+ .map_or(Auto, LengthPercentage),
+ Auto => Auto,
+ }
+ }
+}
+
+/// A wrapper of LengthPercentageOrAuto, whose value must be >= 0.
+pub type NonNegativeLengthPercentageOrAuto =
+ generics::GenericLengthPercentageOrAuto<NonNegativeLengthPercentage>;
+
+impl NonNegativeLengthPercentageOrAuto {
+ computed_length_percentage_or_auto!(NonNegativeLengthPercentage);
+}
+
+#[cfg(feature = "servo")]
+impl MaxSize {
+ /// Convert the computed value into used value.
+ #[inline]
+ pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> {
+ match *self {
+ GenericMaxSize::None => None,
+ GenericMaxSize::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)),
+ }
+ }
+}
+
+impl Size {
+ /// Convert the computed value into used value.
+ #[inline]
+ #[cfg(feature = "servo")]
+ pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> {
+ match *self {
+ GenericSize::Auto => None,
+ GenericSize::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)),
+ }
+ }
+
+ /// Returns true if the computed value is absolute 0 or 0%.
+ #[inline]
+ pub fn is_definitely_zero(&self) -> bool {
+ match *self {
+ Self::Auto => false,
+ Self::LengthPercentage(ref lp) => lp.is_definitely_zero(),
+ #[cfg(feature = "gecko")]
+ Self::MinContent |
+ Self::MaxContent |
+ Self::FitContent |
+ Self::MozAvailable |
+ Self::FitContentFunction(_) => false,
+ }
+ }
+}
+
+/// The computed `<length>` value.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ Serialize,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct CSSPixelLength(CSSFloat);
+
+impl fmt::Debug for CSSPixelLength {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.0.fmt(f)?;
+ f.write_str(" px")
+ }
+}
+
+impl CSSPixelLength {
+ /// Return a new CSSPixelLength.
+ #[inline]
+ pub fn new(px: CSSFloat) -> Self {
+ CSSPixelLength(px)
+ }
+
+ /// Returns a normalized (NaN turned to zero) version of this length.
+ #[inline]
+ pub fn normalized(self) -> Self {
+ Self::new(crate::values::normalize(self.0))
+ }
+
+ /// Returns a finite (normalized and clamped to float min and max) version of this length.
+ #[inline]
+ pub fn finite(self) -> Self {
+ Self::new(crate::values::normalize(self.0).min(f32::MAX).max(f32::MIN))
+ }
+
+ /// Scale the length by a given amount.
+ #[inline]
+ pub fn scale_by(self, scale: CSSFloat) -> Self {
+ CSSPixelLength(self.0 * scale)
+ }
+
+ /// Return the containing pixel value.
+ #[inline]
+ pub fn px(self) -> CSSFloat {
+ self.0
+ }
+
+ /// Return the length with app_unit i32 type.
+ #[inline]
+ pub fn to_i32_au(self) -> i32 {
+ Au::from(self).0
+ }
+
+ /// Return the absolute value of this length.
+ #[inline]
+ pub fn abs(self) -> Self {
+ CSSPixelLength::new(self.0.abs())
+ }
+
+ /// Return the clamped value of this length.
+ #[inline]
+ pub fn clamp_to_non_negative(self) -> Self {
+ CSSPixelLength::new(self.0.max(0.))
+ }
+
+ /// Returns the minimum between `self` and `other`.
+ #[inline]
+ pub fn min(self, other: Self) -> Self {
+ CSSPixelLength::new(self.0.min(other.0))
+ }
+
+ /// Returns the maximum between `self` and `other`.
+ #[inline]
+ pub fn max(self, other: Self) -> Self {
+ CSSPixelLength::new(self.0.max(other.0))
+ }
+
+ /// Sets `self` to the maximum between `self` and `other`.
+ #[inline]
+ pub fn max_assign(&mut self, other: Self) {
+ *self = self.max(other);
+ }
+
+ /// Clamp the value to a lower bound and an optional upper bound.
+ ///
+ /// Can be used for example with `min-width` and `max-width`.
+ #[inline]
+ pub fn clamp_between_extremums(self, min_size: Self, max_size: Option<Self>) -> Self {
+ self.clamp_below_max(max_size).max(min_size)
+ }
+
+ /// Clamp the value to an optional upper bound.
+ ///
+ /// Can be used for example with `max-width`.
+ #[inline]
+ pub fn clamp_below_max(self, max_size: Option<Self>) -> Self {
+ match max_size {
+ None => self,
+ Some(max_size) => self.min(max_size),
+ }
+ }
+}
+
+impl num_traits::Zero for CSSPixelLength {
+ fn zero() -> Self {
+ CSSPixelLength::new(0.)
+ }
+
+ fn is_zero(&self) -> bool {
+ self.px() == 0.
+ }
+}
+
+impl ToCss for CSSPixelLength {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest)?;
+ dest.write_str("px")
+ }
+}
+
+impl std::iter::Sum for CSSPixelLength {
+ fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+ iter.fold(Length::zero(), Add::add)
+ }
+}
+
+impl Add for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn add(self, other: Self) -> Self {
+ Self::new(self.px() + other.px())
+ }
+}
+
+impl AddAssign for CSSPixelLength {
+ #[inline]
+ fn add_assign(&mut self, other: Self) {
+ self.0 += other.0;
+ }
+}
+
+impl Div for CSSPixelLength {
+ type Output = CSSFloat;
+
+ #[inline]
+ fn div(self, other: Self) -> CSSFloat {
+ self.px() / other.px()
+ }
+}
+
+impl Div<CSSFloat> for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn div(self, other: CSSFloat) -> Self {
+ Self::new(self.px() / other)
+ }
+}
+
+impl MulAssign<CSSFloat> for CSSPixelLength {
+ #[inline]
+ fn mul_assign(&mut self, other: CSSFloat) {
+ self.0 *= other;
+ }
+}
+
+impl Mul<CSSFloat> for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn mul(self, other: CSSFloat) -> Self {
+ Self::new(self.px() * other)
+ }
+}
+
+impl Neg for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn neg(self) -> Self {
+ CSSPixelLength::new(-self.0)
+ }
+}
+
+impl Sub for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn sub(self, other: Self) -> Self {
+ Self::new(self.px() - other.px())
+ }
+}
+
+impl From<CSSPixelLength> for Au {
+ #[inline]
+ fn from(len: CSSPixelLength) -> Self {
+ Au::from_f32_px(len.0)
+ }
+}
+
+impl From<Au> for CSSPixelLength {
+ #[inline]
+ fn from(len: Au) -> Self {
+ CSSPixelLength::new(len.to_f32_px())
+ }
+}
+
+impl From<CSSPixelLength> for euclid::Length<CSSFloat, CSSPixel> {
+ #[inline]
+ fn from(length: CSSPixelLength) -> Self {
+ Self::new(length.0)
+ }
+}
+
+/// An alias of computed `<length>` value.
+pub type Length = CSSPixelLength;
+
+/// Either a computed `<length>` or the `auto` keyword.
+pub type LengthOrAuto = generics::GenericLengthPercentageOrAuto<Length>;
+
+/// Either a non-negative `<length>` or the `auto` keyword.
+pub type NonNegativeLengthOrAuto = generics::GenericLengthPercentageOrAuto<NonNegativeLength>;
+
+/// Either a computed `<length>` or a `<number>` value.
+pub type LengthOrNumber = GenericLengthOrNumber<Length, Number>;
+
+/// A wrapper of Length, whose value must be >= 0.
+pub type NonNegativeLength = NonNegative<Length>;
+
+impl ToAnimatedValue for NonNegativeLength {
+ type AnimatedValue = Length;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ NonNegativeLength::new(animated.px().max(0.))
+ }
+}
+
+impl NonNegativeLength {
+ /// Create a NonNegativeLength.
+ #[inline]
+ pub fn new(px: CSSFloat) -> Self {
+ NonNegative(Length::new(px.max(0.)))
+ }
+
+ /// Return the pixel value of |NonNegativeLength|.
+ #[inline]
+ pub fn px(&self) -> CSSFloat {
+ self.0.px()
+ }
+
+ #[inline]
+ /// Ensures it is non negative
+ pub fn clamp(self) -> Self {
+ if (self.0).0 < 0. {
+ Self::zero()
+ } else {
+ self
+ }
+ }
+}
+
+impl From<Length> for NonNegativeLength {
+ #[inline]
+ fn from(len: Length) -> Self {
+ NonNegative(len)
+ }
+}
+
+impl From<Au> for NonNegativeLength {
+ #[inline]
+ fn from(au: Au) -> Self {
+ NonNegative(au.into())
+ }
+}
+
+impl From<NonNegativeLength> for Au {
+ #[inline]
+ fn from(non_negative_len: NonNegativeLength) -> Self {
+ Au::from(non_negative_len.0)
+ }
+}
+
+/// Either a computed NonNegativeLengthPercentage or the `normal` keyword.
+pub type NonNegativeLengthPercentageOrNormal =
+ GenericLengthPercentageOrNormal<NonNegativeLengthPercentage>;
+
+/// Either a non-negative `<length>` or a `<number>`.
+pub type NonNegativeLengthOrNumber = GenericLengthOrNumber<NonNegativeLength, NonNegativeNumber>;
+
+/// A computed value for `min-width`, `min-height`, `width` or `height` property.
+pub type Size = GenericSize<NonNegativeLengthPercentage>;
+
+/// A computed value for `max-width` or `min-height` property.
+pub type MaxSize = GenericMaxSize<NonNegativeLengthPercentage>;
diff --git a/servo/components/style/values/computed/length_percentage.rs b/servo/components/style/values/computed/length_percentage.rs
new file mode 100644
index 0000000000..898281a7ef
--- /dev/null
+++ b/servo/components/style/values/computed/length_percentage.rs
@@ -0,0 +1,1055 @@
+/* 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/. */
+
+//! `<length-percentage>` computed values, and related ones.
+//!
+//! The over-all design is a tagged pointer, with the lower bits of the pointer
+//! being non-zero if it is a non-calc value.
+//!
+//! It is expected to take 64 bits both in x86 and x86-64. This is implemented
+//! as a `union`, with 4 different variants:
+//!
+//! * The length and percentage variants have a { tag, f32 } (effectively)
+//! layout. The tag has to overlap with the lower 2 bits of the calc variant.
+//!
+//! * The `calc()` variant is a { tag, pointer } in x86 (so same as the
+//! others), or just a { pointer } in x86-64 (so that the two bits of the tag
+//! can be obtained from the lower bits of the pointer).
+//!
+//! * There's a `tag` variant just to make clear when only the tag is intended
+//! to be read. Note that the tag needs to be masked always by `TAG_MASK`, to
+//! deal with the pointer variant in x86-64.
+//!
+//! The assertions in the constructor methods ensure that the tag getter matches
+//! our expectations.
+
+use super::{Context, Length, Percentage, ToComputedValue};
+use crate::gecko_bindings::structs::GeckoFontMetrics;
+use crate::values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::calc::{CalcUnits, PositivePercentageBasis};
+use crate::values::generics::{calc, NonNegative};
+use crate::values::specified::length::{FontBaseSize, LineHeightBase};
+use crate::values::{specified, CSSFloat};
+use crate::{Zero, ZeroNoPercent};
+use app_units::Au;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ToCss};
+
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+pub struct LengthVariant {
+ tag: u8,
+ length: Length,
+}
+
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+pub struct PercentageVariant {
+ tag: u8,
+ percentage: Percentage,
+}
+
+// NOTE(emilio): cbindgen only understands the #[cfg] on the top level
+// definition.
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+#[cfg(target_pointer_width = "32")]
+pub struct CalcVariant {
+ tag: u8,
+ ptr: *mut CalcLengthPercentage,
+}
+
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+#[cfg(target_pointer_width = "64")]
+pub struct CalcVariant {
+ ptr: usize, // In little-endian byte order
+}
+
+// `CalcLengthPercentage` is `Send + Sync` as asserted below.
+unsafe impl Send for CalcVariant {}
+unsafe impl Sync for CalcVariant {}
+
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+pub struct TagVariant {
+ tag: u8,
+}
+
+/// A `<length-percentage>` value. This can be either a `<length>`, a
+/// `<percentage>`, or a combination of both via `calc()`.
+///
+/// cbindgen:private-default-tagged-enum-constructor=false
+/// cbindgen:derive-mut-casts=true
+///
+/// https://drafts.csswg.org/css-values-4/#typedef-length-percentage
+///
+/// The tag is stored in the lower two bits.
+///
+/// We need to use a struct instead of the union directly because unions with
+/// Drop implementations are unstable, looks like.
+///
+/// Also we need the union and the variants to be `pub` (even though the member
+/// is private) so that cbindgen generates it. They're not part of the public
+/// API otherwise.
+#[repr(transparent)]
+pub struct LengthPercentage(LengthPercentageUnion);
+
+#[doc(hidden)]
+#[repr(C)]
+pub union LengthPercentageUnion {
+ length: LengthVariant,
+ percentage: PercentageVariant,
+ calc: CalcVariant,
+ tag: TagVariant,
+}
+
+impl LengthPercentageUnion {
+ #[doc(hidden)] // Need to be public so that cbindgen generates it.
+ pub const TAG_CALC: u8 = 0;
+ #[doc(hidden)]
+ pub const TAG_LENGTH: u8 = 1;
+ #[doc(hidden)]
+ pub const TAG_PERCENTAGE: u8 = 2;
+ #[doc(hidden)]
+ pub const TAG_MASK: u8 = 0b11;
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[repr(u8)]
+enum Tag {
+ Calc = LengthPercentageUnion::TAG_CALC,
+ Length = LengthPercentageUnion::TAG_LENGTH,
+ Percentage = LengthPercentageUnion::TAG_PERCENTAGE,
+}
+
+// All the members should be 64 bits, even in 32-bit builds.
+#[allow(unused)]
+unsafe fn static_assert() {
+ fn assert_send_and_sync<T: Send + Sync>() {}
+ std::mem::transmute::<u64, LengthVariant>(0u64);
+ std::mem::transmute::<u64, PercentageVariant>(0u64);
+ std::mem::transmute::<u64, CalcVariant>(0u64);
+ std::mem::transmute::<u64, LengthPercentage>(0u64);
+ assert_send_and_sync::<LengthVariant>();
+ assert_send_and_sync::<PercentageVariant>();
+ assert_send_and_sync::<CalcLengthPercentage>();
+}
+
+impl Drop for LengthPercentage {
+ fn drop(&mut self) {
+ if self.tag() == Tag::Calc {
+ let _ = unsafe { Box::from_raw(self.calc_ptr()) };
+ }
+ }
+}
+
+impl MallocSizeOf for LengthPercentage {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match self.unpack() {
+ Unpacked::Length(..) | Unpacked::Percentage(..) => 0,
+ Unpacked::Calc(c) => unsafe { ops.malloc_size_of(c) },
+ }
+ }
+}
+
+/// An unpacked `<length-percentage>` that borrows the `calc()` variant.
+#[derive(Clone, Debug, PartialEq, ToCss)]
+enum Unpacked<'a> {
+ Calc(&'a CalcLengthPercentage),
+ Length(Length),
+ Percentage(Percentage),
+}
+
+/// An unpacked `<length-percentage>` that mutably borrows the `calc()` variant.
+enum UnpackedMut<'a> {
+ Calc(&'a mut CalcLengthPercentage),
+ Length(Length),
+ Percentage(Percentage),
+}
+
+/// An unpacked `<length-percentage>` that owns the `calc()` variant, for
+/// serialization purposes.
+#[derive(Deserialize, PartialEq, Serialize)]
+enum Serializable {
+ Calc(CalcLengthPercentage),
+ Length(Length),
+ Percentage(Percentage),
+}
+
+impl LengthPercentage {
+ /// 1px length value for SVG defaults
+ #[inline]
+ pub fn one() -> Self {
+ Self::new_length(Length::new(1.))
+ }
+
+ /// 0%
+ #[inline]
+ pub fn zero_percent() -> Self {
+ Self::new_percent(Percentage::zero())
+ }
+
+ fn to_calc_node(&self) -> Cow<CalcNode> {
+ match self.unpack() {
+ Unpacked::Length(l) => Cow::Owned(CalcNode::Leaf(CalcLengthPercentageLeaf::Length(l))),
+ Unpacked::Percentage(p) => {
+ Cow::Owned(CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(p)))
+ },
+ Unpacked::Calc(p) => Cow::Borrowed(&p.node),
+ }
+ }
+
+ /// Constructs a length value.
+ #[inline]
+ pub fn new_length(length: Length) -> Self {
+ let length = Self(LengthPercentageUnion {
+ length: LengthVariant {
+ tag: LengthPercentageUnion::TAG_LENGTH,
+ length,
+ },
+ });
+ debug_assert_eq!(length.tag(), Tag::Length);
+ length
+ }
+
+ /// Constructs a percentage value.
+ #[inline]
+ pub fn new_percent(percentage: Percentage) -> Self {
+ let percent = Self(LengthPercentageUnion {
+ percentage: PercentageVariant {
+ tag: LengthPercentageUnion::TAG_PERCENTAGE,
+ percentage,
+ },
+ });
+ debug_assert_eq!(percent.tag(), Tag::Percentage);
+ percent
+ }
+
+ /// Given a `LengthPercentage` value `v`, construct the value representing
+ /// `calc(100% - v)`.
+ pub fn hundred_percent_minus(v: Self, clamping_mode: AllowedNumericType) -> Self {
+ // TODO: This could in theory take ownership of the calc node in `v` if
+ // possible instead of cloning.
+ let mut node = v.to_calc_node().into_owned();
+ node.negate();
+
+ let new_node = CalcNode::Sum(
+ vec![
+ CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(Percentage::hundred())),
+ node,
+ ]
+ .into(),
+ );
+
+ Self::new_calc(new_node, clamping_mode)
+ }
+
+ /// Given a list of `LengthPercentage` values, construct the value representing
+ /// `calc(100% - the sum of the list)`.
+ pub fn hundred_percent_minus_list(list: &[&Self], clamping_mode: AllowedNumericType) -> Self {
+ let mut new_list = vec![CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(
+ Percentage::hundred(),
+ ))];
+
+ for lp in list.iter() {
+ let mut node = lp.to_calc_node().into_owned();
+ node.negate();
+ new_list.push(node)
+ }
+
+ Self::new_calc(CalcNode::Sum(new_list.into()), clamping_mode)
+ }
+
+ /// Constructs a `calc()` value.
+ #[inline]
+ pub fn new_calc(mut node: CalcNode, clamping_mode: AllowedNumericType) -> Self {
+ node.simplify_and_sort();
+
+ match node {
+ CalcNode::Leaf(l) => {
+ return match l {
+ CalcLengthPercentageLeaf::Length(l) => {
+ Self::new_length(Length::new(clamping_mode.clamp(l.px())).normalized())
+ },
+ CalcLengthPercentageLeaf::Percentage(p) => Self::new_percent(Percentage(
+ clamping_mode.clamp(crate::values::normalize(p.0)),
+ )),
+ CalcLengthPercentageLeaf::Number(number) => {
+ debug_assert!(
+ false,
+ "The final result of a <length-percentage> should never be a number"
+ );
+ Self::new_length(Length::new(number))
+ },
+ };
+ },
+ _ => Self::new_calc_unchecked(Box::new(CalcLengthPercentage {
+ clamping_mode,
+ node,
+ })),
+ }
+ }
+
+ /// Private version of new_calc() that constructs a calc() variant without
+ /// checking.
+ fn new_calc_unchecked(calc: Box<CalcLengthPercentage>) -> Self {
+ let ptr = Box::into_raw(calc);
+
+ #[cfg(target_pointer_width = "32")]
+ let calc = CalcVariant {
+ tag: LengthPercentageUnion::TAG_CALC,
+ ptr,
+ };
+
+ #[cfg(target_pointer_width = "64")]
+ let calc = CalcVariant {
+ #[cfg(target_endian = "little")]
+ ptr: ptr as usize,
+ #[cfg(target_endian = "big")]
+ ptr: (ptr as usize).swap_bytes(),
+ };
+
+ let calc = Self(LengthPercentageUnion { calc });
+ debug_assert_eq!(calc.tag(), Tag::Calc);
+ calc
+ }
+
+ #[inline]
+ fn tag(&self) -> Tag {
+ match unsafe { self.0.tag.tag & LengthPercentageUnion::TAG_MASK } {
+ LengthPercentageUnion::TAG_CALC => Tag::Calc,
+ LengthPercentageUnion::TAG_LENGTH => Tag::Length,
+ LengthPercentageUnion::TAG_PERCENTAGE => Tag::Percentage,
+ _ => unsafe { debug_unreachable!("Bogus tag?") },
+ }
+ }
+
+ #[inline]
+ fn unpack_mut<'a>(&'a mut self) -> UnpackedMut<'a> {
+ unsafe {
+ match self.tag() {
+ Tag::Calc => UnpackedMut::Calc(&mut *self.calc_ptr()),
+ Tag::Length => UnpackedMut::Length(self.0.length.length),
+ Tag::Percentage => UnpackedMut::Percentage(self.0.percentage.percentage),
+ }
+ }
+ }
+
+ #[inline]
+ fn unpack<'a>(&'a self) -> Unpacked<'a> {
+ unsafe {
+ match self.tag() {
+ Tag::Calc => Unpacked::Calc(&*self.calc_ptr()),
+ Tag::Length => Unpacked::Length(self.0.length.length),
+ Tag::Percentage => Unpacked::Percentage(self.0.percentage.percentage),
+ }
+ }
+ }
+
+ #[inline]
+ unsafe fn calc_ptr(&self) -> *mut CalcLengthPercentage {
+ #[cfg(not(all(target_endian = "big", target_pointer_width = "64")))]
+ {
+ self.0.calc.ptr as *mut _
+ }
+ #[cfg(all(target_endian = "big", target_pointer_width = "64"))]
+ {
+ self.0.calc.ptr.swap_bytes() as *mut _
+ }
+ }
+
+ #[inline]
+ fn to_serializable(&self) -> Serializable {
+ match self.unpack() {
+ Unpacked::Calc(c) => Serializable::Calc(c.clone()),
+ Unpacked::Length(l) => Serializable::Length(l),
+ Unpacked::Percentage(p) => Serializable::Percentage(p),
+ }
+ }
+
+ #[inline]
+ fn from_serializable(s: Serializable) -> Self {
+ match s {
+ Serializable::Calc(c) => Self::new_calc_unchecked(Box::new(c)),
+ Serializable::Length(l) => Self::new_length(l),
+ Serializable::Percentage(p) => Self::new_percent(p),
+ }
+ }
+
+ /// Returns true if the computed value is absolute 0 or 0%.
+ #[inline]
+ pub fn is_definitely_zero(&self) -> bool {
+ match self.unpack() {
+ Unpacked::Length(l) => l.px() == 0.0,
+ Unpacked::Percentage(p) => p.0 == 0.0,
+ Unpacked::Calc(..) => false,
+ }
+ }
+
+ /// Resolves the percentage.
+ #[inline]
+ pub fn resolve(&self, basis: Length) -> Length {
+ match self.unpack() {
+ Unpacked::Length(l) => l,
+ Unpacked::Percentage(p) => (basis * p.0).normalized(),
+ Unpacked::Calc(ref c) => c.resolve(basis),
+ }
+ }
+
+ /// Resolves the percentage. Just an alias of resolve().
+ #[inline]
+ pub fn percentage_relative_to(&self, basis: Length) -> Length {
+ self.resolve(basis)
+ }
+
+ /// Return whether there's any percentage in this value.
+ #[inline]
+ pub fn has_percentage(&self) -> bool {
+ match self.unpack() {
+ Unpacked::Length(..) => false,
+ Unpacked::Percentage(..) | Unpacked::Calc(..) => true,
+ }
+ }
+
+ /// Converts to a `<length>` if possible.
+ pub fn to_length(&self) -> Option<Length> {
+ match self.unpack() {
+ Unpacked::Length(l) => Some(l),
+ Unpacked::Percentage(..) | Unpacked::Calc(..) => {
+ debug_assert!(self.has_percentage());
+ return None;
+ },
+ }
+ }
+
+ /// Converts to a `<percentage>` if possible.
+ #[inline]
+ pub fn to_percentage(&self) -> Option<Percentage> {
+ match self.unpack() {
+ Unpacked::Percentage(p) => Some(p),
+ Unpacked::Length(..) | Unpacked::Calc(..) => None,
+ }
+ }
+
+ /// Returns the used value.
+ #[inline]
+ pub fn to_used_value(&self, containing_length: Au) -> Au {
+ Au::from(self.to_pixel_length(containing_length))
+ }
+
+ /// Returns the used value as CSSPixelLength.
+ #[inline]
+ pub fn to_pixel_length(&self, containing_length: Au) -> Length {
+ self.resolve(containing_length.into())
+ }
+
+ /// Convert the computed value into used value.
+ #[inline]
+ pub fn maybe_to_used_value(&self, container_len: Option<Length>) -> Option<Au> {
+ self.maybe_percentage_relative_to(container_len)
+ .map(Au::from)
+ }
+
+ /// If there are special rules for computing percentages in a value (e.g.
+ /// the height property), they apply whenever a calc() expression contains
+ /// percentages.
+ pub fn maybe_percentage_relative_to(&self, container_len: Option<Length>) -> Option<Length> {
+ if let Unpacked::Length(l) = self.unpack() {
+ return Some(l);
+ }
+ Some(self.resolve(container_len?))
+ }
+
+ /// Returns the clamped non-negative values.
+ #[inline]
+ pub fn clamp_to_non_negative(mut self) -> Self {
+ match self.unpack_mut() {
+ UnpackedMut::Length(l) => Self::new_length(l.clamp_to_non_negative()),
+ UnpackedMut::Percentage(p) => Self::new_percent(p.clamp_to_non_negative()),
+ UnpackedMut::Calc(ref mut c) => {
+ c.clamping_mode = AllowedNumericType::NonNegative;
+ self
+ },
+ }
+ }
+}
+
+impl PartialEq for LengthPercentage {
+ fn eq(&self, other: &Self) -> bool {
+ self.unpack() == other.unpack()
+ }
+}
+
+impl fmt::Debug for LengthPercentage {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ self.unpack().fmt(formatter)
+ }
+}
+
+impl ToAnimatedZero for LengthPercentage {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(match self.unpack() {
+ Unpacked::Length(l) => Self::new_length(l.to_animated_zero()?),
+ Unpacked::Percentage(p) => Self::new_percent(p.to_animated_zero()?),
+ Unpacked::Calc(c) => Self::new_calc_unchecked(Box::new(c.to_animated_zero()?)),
+ })
+ }
+}
+
+impl Clone for LengthPercentage {
+ fn clone(&self) -> Self {
+ match self.unpack() {
+ Unpacked::Length(l) => Self::new_length(l),
+ Unpacked::Percentage(p) => Self::new_percent(p),
+ Unpacked::Calc(c) => Self::new_calc_unchecked(Box::new(c.clone())),
+ }
+ }
+}
+
+impl ToComputedValue for specified::LengthPercentage {
+ type ComputedValue = LengthPercentage;
+
+ fn to_computed_value(&self, context: &Context) -> LengthPercentage {
+ match *self {
+ specified::LengthPercentage::Length(ref value) => {
+ LengthPercentage::new_length(value.to_computed_value(context))
+ },
+ specified::LengthPercentage::Percentage(value) => LengthPercentage::new_percent(value),
+ specified::LengthPercentage::Calc(ref calc) => (**calc).to_computed_value(context),
+ }
+ }
+
+ fn from_computed_value(computed: &LengthPercentage) -> Self {
+ match computed.unpack() {
+ Unpacked::Length(ref l) => {
+ specified::LengthPercentage::Length(ToComputedValue::from_computed_value(l))
+ },
+ Unpacked::Percentage(p) => specified::LengthPercentage::Percentage(p),
+ Unpacked::Calc(c) => {
+ // We simplify before constructing the LengthPercentage if
+ // needed, so this is always fine.
+ specified::LengthPercentage::Calc(Box::new(
+ specified::CalcLengthPercentage::from_computed_value(c),
+ ))
+ },
+ }
+ }
+}
+
+impl ComputeSquaredDistance for LengthPercentage {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ // A somewhat arbitrary base, it doesn't really make sense to mix
+ // lengths with percentages, but we can't do much better here, and this
+ // ensures that the distance between length-only and percentage-only
+ // lengths makes sense.
+ let basis = Length::new(100.);
+ self.resolve(basis)
+ .compute_squared_distance(&other.resolve(basis))
+ }
+}
+
+impl ToCss for LengthPercentage {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.unpack().to_css(dest)
+ }
+}
+
+impl Zero for LengthPercentage {
+ fn zero() -> Self {
+ LengthPercentage::new_length(Length::zero())
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.is_definitely_zero()
+ }
+}
+
+impl ZeroNoPercent for LengthPercentage {
+ #[inline]
+ fn is_zero_no_percent(&self) -> bool {
+ self.is_definitely_zero() && !self.has_percentage()
+ }
+}
+
+impl Serialize for LengthPercentage {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ self.to_serializable().serialize(serializer)
+ }
+}
+
+impl<'de> Deserialize<'de> for LengthPercentage {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Ok(Self::from_serializable(Serializable::deserialize(
+ deserializer,
+ )?))
+ }
+}
+
+/// The leaves of a `<length-percentage>` calc expression.
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum CalcLengthPercentageLeaf {
+ Length(Length),
+ Percentage(Percentage),
+ Number(f32),
+}
+
+impl CalcLengthPercentageLeaf {
+ fn is_zero_length(&self) -> bool {
+ match *self {
+ Self::Length(ref l) => l.is_zero(),
+ Self::Percentage(..) => false,
+ Self::Number(..) => false,
+ }
+ }
+}
+
+impl calc::CalcNodeLeaf for CalcLengthPercentageLeaf {
+ fn unit(&self) -> CalcUnits {
+ match self {
+ Self::Length(_) => CalcUnits::LENGTH,
+ Self::Percentage(_) => CalcUnits::PERCENTAGE,
+ Self::Number(_) => CalcUnits::empty(),
+ }
+ }
+
+ fn unitless_value(&self) -> f32 {
+ match *self {
+ Self::Length(ref l) => l.px(),
+ Self::Percentage(ref p) => p.0,
+ Self::Number(n) => n,
+ }
+ }
+
+ fn new_number(value: f32) -> Self {
+ Self::Number(value)
+ }
+
+ fn as_number(&self) -> Option<f32> {
+ match *self {
+ Self::Length(_) | Self::Percentage(_) => None,
+ Self::Number(value) => Some(value),
+ }
+ }
+
+ fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option<std::cmp::Ordering> {
+ use self::CalcLengthPercentageLeaf::*;
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) {
+ return None;
+ }
+
+ let self_negative = self.is_negative();
+ if self_negative != other.is_negative() {
+ return Some(if self_negative { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater });
+ }
+
+ match (self, other) {
+ (&Length(ref one), &Length(ref other)) => one.partial_cmp(other),
+ (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other),
+ (&Number(ref one), &Number(ref other)) => one.partial_cmp(other),
+ _ => unsafe {
+ match *self {
+ Length(..) | Percentage(..) | Number(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in compare()")
+ },
+ }
+ }
+
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> {
+ use self::CalcLengthPercentageLeaf::*;
+
+ // 0px plus anything else is equal to the right hand side.
+ if self.is_zero_length() {
+ *self = other.clone();
+ return Ok(());
+ }
+
+ if other.is_zero_length() {
+ return Ok(());
+ }
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ match (self, other) {
+ (&mut Length(ref mut one), &Length(ref other)) => {
+ *one += *other;
+ },
+ (&mut Percentage(ref mut one), &Percentage(ref other)) => {
+ one.0 += other.0;
+ },
+ (&mut Number(ref mut one), &Number(ref other)) => {
+ *one += *other;
+ },
+ _ => unsafe {
+ match *other {
+ Length(..) | Percentage(..) | Number(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_sum_in_place()")
+ },
+ }
+
+ Ok(())
+ }
+
+ fn try_product_in_place(&mut self, other: &mut Self) -> bool {
+ if let Self::Number(ref mut left) = *self {
+ if let Self::Number(ref right) = *other {
+ // Both sides are numbers, so we can just modify the left side.
+ *left *= *right;
+ true
+ } else {
+ // The right side is not a number, so the result should be in the units of the right
+ // side.
+ other.map(|v| v * *left);
+ std::mem::swap(self, other);
+ true
+ }
+ } else if let Self::Number(ref right) = *other {
+ // The left side is not a number, but the right side is, so the result is the left
+ // side unit.
+ self.map(|v| v * *right);
+ true
+ } else {
+ // Neither side is a number, so a product is not possible.
+ false
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::CalcLengthPercentageLeaf::*;
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+ Ok(match (self, other) {
+ (&Length(ref one), &Length(ref other)) => {
+ Length(super::Length::new(op(one.px(), other.px())))
+ },
+ (&Percentage(one), &Percentage(other)) => {
+ Self::Percentage(super::Percentage(op(one.0, other.0)))
+ },
+ (&Number(one), &Number(other)) => Self::Number(op(one, other)),
+ _ => unsafe {
+ match *self {
+ Length(..) | Percentage(..) | Number(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ fn map(&mut self, mut op: impl FnMut(f32) -> f32) {
+ match self {
+ Self::Length(value) => {
+ *value = Length::new(op(value.px()));
+ },
+ Self::Percentage(value) => {
+ *value = Percentage(op(value.0));
+ },
+ Self::Number(value) => {
+ *value = op(*value);
+ },
+ }
+ }
+
+ fn simplify(&mut self) {}
+
+ fn sort_key(&self) -> calc::SortKey {
+ match *self {
+ Self::Length(..) => calc::SortKey::Px,
+ Self::Percentage(..) => calc::SortKey::Percentage,
+ Self::Number(..) => calc::SortKey::Number,
+ }
+ }
+}
+
+/// The computed version of a calc() node for `<length-percentage>` values.
+pub type CalcNode = calc::GenericCalcNode<CalcLengthPercentageLeaf>;
+
+/// The representation of a calc() function with mixed lengths and percentages.
+#[derive(
+ Clone, Debug, Deserialize, MallocSizeOf, Serialize, ToAnimatedZero, ToResolvedValue, ToCss,
+)]
+#[repr(C)]
+pub struct CalcLengthPercentage {
+ #[animation(constant)]
+ #[css(skip)]
+ clamping_mode: AllowedNumericType,
+ node: CalcNode,
+}
+
+impl CalcLengthPercentage {
+ /// Resolves the percentage.
+ #[inline]
+ pub fn resolve(&self, basis: Length) -> Length {
+ // unwrap() is fine because the conversion below is infallible.
+ if let CalcLengthPercentageLeaf::Length(px) = self
+ .node
+ .resolve_map(|leaf| {
+ Ok(if let CalcLengthPercentageLeaf::Percentage(p) = leaf {
+ CalcLengthPercentageLeaf::Length(Length::new(basis.px() * p.0))
+ } else {
+ leaf.clone()
+ })
+ })
+ .unwrap()
+ {
+ Length::new(self.clamping_mode.clamp(px.px())).normalized()
+ } else {
+ unreachable!("resolve_map should turn percentages to lengths, and parsing should ensure that we don't end up with a number");
+ }
+ }
+}
+
+// NOTE(emilio): We don't compare `clamping_mode` since we want to preserve the
+// invariant that `from_computed_value(length).to_computed_value(..) == length`.
+//
+// Right now for e.g. a non-negative length, we set clamping_mode to `All`
+// unconditionally for non-calc values, and to `NonNegative` for calc.
+//
+// If we determine that it's sound, from_computed_value() can generate an
+// absolute length, which then would get `All` as the clamping mode.
+//
+// We may want to just eagerly-detect whether we can clamp in
+// `LengthPercentage::new` and switch to `AllowedNumericType::NonNegative` then,
+// maybe.
+impl PartialEq for CalcLengthPercentage {
+ fn eq(&self, other: &Self) -> bool {
+ self.node == other.node
+ }
+}
+
+impl specified::CalcLengthPercentage {
+ /// Compute the value, zooming any absolute units by the zoom function.
+ fn to_computed_value_with_zoom<F>(
+ &self,
+ context: &Context,
+ zoom_fn: F,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> LengthPercentage
+ where
+ F: Fn(Length) -> Length,
+ {
+ use crate::values::specified::calc::Leaf;
+
+ let node = self.node.map_leaves(|leaf| match *leaf {
+ Leaf::Percentage(p) => CalcLengthPercentageLeaf::Percentage(Percentage(p)),
+ Leaf::Length(l) => CalcLengthPercentageLeaf::Length({
+ let result =
+ l.to_computed_value_with_base_size(context, base_size, line_height_base);
+ if l.should_zoom_text() {
+ zoom_fn(result)
+ } else {
+ result
+ }
+ }),
+ Leaf::Number(n) => CalcLengthPercentageLeaf::Number(n),
+ Leaf::Angle(..) | Leaf::Time(..) | Leaf::Resolution(..) => {
+ unreachable!("Shouldn't have parsed")
+ },
+ });
+
+ LengthPercentage::new_calc(node, self.clamping_mode)
+ }
+
+ /// Compute font-size or line-height taking into account text-zoom if necessary.
+ pub fn to_computed_value_zoomed(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> LengthPercentage {
+ self.to_computed_value_with_zoom(
+ context,
+ |abs| context.maybe_zoom_text(abs),
+ base_size,
+ line_height_base,
+ )
+ }
+
+ /// Compute the value into pixel length as CSSFloat without context,
+ /// so it returns Err(()) if there is any non-absolute unit.
+ pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> {
+ use crate::values::specified::calc::Leaf;
+ use crate::values::specified::length::NoCalcLength;
+
+ // Simplification should've turned this into an absolute length,
+ // otherwise it wouldn't have been able to.
+ match self.node {
+ calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::Absolute(ref l))) => Ok(l.to_px()),
+ _ => Err(()),
+ }
+ }
+
+ /// Compute the value into pixel length as CSSFloat, using the get_font_metrics function
+ /// if provided to resolve font-relative dimensions.
+ pub fn to_computed_pixel_length_with_font_metrics(
+ &self,
+ get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>,
+ ) -> Result<CSSFloat, ()> {
+ use crate::values::specified::calc::Leaf;
+ use crate::values::specified::length::NoCalcLength;
+
+ match self.node {
+ calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::Absolute(ref l))) => Ok(l.to_px()),
+ calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::FontRelative(ref l))) => {
+ if let Some(getter) = get_font_metrics {
+ l.to_computed_pixel_length_with_font_metrics(getter)
+ } else {
+ Err(())
+ }
+ },
+ _ => Err(()),
+ }
+ }
+
+ /// Compute the calc using the current font-size and line-height. (and without text-zoom).
+ pub fn to_computed_value(&self, context: &Context) -> LengthPercentage {
+ self.to_computed_value_with_zoom(
+ context,
+ |abs| abs,
+ FontBaseSize::CurrentStyle,
+ LineHeightBase::CurrentStyle,
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &CalcLengthPercentage) -> Self {
+ use crate::values::specified::calc::Leaf;
+ use crate::values::specified::length::NoCalcLength;
+
+ specified::CalcLengthPercentage {
+ clamping_mode: computed.clamping_mode,
+ node: computed.node.map_leaves(|l| match l {
+ CalcLengthPercentageLeaf::Length(ref l) => {
+ Leaf::Length(NoCalcLength::from_px(l.px()))
+ },
+ CalcLengthPercentageLeaf::Percentage(ref p) => Leaf::Percentage(p.0),
+ CalcLengthPercentageLeaf::Number(n) => Leaf::Number(*n),
+ }),
+ }
+ }
+}
+
+/// https://drafts.csswg.org/css-transitions/#animtype-lpcalc
+/// https://drafts.csswg.org/css-values-4/#combine-math
+/// https://drafts.csswg.org/css-values-4/#combine-mixed
+impl Animate for LengthPercentage {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(match (self.unpack(), other.unpack()) {
+ (Unpacked::Length(one), Unpacked::Length(other)) => {
+ Self::new_length(one.animate(&other, procedure)?)
+ },
+ (Unpacked::Percentage(one), Unpacked::Percentage(other)) => {
+ Self::new_percent(one.animate(&other, procedure)?)
+ },
+ _ => {
+ use calc::CalcNodeLeaf;
+
+ fn product_with(mut node: CalcNode, product: f32) -> CalcNode {
+ let mut number = CalcNode::Leaf(CalcLengthPercentageLeaf::new_number(product));
+ if !node.try_product_in_place(&mut number) {
+ CalcNode::Product(vec![node, number].into())
+ } else {
+ node
+ }
+ }
+
+ let (l, r) = procedure.weights();
+ let one = product_with(self.to_calc_node().into_owned(), l as f32);
+ let other = product_with(other.to_calc_node().into_owned(), r as f32);
+
+ Self::new_calc(
+ CalcNode::Sum(vec![one, other].into()),
+ AllowedNumericType::All,
+ )
+ },
+ })
+ }
+}
+
+/// A wrapper of LengthPercentage, whose value must be >= 0.
+pub type NonNegativeLengthPercentage = NonNegative<LengthPercentage>;
+
+impl ToAnimatedValue for NonNegativeLengthPercentage {
+ type AnimatedValue = LengthPercentage;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ NonNegative(animated.clamp_to_non_negative())
+ }
+}
+
+impl NonNegativeLengthPercentage {
+ /// Returns true if the computed value is absolute 0 or 0%.
+ #[inline]
+ pub fn is_definitely_zero(&self) -> bool {
+ self.0.is_definitely_zero()
+ }
+
+ /// Returns the used value.
+ #[inline]
+ pub fn to_used_value(&self, containing_length: Au) -> Au {
+ let resolved = self.0.to_used_value(containing_length);
+ std::cmp::max(resolved, Au(0))
+ }
+
+ /// Convert the computed value into used value.
+ #[inline]
+ pub fn maybe_to_used_value(&self, containing_length: Option<Au>) -> Option<Au> {
+ let resolved = self
+ .0
+ .maybe_to_used_value(containing_length.map(|v| v.into()))?;
+ Some(std::cmp::max(resolved, Au(0)))
+ }
+}
diff --git a/servo/components/style/values/computed/list.rs b/servo/components/style/values/computed/list.rs
new file mode 100644
index 0000000000..3e5d1eb220
--- /dev/null
+++ b/servo/components/style/values/computed/list.rs
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! `list` computed values.
+
+#[cfg(feature = "gecko")]
+pub use crate::values::specified::list::ListStyleType;
+pub use crate::values::specified::list::Quotes;
+
+impl Quotes {
+ /// Initial value for `quotes`.
+ #[inline]
+ pub fn get_initial_value() -> Quotes {
+ Quotes::Auto
+ }
+}
diff --git a/servo/components/style/values/computed/mod.rs b/servo/components/style/values/computed/mod.rs
new file mode 100644
index 0000000000..de5db2cdab
--- /dev/null
+++ b/servo/components/style/values/computed/mod.rs
@@ -0,0 +1,1035 @@
+/* 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/. */
+
+//! Computed values.
+
+use self::transform::DirectionVector;
+use super::animated::ToAnimatedValue;
+use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent;
+use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks;
+use super::generics::grid::{GenericGridLine, GenericTrackBreadth};
+use super::generics::grid::{GenericTrackSize, TrackList as GenericTrackList};
+use super::generics::transform::IsParallelTo;
+use super::generics::{self, GreaterThanOrEqualToOne, NonNegative, ZeroToOne};
+use super::specified;
+use super::{CSSFloat, CSSInteger};
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::context::QuirksMode;
+use crate::custom_properties::ComputedCustomProperties;
+use crate::font_metrics::{FontMetrics, FontMetricsOrientation};
+use crate::media_queries::Device;
+#[cfg(feature = "gecko")]
+use crate::properties;
+use crate::properties::{ComputedValues, StyleBuilder};
+use crate::rule_cache::RuleCacheConditions;
+use crate::stylesheets::container_rule::{
+ ContainerInfo, ContainerSizeQuery, ContainerSizeQueryResult,
+};
+use crate::stylist::Stylist;
+use crate::values::specified::length::FontBaseSize;
+use crate::{ArcSlice, Atom, One};
+use euclid::{default, Point2D, Rect, Size2D};
+use servo_arc::Arc;
+use std::cell::RefCell;
+use std::cmp;
+use std::f32;
+use std::ops::{Add, Sub};
+
+#[cfg(feature = "gecko")]
+pub use self::align::{
+ AlignContent, AlignItems, AlignTracks, JustifyContent, JustifyItems, JustifyTracks,
+ SelfAlignment,
+};
+#[cfg(feature = "gecko")]
+pub use self::align::{AlignSelf, JustifySelf};
+pub use self::angle::Angle;
+pub use self::animation::{
+ AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState,
+ AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis,
+ ScrollTimelineName, TransitionProperty, ViewTimelineInset
+};
+pub use self::background::{BackgroundRepeat, BackgroundSize};
+pub use self::basic_shape::FillRule;
+pub use self::border::{
+ BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice,
+ BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, LineWidth,
+};
+pub use self::box_::{
+ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize,
+ ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow,
+ OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign,
+ ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter,
+ TouchAction, VerticalAlign, WillChange, Zoom,
+};
+pub use self::color::{
+ Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust,
+};
+pub use self::column::ColumnCount;
+pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet};
+pub use self::easing::TimingFunction;
+pub use self::effects::{BoxShadow, Filter, SimpleShadow};
+pub use self::flex::FlexBasis;
+pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle};
+pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric};
+pub use self::font::{FontSize, FontSizeAdjust, FontStretch, FontSynthesis, LineHeight};
+pub use self::font::{FontVariantAlternates, FontWeight};
+pub use self::font::{FontVariantEastAsian, FontVariationSettings};
+pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale};
+pub use self::image::{Gradient, Image, ImageRendering, LineDirection};
+pub use self::length::{CSSPixelLength, NonNegativeLength};
+pub use self::length::{Length, LengthOrNumber, LengthPercentage, NonNegativeLengthOrNumber};
+pub use self::length::{LengthOrAuto, LengthPercentageOrAuto, MaxSize, Size};
+pub use self::length::{NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto};
+#[cfg(feature = "gecko")]
+pub use self::list::ListStyleType;
+pub use self::list::Quotes;
+pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate};
+pub use self::outline::OutlineStyle;
+pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize};
+pub use self::percentage::{NonNegativePercentage, Percentage};
+pub use self::position::AspectRatio;
+pub use self::position::{
+ GridAutoFlow, GridTemplateAreas, MasonryAutoFlow, Position, PositionOrAuto, ZIndex,
+};
+pub use self::ratio::Ratio;
+pub use self::rect::NonNegativeLengthOrNumberRect;
+pub use self::resolution::Resolution;
+pub use self::svg::{DProperty, MozContextProperties};
+pub use self::svg::{SVGLength, SVGOpacity, SVGPaint, SVGPaintKind};
+pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth};
+pub use self::text::HyphenateCharacter;
+pub use self::text::TextUnderlinePosition;
+pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextIndent};
+pub use self::text::{OverflowWrap, RubyPosition, TextOverflow, WordBreak, WordSpacing};
+pub use self::text::{TextAlign, TextAlignLast, TextEmphasisPosition, TextEmphasisStyle};
+pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify};
+pub use self::time::Time;
+pub use self::transform::{Rotate, Scale, Transform, TransformBox, TransformOperation};
+pub use self::transform::{TransformOrigin, TransformStyle, Translate};
+#[cfg(feature = "gecko")]
+pub use self::ui::CursorImage;
+pub use self::ui::{BoolInteger, Cursor, UserSelect};
+pub use super::specified::TextTransform;
+pub use super::specified::ViewportVariant;
+pub use super::specified::{BorderStyle, TextDecorationLine};
+pub use app_units::Au;
+
+#[cfg(feature = "gecko")]
+pub mod align;
+pub mod angle;
+pub mod animation;
+pub mod background;
+pub mod basic_shape;
+pub mod border;
+#[path = "box.rs"]
+pub mod box_;
+pub mod color;
+pub mod column;
+pub mod counters;
+pub mod easing;
+pub mod effects;
+pub mod flex;
+pub mod font;
+pub mod image;
+pub mod length;
+pub mod length_percentage;
+pub mod list;
+pub mod motion;
+pub mod outline;
+pub mod page;
+pub mod percentage;
+pub mod position;
+pub mod ratio;
+pub mod rect;
+pub mod resolution;
+pub mod svg;
+pub mod table;
+pub mod text;
+pub mod time;
+pub mod transform;
+pub mod ui;
+pub mod url;
+
+/// A `Context` is all the data a specified value could ever need to compute
+/// itself and be transformed to a computed value.
+pub struct Context<'a> {
+ /// Values accessed through this need to be in the properties "computed
+ /// early": color, text-decoration, font-size, display, position, float,
+ /// border-*-style, outline-style, font-family, writing-mode...
+ pub builder: StyleBuilder<'a>,
+
+ /// A cached computed system font value, for use by gecko.
+ ///
+ /// See properties/longhands/font.mako.rs
+ #[cfg(feature = "gecko")]
+ pub cached_system_font: Option<properties::longhands::system_font::ComputedSystemFont>,
+
+ /// A dummy option for servo so initializing a computed::Context isn't
+ /// painful.
+ ///
+ /// TODO(emilio): Make constructors for Context, and drop this.
+ #[cfg(feature = "servo")]
+ pub cached_system_font: Option<()>,
+
+ /// Whether or not we are computing the media list in a media query.
+ pub in_media_query: bool,
+
+ /// Whether or not we are computing the container query condition.
+ pub in_container_query: bool,
+
+ /// The quirks mode of this context.
+ pub quirks_mode: QuirksMode,
+
+ /// Whether this computation is being done for a SMIL animation.
+ ///
+ /// This is used to allow certain properties to generate out-of-range
+ /// values, which SMIL allows.
+ pub for_smil_animation: bool,
+
+ /// Returns the container information to evaluate a given container query.
+ pub container_info: Option<ContainerInfo>,
+
+ /// Whether we're computing a value for a non-inherited property.
+ /// False if we are computed a value for an inherited property or not computing for a property
+ /// at all (e.g. in a media query evaluation).
+ pub for_non_inherited_property: bool,
+
+ /// The conditions to cache a rule node on the rule cache.
+ ///
+ /// FIXME(emilio): Drop the refcell.
+ pub rule_cache_conditions: RefCell<&'a mut RuleCacheConditions>,
+
+ /// Container size query for this context.
+ container_size_query: RefCell<ContainerSizeQuery<'a>>,
+}
+
+impl<'a> Context<'a> {
+ /// Lazily evaluate the container size query, returning the result.
+ pub fn get_container_size_query(&self) -> ContainerSizeQueryResult {
+ let mut resolved = self.container_size_query.borrow_mut();
+ resolved.get().clone()
+ }
+
+ /// Creates a suitable context for media query evaluation, in which
+ /// font-relative units compute against the system_font, and executes `f`
+ /// with it.
+ pub fn for_media_query_evaluation<F, R>(device: &Device, quirks_mode: QuirksMode, f: F) -> R
+ where
+ F: FnOnce(&Context) -> R,
+ {
+ let mut conditions = RuleCacheConditions::default();
+ let context = Context {
+ builder: StyleBuilder::for_inheritance(device, None, None, None),
+ cached_system_font: None,
+ in_media_query: true,
+ in_container_query: false,
+ quirks_mode,
+ for_smil_animation: false,
+ container_info: None,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(&mut conditions),
+ container_size_query: RefCell::new(ContainerSizeQuery::none()),
+ };
+ f(&context)
+ }
+
+ /// Creates a suitable context for container query evaluation for the style
+ /// specified.
+ pub fn for_container_query_evaluation<F, R>(
+ device: &Device,
+ stylist: Option<&Stylist>,
+ container_info_and_style: Option<(ContainerInfo, Arc<ComputedValues>)>,
+ container_size_query: ContainerSizeQuery,
+ f: F,
+ ) -> R
+ where
+ F: FnOnce(&Context) -> R,
+ {
+ let mut conditions = RuleCacheConditions::default();
+
+ let (container_info, style) = match container_info_and_style {
+ Some((ci, s)) => (Some(ci), Some(s)),
+ None => (None, None),
+ };
+
+ let style = style.as_ref().map(|s| &**s);
+ let quirks_mode = device.quirks_mode();
+ let context = Context {
+ builder: StyleBuilder::for_inheritance(device, stylist, style, None),
+ cached_system_font: None,
+ in_media_query: false,
+ in_container_query: true,
+ quirks_mode,
+ for_smil_animation: false,
+ container_info,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(&mut conditions),
+ container_size_query: RefCell::new(container_size_query),
+ };
+
+ f(&context)
+ }
+
+ /// Creates a context suitable for more general cases.
+ pub fn new(
+ builder: StyleBuilder<'a>,
+ quirks_mode: QuirksMode,
+ rule_cache_conditions: &'a mut RuleCacheConditions,
+ container_size_query: ContainerSizeQuery<'a>,
+ ) -> Self {
+ Self {
+ builder,
+ cached_system_font: None,
+ in_media_query: false,
+ in_container_query: false,
+ quirks_mode,
+ container_info: None,
+ for_smil_animation: false,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(rule_cache_conditions),
+ container_size_query: RefCell::new(container_size_query),
+ }
+ }
+
+ /// Creates a context suitable for computing animations.
+ pub fn new_for_animation(
+ builder: StyleBuilder<'a>,
+ for_smil_animation: bool,
+ quirks_mode: QuirksMode,
+ rule_cache_conditions: &'a mut RuleCacheConditions,
+ container_size_query: ContainerSizeQuery<'a>,
+ ) -> Self {
+ Self {
+ builder,
+ cached_system_font: None,
+ in_media_query: false,
+ in_container_query: false,
+ quirks_mode,
+ container_info: None,
+ for_smil_animation,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(rule_cache_conditions),
+ container_size_query: RefCell::new(container_size_query),
+ }
+ }
+
+ /// Creates a context suitable for computing the initial value of @property.
+ pub fn new_for_initial_at_property_value(
+ stylist: &'a Stylist,
+ rule_cache_conditions: &'a mut RuleCacheConditions,
+ ) -> Self {
+ Self {
+ builder: StyleBuilder::new(stylist.device(), Some(stylist), None, None, None, false),
+ cached_system_font: None,
+ // Because font-relative values are disallowed in @property initial values, we do not
+ // need to keep track of whether we're in a media query, whether we're in a container
+ // query, and so on.
+ in_media_query: false,
+ in_container_query: false,
+ quirks_mode: stylist.quirks_mode(),
+ container_info: None,
+ for_smil_animation: false,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(rule_cache_conditions),
+ container_size_query: RefCell::new(ContainerSizeQuery::none()),
+ }
+ }
+
+ /// The current device.
+ pub fn device(&self) -> &Device {
+ self.builder.device
+ }
+
+ /// Get the inherited custom properties map.
+ pub fn inherited_custom_properties(&self) -> &ComputedCustomProperties {
+ &self.builder.inherited_custom_properties()
+ }
+
+ /// Whether the style is for the root element.
+ pub fn is_root_element(&self) -> bool {
+ self.builder.is_root_element
+ }
+
+ /// Queries font metrics.
+ pub fn query_font_metrics(
+ &self,
+ base_size: FontBaseSize,
+ orientation: FontMetricsOrientation,
+ retrieve_math_scales: bool,
+ ) -> FontMetrics {
+ if self.for_non_inherited_property {
+ self.rule_cache_conditions.borrow_mut().set_uncacheable();
+ }
+ self.builder.add_flags(match base_size {
+ FontBaseSize::CurrentStyle => ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS,
+ FontBaseSize::InheritedStyle => ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS,
+ });
+ let size = base_size.resolve(self).used_size();
+ let style = self.style();
+
+ let (wm, font) = match base_size {
+ FontBaseSize::CurrentStyle => (style.writing_mode, style.get_font()),
+ // This is only used for font-size computation.
+ FontBaseSize::InheritedStyle => {
+ (*style.inherited_writing_mode(), style.get_parent_font())
+ },
+ };
+
+ let vertical = match orientation {
+ FontMetricsOrientation::MatchContextPreferHorizontal => {
+ wm.is_vertical() && wm.is_upright()
+ },
+ FontMetricsOrientation::MatchContextPreferVertical => wm.is_text_vertical(),
+ FontMetricsOrientation::Horizontal => false,
+ };
+ self.device().query_font_metrics(
+ vertical,
+ font,
+ size,
+ self.in_media_or_container_query(),
+ retrieve_math_scales,
+ )
+ }
+
+ /// The current viewport size, used to resolve viewport units.
+ pub fn viewport_size_for_viewport_unit_resolution(
+ &self,
+ variant: ViewportVariant,
+ ) -> default::Size2D<Au> {
+ self.builder
+ .add_flags(ComputedValueFlags::USES_VIEWPORT_UNITS);
+ self.builder
+ .device
+ .au_viewport_size_for_viewport_unit_resolution(variant)
+ }
+
+ /// Whether we're in a media or container query.
+ pub fn in_media_or_container_query(&self) -> bool {
+ self.in_media_query || self.in_container_query
+ }
+
+ /// The default computed style we're getting our reset style from.
+ pub fn default_style(&self) -> &ComputedValues {
+ self.builder.default_style()
+ }
+
+ /// The current style.
+ pub fn style(&self) -> &StyleBuilder {
+ &self.builder
+ }
+
+ /// Apply text-zoom if enabled.
+ #[cfg(feature = "gecko")]
+ pub fn maybe_zoom_text(&self, size: CSSPixelLength) -> CSSPixelLength {
+ if self
+ .style()
+ .get_font()
+ .clone__x_text_scale()
+ .text_zoom_enabled()
+ {
+ self.device().zoom_text(size)
+ } else {
+ size
+ }
+ }
+
+ /// (Servo doesn't do text-zoom)
+ #[cfg(feature = "servo")]
+ pub fn maybe_zoom_text(&self, size: CSSPixelLength) -> CSSPixelLength {
+ size
+ }
+}
+
+/// An iterator over a slice of computed values
+#[derive(Clone)]
+pub struct ComputedVecIter<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> {
+ cx: &'cx Context<'cx_a>,
+ values: &'a [S],
+}
+
+impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> ComputedVecIter<'a, 'cx, 'cx_a, S> {
+ /// Construct an iterator from a slice of specified values and a context
+ pub fn new(cx: &'cx Context<'cx_a>, values: &'a [S]) -> Self {
+ ComputedVecIter { cx, values }
+ }
+}
+
+impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> ExactSizeIterator
+ for ComputedVecIter<'a, 'cx, 'cx_a, S>
+{
+ fn len(&self) -> usize {
+ self.values.len()
+ }
+}
+
+impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> Iterator for ComputedVecIter<'a, 'cx, 'cx_a, S> {
+ type Item = S::ComputedValue;
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some((next, rest)) = self.values.split_first() {
+ let ret = next.to_computed_value(self.cx);
+ self.values = rest;
+ Some(ret)
+ } else {
+ None
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (self.values.len(), Some(self.values.len()))
+ }
+}
+
+/// A trait to represent the conversion between computed and specified values.
+///
+/// This trait is derivable with `#[derive(ToComputedValue)]`. The derived
+/// implementation just calls `ToComputedValue::to_computed_value` on each field
+/// of the passed value. The deriving code assumes that if the type isn't
+/// generic, then the trait can be implemented as simple `Clone::clone` calls,
+/// this means that a manual implementation with `ComputedValue = Self` is bogus
+/// if it returns anything else than a clone.
+pub trait ToComputedValue {
+ /// The computed value type we're going to be converted to.
+ type ComputedValue;
+
+ /// Convert a specified value to a computed value, using itself and the data
+ /// inside the `Context`.
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue;
+
+ /// Convert a computed value to specified value form.
+ ///
+ /// This will be used for recascading during animation.
+ /// Such from_computed_valued values should recompute to the same value.
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self;
+}
+
+impl<A, B> ToComputedValue for (A, B)
+where
+ A: ToComputedValue,
+ B: ToComputedValue,
+{
+ type ComputedValue = (
+ <A as ToComputedValue>::ComputedValue,
+ <B as ToComputedValue>::ComputedValue,
+ );
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ (
+ self.0.to_computed_value(context),
+ self.1.to_computed_value(context),
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ (
+ A::from_computed_value(&computed.0),
+ B::from_computed_value(&computed.1),
+ )
+ }
+}
+
+impl<T> ToComputedValue for Option<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = Option<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.as_ref().map(|item| item.to_computed_value(context))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed.as_ref().map(T::from_computed_value)
+ }
+}
+
+impl<T> ToComputedValue for default::Size2D<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = default::Size2D<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ Size2D::new(
+ self.width.to_computed_value(context),
+ self.height.to_computed_value(context),
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Size2D::new(
+ T::from_computed_value(&computed.width),
+ T::from_computed_value(&computed.height),
+ )
+ }
+}
+
+impl<T> ToComputedValue for Vec<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = Vec<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.iter()
+ .map(|item| item.to_computed_value(context))
+ .collect()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed.iter().map(T::from_computed_value).collect()
+ }
+}
+
+impl<T> ToComputedValue for Box<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = Box<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ Box::new(T::to_computed_value(self, context))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Box::new(T::from_computed_value(computed))
+ }
+}
+
+impl<T> ToComputedValue for Box<[T]>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = Box<[<T as ToComputedValue>::ComputedValue]>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.iter()
+ .map(|item| item.to_computed_value(context))
+ .collect::<Vec<_>>()
+ .into_boxed_slice()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed
+ .iter()
+ .map(T::from_computed_value)
+ .collect::<Vec<_>>()
+ .into_boxed_slice()
+ }
+}
+
+impl<T> ToComputedValue for crate::OwnedSlice<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = crate::OwnedSlice<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.iter()
+ .map(|item| item.to_computed_value(context))
+ .collect()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed.iter().map(T::from_computed_value).collect()
+ }
+}
+
+// NOTE(emilio): This is implementable more generically, but it's unlikely
+// what you want there, as it forces you to have an extra allocation.
+//
+// We could do that if needed, ideally with specialization for the case where
+// ComputedValue = T. But we don't need it for now.
+impl<T> ToComputedValue for Arc<T>
+where
+ T: ToComputedValue<ComputedValue = T>,
+{
+ type ComputedValue = Self;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self {
+ self.clone()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self) -> Self {
+ computed.clone()
+ }
+}
+
+// Same caveat as above applies.
+impl<T> ToComputedValue for ArcSlice<T>
+where
+ T: ToComputedValue<ComputedValue = T>,
+{
+ type ComputedValue = Self;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self {
+ self.clone()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self) -> Self {
+ computed.clone()
+ }
+}
+
+trivial_to_computed_value!(());
+trivial_to_computed_value!(bool);
+trivial_to_computed_value!(f32);
+trivial_to_computed_value!(i32);
+trivial_to_computed_value!(u8);
+trivial_to_computed_value!(u16);
+trivial_to_computed_value!(u32);
+trivial_to_computed_value!(usize);
+trivial_to_computed_value!(Atom);
+trivial_to_computed_value!(crate::values::AtomIdent);
+#[cfg(feature = "servo")]
+trivial_to_computed_value!(crate::Namespace);
+#[cfg(feature = "servo")]
+trivial_to_computed_value!(crate::Prefix);
+trivial_to_computed_value!(String);
+trivial_to_computed_value!(Box<str>);
+trivial_to_computed_value!(crate::OwnedStr);
+trivial_to_computed_value!(style_traits::values::specified::AllowedNumericType);
+trivial_to_computed_value!(crate::values::generics::color::ColorMixFlags);
+
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[repr(C, u8)]
+pub enum AngleOrPercentage {
+ Percentage(Percentage),
+ Angle(Angle),
+}
+
+impl ToComputedValue for specified::AngleOrPercentage {
+ type ComputedValue = AngleOrPercentage;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> AngleOrPercentage {
+ match *self {
+ specified::AngleOrPercentage::Percentage(percentage) => {
+ AngleOrPercentage::Percentage(percentage.to_computed_value(context))
+ },
+ specified::AngleOrPercentage::Angle(angle) => {
+ AngleOrPercentage::Angle(angle.to_computed_value(context))
+ },
+ }
+ }
+ #[inline]
+ fn from_computed_value(computed: &AngleOrPercentage) -> Self {
+ match *computed {
+ AngleOrPercentage::Percentage(percentage) => specified::AngleOrPercentage::Percentage(
+ ToComputedValue::from_computed_value(&percentage),
+ ),
+ AngleOrPercentage::Angle(angle) => {
+ specified::AngleOrPercentage::Angle(ToComputedValue::from_computed_value(&angle))
+ },
+ }
+ }
+}
+
+/// A `<number>` value.
+pub type Number = CSSFloat;
+
+impl IsParallelTo for (Number, Number, Number) {
+ fn is_parallel_to(&self, vector: &DirectionVector) -> bool {
+ use euclid::approxeq::ApproxEq;
+ // If a and b is parallel, the angle between them is 0deg, so
+ // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0.
+ let self_vector = DirectionVector::new(self.0, self.1, self.2);
+ self_vector
+ .cross(*vector)
+ .square_length()
+ .approx_eq(&0.0f32)
+ }
+}
+
+/// A wrapper of Number, but the value >= 0.
+pub type NonNegativeNumber = NonNegative<CSSFloat>;
+
+impl ToAnimatedValue for NonNegativeNumber {
+ type AnimatedValue = CSSFloat;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.max(0.).into()
+ }
+}
+
+impl From<CSSFloat> for NonNegativeNumber {
+ #[inline]
+ fn from(number: CSSFloat) -> NonNegativeNumber {
+ NonNegative::<CSSFloat>(number)
+ }
+}
+
+impl From<NonNegativeNumber> for CSSFloat {
+ #[inline]
+ fn from(number: NonNegativeNumber) -> CSSFloat {
+ number.0
+ }
+}
+
+impl One for NonNegativeNumber {
+ #[inline]
+ fn one() -> Self {
+ NonNegative(1.0)
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.0 == 1.0
+ }
+}
+
+/// A wrapper of Number, but the value between 0 and 1
+pub type ZeroToOneNumber = ZeroToOne<CSSFloat>;
+
+impl ToAnimatedValue for ZeroToOneNumber {
+ type AnimatedValue = CSSFloat;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Self(animated.max(0.).min(1.))
+ }
+}
+
+impl From<CSSFloat> for ZeroToOneNumber {
+ #[inline]
+ fn from(number: CSSFloat) -> Self {
+ Self(number)
+ }
+}
+
+/// A wrapper of Number, but the value >= 1.
+pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<CSSFloat>;
+
+impl ToAnimatedValue for GreaterThanOrEqualToOneNumber {
+ type AnimatedValue = CSSFloat;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.max(1.).into()
+ }
+}
+
+impl From<CSSFloat> for GreaterThanOrEqualToOneNumber {
+ #[inline]
+ fn from(number: CSSFloat) -> GreaterThanOrEqualToOneNumber {
+ GreaterThanOrEqualToOne::<CSSFloat>(number)
+ }
+}
+
+impl From<GreaterThanOrEqualToOneNumber> for CSSFloat {
+ #[inline]
+ fn from(number: GreaterThanOrEqualToOneNumber) -> CSSFloat {
+ number.0
+ }
+}
+
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[repr(C, u8)]
+pub enum NumberOrPercentage {
+ Percentage(Percentage),
+ Number(Number),
+}
+
+impl NumberOrPercentage {
+ fn clamp_to_non_negative(self) -> Self {
+ match self {
+ NumberOrPercentage::Percentage(p) => {
+ NumberOrPercentage::Percentage(p.clamp_to_non_negative())
+ },
+ NumberOrPercentage::Number(n) => NumberOrPercentage::Number(n.max(0.)),
+ }
+ }
+}
+
+impl ToComputedValue for specified::NumberOrPercentage {
+ type ComputedValue = NumberOrPercentage;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> NumberOrPercentage {
+ match *self {
+ specified::NumberOrPercentage::Percentage(percentage) => {
+ NumberOrPercentage::Percentage(percentage.to_computed_value(context))
+ },
+ specified::NumberOrPercentage::Number(number) => {
+ NumberOrPercentage::Number(number.to_computed_value(context))
+ },
+ }
+ }
+ #[inline]
+ fn from_computed_value(computed: &NumberOrPercentage) -> Self {
+ match *computed {
+ NumberOrPercentage::Percentage(percentage) => {
+ specified::NumberOrPercentage::Percentage(ToComputedValue::from_computed_value(
+ &percentage,
+ ))
+ },
+ NumberOrPercentage::Number(number) => {
+ specified::NumberOrPercentage::Number(ToComputedValue::from_computed_value(&number))
+ },
+ }
+ }
+}
+
+/// A non-negative <number-percentage>.
+pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>;
+
+impl NonNegativeNumberOrPercentage {
+ /// Returns the `100%` value.
+ #[inline]
+ pub fn hundred_percent() -> Self {
+ NonNegative(NumberOrPercentage::Percentage(Percentage::hundred()))
+ }
+}
+
+impl ToAnimatedValue for NonNegativeNumberOrPercentage {
+ type AnimatedValue = NumberOrPercentage;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ NonNegative(animated.clamp_to_non_negative())
+ }
+}
+
+/// A type used for opacity.
+pub type Opacity = CSSFloat;
+
+/// A `<integer>` value.
+pub type Integer = CSSInteger;
+
+/// A wrapper of Integer, but only accept a value >= 1.
+pub type PositiveInteger = GreaterThanOrEqualToOne<CSSInteger>;
+
+impl ToAnimatedValue for PositiveInteger {
+ type AnimatedValue = CSSInteger;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ cmp::max(animated, 1).into()
+ }
+}
+
+impl From<CSSInteger> for PositiveInteger {
+ #[inline]
+ fn from(int: CSSInteger) -> PositiveInteger {
+ GreaterThanOrEqualToOne::<CSSInteger>(int)
+ }
+}
+
+/// rect(...) | auto
+pub type ClipRect = generics::GenericClipRect<LengthOrAuto>;
+
+/// rect(...) | auto
+pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>;
+
+/// The computed value of a grid `<track-breadth>`
+pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>;
+
+/// The computed value of a grid `<track-size>`
+pub type TrackSize = GenericTrackSize<LengthPercentage>;
+
+/// The computed value of a grid `<track-size>+`
+pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>;
+
+/// The computed value of a grid `<track-list>`
+/// (could also be `<auto-track-list>` or `<explicit-track-list>`)
+pub type TrackList = GenericTrackList<LengthPercentage, Integer>;
+
+/// The computed value of a `<grid-line>`.
+pub type GridLine = GenericGridLine<Integer>;
+
+/// `<grid-template-rows> | <grid-template-columns>`
+pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>;
+
+impl ClipRect {
+ /// Given a border box, resolves the clip rect against the border box
+ /// in the same space the border box is in
+ pub fn for_border_rect<T: Copy + From<Length> + Add<Output = T> + Sub<Output = T>, U>(
+ &self,
+ border_box: Rect<T, U>,
+ ) -> Rect<T, U> {
+ fn extract_clip_component<T: From<Length>>(p: &LengthOrAuto, or: T) -> T {
+ match *p {
+ LengthOrAuto::Auto => or,
+ LengthOrAuto::LengthPercentage(ref length) => T::from(*length),
+ }
+ }
+
+ let clip_origin = Point2D::new(
+ From::from(self.left.auto_is(|| Length::new(0.))),
+ From::from(self.top.auto_is(|| Length::new(0.))),
+ );
+ let right = extract_clip_component(&self.right, border_box.size.width);
+ let bottom = extract_clip_component(&self.bottom, border_box.size.height);
+ let clip_size = Size2D::new(right - clip_origin.x, bottom - clip_origin.y);
+
+ Rect::new(clip_origin, clip_size).translate(border_box.origin.to_vector())
+ }
+}
diff --git a/servo/components/style/values/computed/motion.rs b/servo/components/style/values/computed/motion.rs
new file mode 100644
index 0000000000..37b9f4909e
--- /dev/null
+++ b/servo/components/style/values/computed/motion.rs
@@ -0,0 +1,70 @@
+/* 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/. */
+
+//! Computed types for CSS values that are related to motion path.
+
+use crate::values::computed::basic_shape::BasicShape;
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{Angle, LengthPercentage, Position};
+use crate::values::generics::motion::{
+ GenericOffsetPath, GenericOffsetPathFunction, GenericOffsetPosition, GenericRayFunction,
+};
+use crate::Zero;
+
+/// The computed value of ray() function.
+pub type RayFunction = GenericRayFunction<Angle, Position>;
+
+/// The computed value of <offset-path>.
+pub type OffsetPathFunction = GenericOffsetPathFunction<BasicShape, RayFunction, ComputedUrl>;
+
+/// The computed value of `offset-path`.
+pub type OffsetPath = GenericOffsetPath<OffsetPathFunction>;
+
+/// The computed value of `offset-position`.
+pub type OffsetPosition = GenericOffsetPosition<LengthPercentage, LengthPercentage>;
+
+#[inline]
+fn is_auto_zero_angle(auto: &bool, angle: &Angle) -> bool {
+ *auto && angle.is_zero()
+}
+
+/// A computed offset-rotate.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[repr(C)]
+pub struct OffsetRotate {
+ /// If auto is false, this is a fixed angle which indicates a
+ /// constant clockwise rotation transformation applied to it by this
+ /// specified rotation angle. Otherwise, the angle will be added to
+ /// the angle of the direction in layout.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub auto: bool,
+ /// The angle value.
+ #[css(contextual_skip_if = "is_auto_zero_angle")]
+ pub angle: Angle,
+}
+
+impl OffsetRotate {
+ /// Returns "auto 0deg".
+ #[inline]
+ pub fn auto() -> Self {
+ OffsetRotate {
+ auto: true,
+ angle: Zero::zero(),
+ }
+ }
+}
diff --git a/servo/components/style/values/computed/outline.rs b/servo/components/style/values/computed/outline.rs
new file mode 100644
index 0000000000..f872176529
--- /dev/null
+++ b/servo/components/style/values/computed/outline.rs
@@ -0,0 +1,7 @@
+/* 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/. */
+
+//! Computed values for outline properties
+
+pub use crate::values::specified::OutlineStyle;
diff --git a/servo/components/style/values/computed/page.rs b/servo/components/style/values/computed/page.rs
new file mode 100644
index 0000000000..6f71c912cf
--- /dev/null
+++ b/servo/components/style/values/computed/page.rs
@@ -0,0 +1,75 @@
+/* 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/. */
+
+//! Computed @page at-rule properties and named-page style properties
+
+use crate::values::computed::length::NonNegativeLength;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics;
+use crate::values::generics::size::Size2D;
+
+use crate::values::specified::page as specified;
+pub use generics::page::GenericPageSize;
+pub use generics::page::PageOrientation;
+pub use generics::page::PageSizeOrientation;
+pub use generics::page::PaperSize;
+pub use specified::PageName;
+
+/// Computed value of the @page size descriptor
+///
+/// The spec says that the computed value should be the same as the specified
+/// value but with all absolute units, but it's not currently possibly observe
+/// the computed value of page-size.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)]
+#[repr(C, u8)]
+pub enum PageSize {
+ /// Specified size, paper size, or paper size and orientation.
+ Size(Size2D<NonNegativeLength>),
+ /// `landscape` or `portrait` value, no specified size.
+ Orientation(PageSizeOrientation),
+ /// `auto` value
+ Auto,
+}
+
+impl ToComputedValue for specified::PageSize {
+ type ComputedValue = PageSize;
+
+ fn to_computed_value(&self, ctx: &Context) -> Self::ComputedValue {
+ match &*self {
+ Self::Size(s) => PageSize::Size(s.to_computed_value(ctx)),
+ Self::PaperSize(p, PageSizeOrientation::Landscape) => PageSize::Size(Size2D {
+ width: p.long_edge().to_computed_value(ctx),
+ height: p.short_edge().to_computed_value(ctx),
+ }),
+ Self::PaperSize(p, PageSizeOrientation::Portrait) => PageSize::Size(Size2D {
+ width: p.short_edge().to_computed_value(ctx),
+ height: p.long_edge().to_computed_value(ctx),
+ }),
+ Self::Orientation(o) => PageSize::Orientation(*o),
+ Self::Auto => PageSize::Auto,
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match *computed {
+ PageSize::Size(s) => Self::Size(ToComputedValue::from_computed_value(&s)),
+ PageSize::Orientation(o) => Self::Orientation(o),
+ PageSize::Auto => Self::Auto,
+ }
+ }
+}
+
+impl PageSize {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ PageSize::Auto
+ }
+
+ /// Whether this is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, PageSize::Auto)
+ }
+}
diff --git a/servo/components/style/values/computed/percentage.rs b/servo/components/style/values/computed/percentage.rs
new file mode 100644
index 0000000000..994c01594a
--- /dev/null
+++ b/servo/components/style/values/computed/percentage.rs
@@ -0,0 +1,136 @@
+/* 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/. */
+
+//! Computed percentages.
+
+use crate::values::animated::ToAnimatedValue;
+use crate::values::generics::NonNegative;
+use crate::values::specified::percentage::ToPercentage;
+use crate::values::{serialize_normalized_percentage, CSSFloat};
+use crate::Zero;
+use std::fmt;
+use style_traits::{CssWriter, ToCss};
+
+/// A computed percentage.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Default,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct Percentage(pub CSSFloat);
+
+impl Percentage {
+ /// 100%
+ #[inline]
+ pub fn hundred() -> Self {
+ Percentage(1.)
+ }
+
+ /// Returns the absolute value for this percentage.
+ #[inline]
+ pub fn abs(&self) -> Self {
+ Percentage(self.0.abs())
+ }
+
+ /// Clamps this percentage to a non-negative percentage.
+ #[inline]
+ pub fn clamp_to_non_negative(self) -> Self {
+ Percentage(self.0.max(0.))
+ }
+}
+
+impl Zero for Percentage {
+ fn zero() -> Self {
+ Percentage(0.)
+ }
+
+ fn is_zero(&self) -> bool {
+ self.0 == 0.
+ }
+}
+
+impl ToPercentage for Percentage {
+ fn to_percentage(&self) -> CSSFloat {
+ self.0
+ }
+}
+
+impl std::ops::AddAssign for Percentage {
+ fn add_assign(&mut self, other: Self) {
+ self.0 += other.0
+ }
+}
+
+impl std::ops::Add for Percentage {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Percentage(self.0 + other.0)
+ }
+}
+
+impl std::ops::Sub for Percentage {
+ type Output = Self;
+
+ fn sub(self, other: Self) -> Self {
+ Percentage(self.0 - other.0)
+ }
+}
+
+impl std::ops::Rem for Percentage {
+ type Output = Self;
+
+ fn rem(self, other: Self) -> Self {
+ Percentage(self.0 % other.0)
+ }
+}
+
+impl ToCss for Percentage {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ serialize_normalized_percentage(self.0, dest)
+ }
+}
+
+/// A wrapper over a `Percentage`, whose value should be clamped to 0.
+pub type NonNegativePercentage = NonNegative<Percentage>;
+
+impl NonNegativePercentage {
+ /// 100%
+ #[inline]
+ pub fn hundred() -> Self {
+ NonNegative(Percentage::hundred())
+ }
+}
+
+impl ToAnimatedValue for NonNegativePercentage {
+ type AnimatedValue = Percentage;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ NonNegative(animated.clamp_to_non_negative())
+ }
+}
diff --git a/servo/components/style/values/computed/position.rs b/servo/components/style/values/computed/position.rs
new file mode 100644
index 0000000000..5a10c0f23d
--- /dev/null
+++ b/servo/components/style/values/computed/position.rs
@@ -0,0 +1,74 @@
+/* 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/. */
+
+//! CSS handling for the computed value of
+//! [`position`][position] values.
+//!
+//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position
+
+use crate::values::computed::{Integer, LengthPercentage, NonNegativeNumber, Percentage};
+use crate::values::generics::position::AspectRatio as GenericAspectRatio;
+use crate::values::generics::position::Position as GenericPosition;
+use crate::values::generics::position::PositionComponent as GenericPositionComponent;
+use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto;
+use crate::values::generics::position::ZIndex as GenericZIndex;
+pub use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas, MasonryAutoFlow};
+use crate::Zero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// The computed value of a CSS `<position>`
+pub type Position = GenericPosition<HorizontalPosition, VerticalPosition>;
+
+/// The computed value of an `auto | <position>`
+pub type PositionOrAuto = GenericPositionOrAuto<Position>;
+
+/// The computed value of a CSS horizontal position.
+pub type HorizontalPosition = LengthPercentage;
+
+/// The computed value of a CSS vertical position.
+pub type VerticalPosition = LengthPercentage;
+
+impl Position {
+ /// `50% 50%`
+ #[inline]
+ pub fn center() -> Self {
+ Self::new(
+ LengthPercentage::new_percent(Percentage(0.5)),
+ LengthPercentage::new_percent(Percentage(0.5)),
+ )
+ }
+
+ /// `0% 0%`
+ #[inline]
+ pub fn zero() -> Self {
+ Self::new(LengthPercentage::zero(), LengthPercentage::zero())
+ }
+}
+
+impl ToCss for Position {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.horizontal.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.vertical.to_css(dest)
+ }
+}
+
+impl GenericPositionComponent for LengthPercentage {
+ fn is_center(&self) -> bool {
+ match self.to_percentage() {
+ Some(Percentage(per)) => per == 0.5,
+ _ => false,
+ }
+ }
+}
+
+/// A computed value for the `z-index` property.
+pub type ZIndex = GenericZIndex<Integer>;
+
+/// A computed value for the `aspect-ratio` property.
+pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>;
diff --git a/servo/components/style/values/computed/ratio.rs b/servo/components/style/values/computed/ratio.rs
new file mode 100644
index 0000000000..ae8997cfc0
--- /dev/null
+++ b/servo/components/style/values/computed/ratio.rs
@@ -0,0 +1,115 @@
+/* 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/. */
+
+//! `<ratio>` computed values.
+
+use crate::values::animated::{Animate, Procedure};
+use crate::values::computed::NonNegativeNumber;
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::ratio::Ratio as GenericRatio;
+use crate::{One, Zero};
+use std::cmp::{Ordering, PartialOrd};
+
+/// A computed <ratio> value.
+pub type Ratio = GenericRatio<NonNegativeNumber>;
+
+impl PartialOrd for Ratio {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ f64::partial_cmp(
+ &((self.0).0 as f64 * (other.1).0 as f64),
+ &((self.1).0 as f64 * (other.0).0 as f64),
+ )
+ }
+}
+
+/// https://drafts.csswg.org/css-values/#combine-ratio
+impl Animate for Ratio {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ // If either <ratio> is degenerate, the values cannot be interpolated.
+ if self.is_degenerate() || other.is_degenerate() {
+ return Err(());
+ }
+
+ // Addition of <ratio>s is not possible, and based on
+ // https://drafts.csswg.org/css-values-4/#not-additive,
+ // we simply use the first value as the result value.
+ // Besides, the procedure for accumulation should be identical to addition here.
+ if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) {
+ return Ok(self.clone());
+ }
+
+ // The interpolation of a <ratio> is defined by converting each <ratio> to a number by
+ // dividing the first value by the second (so a ratio of 3 / 2 would become 1.5), taking
+ // the logarithm of that result (so the 1.5 would become approximately 0.176), then
+ // interpolating those values.
+ //
+ // The result during the interpolation is converted back to a <ratio> by inverting the
+ // logarithm, then interpreting the result as a <ratio> with the result as the first value
+ // and 1 as the second value.
+ let start = self.to_f32().ln();
+ let end = other.to_f32().ln();
+ let e = std::f32::consts::E;
+ let result = e.powf(start.animate(&end, procedure)?);
+ // The range of the result is [0, inf), based on the easing function.
+ if result.is_zero() || result.is_infinite() {
+ return Err(());
+ }
+ Ok(Ratio::new(result, 1.0f32))
+ }
+}
+
+impl ComputeSquaredDistance for Ratio {
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ if self.is_degenerate() || other.is_degenerate() {
+ return Err(());
+ }
+ // Use the distance of their logarithm values. (This is used by testing, so don't need to
+ // care about the base. Here we use the same base as that in animate().)
+ self.to_f32()
+ .ln()
+ .compute_squared_distance(&other.to_f32().ln())
+ }
+}
+
+impl Zero for Ratio {
+ fn zero() -> Self {
+ Self::new(Zero::zero(), One::one())
+ }
+
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+impl Ratio {
+ /// Returns a new Ratio.
+ #[inline]
+ pub fn new(a: f32, b: f32) -> Self {
+ GenericRatio(a.into(), b.into())
+ }
+
+ /// Returns the used value. A ratio of 0/0 behaves as the ratio 1/0.
+ /// https://drafts.csswg.org/css-values-4/#ratios
+ pub fn used_value(self) -> Self {
+ if self.0.is_zero() && self.1.is_zero() {
+ Ratio::new(One::one(), Zero::zero())
+ } else {
+ self
+ }
+ }
+
+ /// Returns true if this is a degenerate ratio.
+ /// https://drafts.csswg.org/css-values/#degenerate-ratio
+ #[inline]
+ pub fn is_degenerate(&self) -> bool {
+ self.0.is_zero() || self.1.is_zero()
+ }
+
+ /// Returns the f32 value by dividing the first value by the second one.
+ #[inline]
+ fn to_f32(&self) -> f32 {
+ debug_assert!(!self.is_degenerate());
+ (self.0).0 / (self.1).0
+ }
+}
diff --git a/servo/components/style/values/computed/rect.rs b/servo/components/style/values/computed/rect.rs
new file mode 100644
index 0000000000..ec44360fc8
--- /dev/null
+++ b/servo/components/style/values/computed/rect.rs
@@ -0,0 +1,11 @@
+/* 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/. */
+
+//! Computed types for CSS borders.
+
+use crate::values::computed::length::NonNegativeLengthOrNumber;
+use crate::values::generics::rect::Rect;
+
+/// A specified rectangle made of four `<length-or-number>` values.
+pub type NonNegativeLengthOrNumberRect = Rect<NonNegativeLengthOrNumber>;
diff --git a/servo/components/style/values/computed/resolution.rs b/servo/components/style/values/computed/resolution.rs
new file mode 100644
index 0000000000..430c80d211
--- /dev/null
+++ b/servo/components/style/values/computed/resolution.rs
@@ -0,0 +1,56 @@
+/* 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/. */
+
+//! Resolution values:
+//!
+//! https://drafts.csswg.org/css-values/#resolution
+
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::specified;
+use crate::values::CSSFloat;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A computed `<resolution>`.
+#[repr(C)]
+#[derive(Animate, Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem)]
+pub struct Resolution(CSSFloat);
+
+impl Resolution {
+ /// Returns this resolution value as dppx.
+ #[inline]
+ pub fn dppx(&self) -> CSSFloat {
+ self.0
+ }
+
+ /// Return a computed `resolution` value from a dppx float value.
+ #[inline]
+ pub fn from_dppx(dppx: CSSFloat) -> Self {
+ Resolution(dppx)
+ }
+}
+
+impl ToComputedValue for specified::Resolution {
+ type ComputedValue = Resolution;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ Resolution(crate::values::normalize(self.dppx().max(0.0)))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ specified::Resolution::from_dppx(computed.dppx())
+ }
+}
+
+impl ToCss for Resolution {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.dppx().to_css(dest)?;
+ dest.write_str("dppx")
+ }
+}
diff --git a/servo/components/style/values/computed/svg.rs b/servo/components/style/values/computed/svg.rs
new file mode 100644
index 0000000000..6efdfca36b
--- /dev/null
+++ b/servo/components/style/values/computed/svg.rs
@@ -0,0 +1,66 @@
+/* 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/. */
+
+//! Computed types for SVG properties.
+
+use crate::values::computed::color::Color;
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{LengthPercentage, NonNegativeLengthPercentage, Opacity};
+use crate::values::generics::svg as generic;
+use crate::Zero;
+
+pub use crate::values::specified::{DProperty, MozContextProperties, SVGPaintOrder};
+
+/// Computed SVG Paint value
+pub type SVGPaint = generic::GenericSVGPaint<Color, ComputedUrl>;
+
+/// Computed SVG Paint Kind value
+pub type SVGPaintKind = generic::GenericSVGPaintKind<Color, ComputedUrl>;
+
+impl SVGPaint {
+ /// Opaque black color
+ pub const BLACK: Self = Self {
+ kind: generic::SVGPaintKind::Color(Color::BLACK),
+ fallback: generic::SVGPaintFallback::Unset,
+ };
+}
+
+/// <length> | <percentage> | <number> | context-value
+pub type SVGLength = generic::GenericSVGLength<LengthPercentage>;
+
+impl SVGLength {
+ /// `0px`
+ pub fn zero() -> Self {
+ generic::SVGLength::LengthPercentage(LengthPercentage::zero())
+ }
+}
+
+/// An non-negative wrapper of SVGLength.
+pub type SVGWidth = generic::GenericSVGLength<NonNegativeLengthPercentage>;
+
+impl SVGWidth {
+ /// `1px`.
+ pub fn one() -> Self {
+ use crate::values::generics::NonNegative;
+ generic::SVGLength::LengthPercentage(NonNegative(LengthPercentage::one()))
+ }
+}
+
+/// [ <length> | <percentage> | <number> ]# | context-value
+pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray<NonNegativeLengthPercentage>;
+
+impl Default for SVGStrokeDashArray {
+ fn default() -> Self {
+ generic::SVGStrokeDashArray::Values(Default::default())
+ }
+}
+
+/// <opacity-value> | context-fill-opacity | context-stroke-opacity
+pub type SVGOpacity = generic::GenericSVGOpacity<Opacity>;
+
+impl Default for SVGOpacity {
+ fn default() -> Self {
+ generic::SVGOpacity::Opacity(1.)
+ }
+}
diff --git a/servo/components/style/values/computed/table.rs b/servo/components/style/values/computed/table.rs
new file mode 100644
index 0000000000..47109e20ec
--- /dev/null
+++ b/servo/components/style/values/computed/table.rs
@@ -0,0 +1,7 @@
+/* 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/. */
+
+//! Computed types for CSS values related to tables.
+
+pub use super::specified::table::CaptionSide;
diff --git a/servo/components/style/values/computed/text.rs b/servo/components/style/values/computed/text.rs
new file mode 100644
index 0000000000..a4fec654a5
--- /dev/null
+++ b/servo/components/style/values/computed/text.rs
@@ -0,0 +1,228 @@
+/* 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/. */
+
+//! Computed types for text properties.
+
+#[cfg(feature = "servo")]
+use crate::properties::StyleBuilder;
+use crate::values::computed::length::{Length, LengthPercentage};
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::text::InitialLetter as GenericInitialLetter;
+use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing};
+use crate::values::specified::text::{self as specified, TextOverflowSide};
+use crate::values::specified::text::{TextEmphasisFillMode, TextEmphasisShapeKeyword};
+use crate::values::{CSSFloat, CSSInteger};
+use crate::Zero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+pub use crate::values::specified::text::{
+ MozControlCharacterVisibility, TextAlignLast, TextUnderlinePosition,
+};
+pub use crate::values::specified::HyphenateCharacter;
+pub use crate::values::specified::{LineBreak, OverflowWrap, RubyPosition, WordBreak};
+pub use crate::values::specified::{TextDecorationLine, TextEmphasisPosition};
+pub use crate::values::specified::{TextDecorationSkipInk, TextJustify, TextTransform};
+
+/// A computed value for the `initial-letter` property.
+pub type InitialLetter = GenericInitialLetter<CSSFloat, CSSInteger>;
+
+/// Implements type for `text-decoration-thickness` property.
+pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>;
+
+/// The computed value of `text-align`.
+pub type TextAlign = specified::TextAlignKeyword;
+
+/// The computed value of `text-indent`.
+pub type TextIndent = GenericTextIndent<LengthPercentage>;
+
+/// A computed value for the `letter-spacing` property.
+#[repr(transparent)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToResolvedValue,
+)]
+pub struct LetterSpacing(pub Length);
+
+impl LetterSpacing {
+ /// Return the `normal` computed value, which is just zero.
+ #[inline]
+ pub fn normal() -> Self {
+ LetterSpacing(Length::zero())
+ }
+}
+
+impl ToCss for LetterSpacing {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ // https://drafts.csswg.org/css-text/#propdef-letter-spacing
+ //
+ // For legacy reasons, a computed letter-spacing of zero yields a
+ // resolved value (getComputedStyle() return value) of normal.
+ if self.0.is_zero() {
+ return dest.write_str("normal");
+ }
+ self.0.to_css(dest)
+ }
+}
+
+impl ToComputedValue for specified::LetterSpacing {
+ type ComputedValue = LetterSpacing;
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ Spacing::Normal => LetterSpacing(Length::zero()),
+ Spacing::Value(ref v) => LetterSpacing(v.to_computed_value(context)),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ if computed.0.is_zero() {
+ return Spacing::Normal;
+ }
+ Spacing::Value(ToComputedValue::from_computed_value(&computed.0))
+ }
+}
+
+/// A computed value for the `word-spacing` property.
+pub type WordSpacing = LengthPercentage;
+
+impl ToComputedValue for specified::WordSpacing {
+ type ComputedValue = WordSpacing;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ Spacing::Normal => LengthPercentage::zero(),
+ Spacing::Value(ref v) => v.to_computed_value(context),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Spacing::Value(ToComputedValue::from_computed_value(computed))
+ }
+}
+
+impl WordSpacing {
+ /// Return the `normal` computed value, which is just zero.
+ #[inline]
+ pub fn normal() -> Self {
+ LengthPercentage::zero()
+ }
+}
+
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue)]
+#[repr(C)]
+/// text-overflow.
+/// When the specified value only has one side, that's the "second"
+/// side, and the sides are logical, so "second" means "end". The
+/// start side is Clip in that case.
+///
+/// When the specified value has two sides, those are our "first"
+/// and "second" sides, and they are physical sides ("left" and
+/// "right").
+pub struct TextOverflow {
+ /// First side
+ pub first: TextOverflowSide,
+ /// Second side
+ pub second: TextOverflowSide,
+ /// True if the specified value only has one side.
+ pub sides_are_logical: bool,
+}
+
+impl TextOverflow {
+ /// Returns the initial `text-overflow` value
+ pub fn get_initial_value() -> TextOverflow {
+ TextOverflow {
+ first: TextOverflowSide::Clip,
+ second: TextOverflowSide::Clip,
+ sides_are_logical: true,
+ }
+ }
+}
+
+impl ToCss for TextOverflow {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.sides_are_logical {
+ debug_assert_eq!(self.first, TextOverflowSide::Clip);
+ self.second.to_css(dest)?;
+ } else {
+ self.first.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.second.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// A struct that represents the _used_ value of the text-decoration property.
+///
+/// FIXME(emilio): This is done at style resolution time, though probably should
+/// be done at layout time, otherwise we need to account for display: contents
+/// and similar stuff when we implement it.
+///
+/// FIXME(emilio): Also, should be just a bitfield instead of three bytes.
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToResolvedValue)]
+pub struct TextDecorationsInEffect {
+ /// Whether an underline is in effect.
+ pub underline: bool,
+ /// Whether an overline decoration is in effect.
+ pub overline: bool,
+ /// Whether a line-through style is in effect.
+ pub line_through: bool,
+}
+
+impl TextDecorationsInEffect {
+ /// Computes the text-decorations in effect for a given style.
+ #[cfg(feature = "servo")]
+ pub fn from_style(style: &StyleBuilder) -> Self {
+ // Start with no declarations if this is an atomic inline-level box;
+ // otherwise, start with the declarations in effect and add in the text
+ // decorations that this block specifies.
+ let mut result = if style.get_box().clone_display().is_atomic_inline_level() {
+ Self::default()
+ } else {
+ style
+ .get_parent_inherited_text()
+ .text_decorations_in_effect
+ .clone()
+ };
+
+ let line = style.get_text().clone_text_decoration_line();
+
+ result.underline |= line.contains(TextDecorationLine::UNDERLINE);
+ result.overline |= line.contains(TextDecorationLine::OVERLINE);
+ result.line_through |= line.contains(TextDecorationLine::LINE_THROUGH);
+
+ result
+ }
+}
+
+/// Computed value for the text-emphasis-style property
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue)]
+#[allow(missing_docs)]
+#[repr(C, u8)]
+pub enum TextEmphasisStyle {
+ /// [ <fill> || <shape> ]
+ Keyword {
+ #[css(skip_if = "TextEmphasisFillMode::is_filled")]
+ fill: TextEmphasisFillMode,
+ shape: TextEmphasisShapeKeyword,
+ },
+ /// `none`
+ None,
+ /// `<string>` (of which only the first grapheme cluster will be used).
+ String(crate::OwnedStr),
+}
diff --git a/servo/components/style/values/computed/time.rs b/servo/components/style/values/computed/time.rs
new file mode 100644
index 0000000000..b81c6e879a
--- /dev/null
+++ b/servo/components/style/values/computed/time.rs
@@ -0,0 +1,45 @@
+/* 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/. */
+
+//! Computed time values.
+
+use crate::values::CSSFloat;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A computed `<time>` value.
+#[derive(Animate, Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToResolvedValue)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(C)]
+pub struct Time {
+ seconds: CSSFloat,
+}
+
+impl Time {
+ /// Creates a time value from a seconds amount.
+ pub fn from_seconds(seconds: CSSFloat) -> Self {
+ Time { seconds }
+ }
+
+ /// Returns `0s`.
+ pub fn zero() -> Self {
+ Self::from_seconds(0.0)
+ }
+
+ /// Returns the amount of seconds this time represents.
+ #[inline]
+ pub fn seconds(&self) -> CSSFloat {
+ self.seconds
+ }
+}
+
+impl ToCss for Time {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.seconds().to_css(dest)?;
+ dest.write_char('s')
+ }
+}
diff --git a/servo/components/style/values/computed/transform.rs b/servo/components/style/values/computed/transform.rs
new file mode 100644
index 0000000000..b1a617fd4b
--- /dev/null
+++ b/servo/components/style/values/computed/transform.rs
@@ -0,0 +1,559 @@
+/* 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/. */
+
+//! Computed types for CSS values that are related to transformations.
+
+use super::CSSFloat;
+use crate::values::animated::transform::{Perspective, Scale3D, Translate3D};
+use crate::values::animated::ToAnimatedZero;
+use crate::values::computed::{Angle, Integer, Length, LengthPercentage, Number, Percentage};
+use crate::values::generics::transform as generic;
+use crate::Zero;
+use euclid::default::{Transform3D, Vector3D};
+
+pub use crate::values::generics::transform::TransformStyle;
+pub use crate::values::specified::transform::TransformBox;
+
+/// A single operation in a computed CSS `transform`
+pub type TransformOperation =
+ generic::GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>;
+/// A computed CSS `transform`
+pub type Transform = generic::GenericTransform<TransformOperation>;
+
+/// The computed value of a CSS `<transform-origin>`
+pub type TransformOrigin =
+ generic::GenericTransformOrigin<LengthPercentage, LengthPercentage, Length>;
+
+/// The computed value of the `perspective()` transform function.
+pub type PerspectiveFunction = generic::PerspectiveFunction<Length>;
+
+/// A vector to represent the direction vector (rotate axis) for Rotate3D.
+pub type DirectionVector = Vector3D<CSSFloat>;
+
+impl TransformOrigin {
+ /// Returns the initial computed value for `transform-origin`.
+ #[inline]
+ pub fn initial_value() -> Self {
+ Self::new(
+ LengthPercentage::new_percent(Percentage(0.5)),
+ LengthPercentage::new_percent(Percentage(0.5)),
+ Length::new(0.),
+ )
+ }
+}
+
+/// computed value of matrix3d()
+pub type Matrix3D = generic::Matrix3D<Number>;
+
+/// computed value of matrix()
+pub type Matrix = generic::Matrix<Number>;
+
+// we rustfmt_skip here because we want the matrices to look like
+// matrices instead of being split across lines
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl Matrix3D {
+ /// Get an identity matrix
+ #[inline]
+ pub fn identity() -> Self {
+ Self {
+ m11: 1.0, m12: 0.0, m13: 0.0, m14: 0.0,
+ m21: 0.0, m22: 1.0, m23: 0.0, m24: 0.0,
+ m31: 0.0, m32: 0.0, m33: 1.0, m34: 0.0,
+ m41: 0., m42: 0., m43: 0., m44: 1.0
+ }
+ }
+
+ /// Convert to a 2D Matrix
+ #[inline]
+ pub fn into_2d(self) -> Result<Matrix, ()> {
+ if self.m13 == 0. && self.m23 == 0. &&
+ self.m31 == 0. && self.m32 == 0. &&
+ self.m33 == 1. && self.m34 == 0. &&
+ self.m14 == 0. && self.m24 == 0. &&
+ self.m43 == 0. && self.m44 == 1. {
+ Ok(Matrix {
+ a: self.m11, c: self.m21, e: self.m41,
+ b: self.m12, d: self.m22, f: self.m42,
+ })
+ } else {
+ Err(())
+ }
+ }
+
+ /// Return true if this has 3D components.
+ #[inline]
+ pub fn is_3d(&self) -> bool {
+ self.m13 != 0.0 || self.m14 != 0.0 ||
+ self.m23 != 0.0 || self.m24 != 0.0 ||
+ self.m31 != 0.0 || self.m32 != 0.0 ||
+ self.m33 != 1.0 || self.m34 != 0.0 ||
+ self.m43 != 0.0 || self.m44 != 1.0
+ }
+
+ /// Return determinant value.
+ #[inline]
+ pub fn determinant(&self) -> CSSFloat {
+ self.m14 * self.m23 * self.m32 * self.m41 -
+ self.m13 * self.m24 * self.m32 * self.m41 -
+ self.m14 * self.m22 * self.m33 * self.m41 +
+ self.m12 * self.m24 * self.m33 * self.m41 +
+ self.m13 * self.m22 * self.m34 * self.m41 -
+ self.m12 * self.m23 * self.m34 * self.m41 -
+ self.m14 * self.m23 * self.m31 * self.m42 +
+ self.m13 * self.m24 * self.m31 * self.m42 +
+ self.m14 * self.m21 * self.m33 * self.m42 -
+ self.m11 * self.m24 * self.m33 * self.m42 -
+ self.m13 * self.m21 * self.m34 * self.m42 +
+ self.m11 * self.m23 * self.m34 * self.m42 +
+ self.m14 * self.m22 * self.m31 * self.m43 -
+ self.m12 * self.m24 * self.m31 * self.m43 -
+ self.m14 * self.m21 * self.m32 * self.m43 +
+ self.m11 * self.m24 * self.m32 * self.m43 +
+ self.m12 * self.m21 * self.m34 * self.m43 -
+ self.m11 * self.m22 * self.m34 * self.m43 -
+ self.m13 * self.m22 * self.m31 * self.m44 +
+ self.m12 * self.m23 * self.m31 * self.m44 +
+ self.m13 * self.m21 * self.m32 * self.m44 -
+ self.m11 * self.m23 * self.m32 * self.m44 -
+ self.m12 * self.m21 * self.m33 * self.m44 +
+ self.m11 * self.m22 * self.m33 * self.m44
+ }
+
+ /// Transpose a matrix.
+ #[inline]
+ pub fn transpose(&self) -> Self {
+ Self {
+ m11: self.m11, m12: self.m21, m13: self.m31, m14: self.m41,
+ m21: self.m12, m22: self.m22, m23: self.m32, m24: self.m42,
+ m31: self.m13, m32: self.m23, m33: self.m33, m34: self.m43,
+ m41: self.m14, m42: self.m24, m43: self.m34, m44: self.m44,
+ }
+ }
+
+ /// Return inverse matrix.
+ pub fn inverse(&self) -> Result<Matrix3D, ()> {
+ let mut det = self.determinant();
+
+ if det == 0.0 {
+ return Err(());
+ }
+
+ det = 1.0 / det;
+ let x = Matrix3D {
+ m11: det *
+ (self.m23 * self.m34 * self.m42 - self.m24 * self.m33 * self.m42 +
+ self.m24 * self.m32 * self.m43 - self.m22 * self.m34 * self.m43 -
+ self.m23 * self.m32 * self.m44 + self.m22 * self.m33 * self.m44),
+ m12: det *
+ (self.m14 * self.m33 * self.m42 - self.m13 * self.m34 * self.m42 -
+ self.m14 * self.m32 * self.m43 + self.m12 * self.m34 * self.m43 +
+ self.m13 * self.m32 * self.m44 - self.m12 * self.m33 * self.m44),
+ m13: det *
+ (self.m13 * self.m24 * self.m42 - self.m14 * self.m23 * self.m42 +
+ self.m14 * self.m22 * self.m43 - self.m12 * self.m24 * self.m43 -
+ self.m13 * self.m22 * self.m44 + self.m12 * self.m23 * self.m44),
+ m14: det *
+ (self.m14 * self.m23 * self.m32 - self.m13 * self.m24 * self.m32 -
+ self.m14 * self.m22 * self.m33 + self.m12 * self.m24 * self.m33 +
+ self.m13 * self.m22 * self.m34 - self.m12 * self.m23 * self.m34),
+ m21: det *
+ (self.m24 * self.m33 * self.m41 - self.m23 * self.m34 * self.m41 -
+ self.m24 * self.m31 * self.m43 + self.m21 * self.m34 * self.m43 +
+ self.m23 * self.m31 * self.m44 - self.m21 * self.m33 * self.m44),
+ m22: det *
+ (self.m13 * self.m34 * self.m41 - self.m14 * self.m33 * self.m41 +
+ self.m14 * self.m31 * self.m43 - self.m11 * self.m34 * self.m43 -
+ self.m13 * self.m31 * self.m44 + self.m11 * self.m33 * self.m44),
+ m23: det *
+ (self.m14 * self.m23 * self.m41 - self.m13 * self.m24 * self.m41 -
+ self.m14 * self.m21 * self.m43 + self.m11 * self.m24 * self.m43 +
+ self.m13 * self.m21 * self.m44 - self.m11 * self.m23 * self.m44),
+ m24: det *
+ (self.m13 * self.m24 * self.m31 - self.m14 * self.m23 * self.m31 +
+ self.m14 * self.m21 * self.m33 - self.m11 * self.m24 * self.m33 -
+ self.m13 * self.m21 * self.m34 + self.m11 * self.m23 * self.m34),
+ m31: det *
+ (self.m22 * self.m34 * self.m41 - self.m24 * self.m32 * self.m41 +
+ self.m24 * self.m31 * self.m42 - self.m21 * self.m34 * self.m42 -
+ self.m22 * self.m31 * self.m44 + self.m21 * self.m32 * self.m44),
+ m32: det *
+ (self.m14 * self.m32 * self.m41 - self.m12 * self.m34 * self.m41 -
+ self.m14 * self.m31 * self.m42 + self.m11 * self.m34 * self.m42 +
+ self.m12 * self.m31 * self.m44 - self.m11 * self.m32 * self.m44),
+ m33: det *
+ (self.m12 * self.m24 * self.m41 - self.m14 * self.m22 * self.m41 +
+ self.m14 * self.m21 * self.m42 - self.m11 * self.m24 * self.m42 -
+ self.m12 * self.m21 * self.m44 + self.m11 * self.m22 * self.m44),
+ m34: det *
+ (self.m14 * self.m22 * self.m31 - self.m12 * self.m24 * self.m31 -
+ self.m14 * self.m21 * self.m32 + self.m11 * self.m24 * self.m32 +
+ self.m12 * self.m21 * self.m34 - self.m11 * self.m22 * self.m34),
+ m41: det *
+ (self.m23 * self.m32 * self.m41 - self.m22 * self.m33 * self.m41 -
+ self.m23 * self.m31 * self.m42 + self.m21 * self.m33 * self.m42 +
+ self.m22 * self.m31 * self.m43 - self.m21 * self.m32 * self.m43),
+ m42: det *
+ (self.m12 * self.m33 * self.m41 - self.m13 * self.m32 * self.m41 +
+ self.m13 * self.m31 * self.m42 - self.m11 * self.m33 * self.m42 -
+ self.m12 * self.m31 * self.m43 + self.m11 * self.m32 * self.m43),
+ m43: det *
+ (self.m13 * self.m22 * self.m41 - self.m12 * self.m23 * self.m41 -
+ self.m13 * self.m21 * self.m42 + self.m11 * self.m23 * self.m42 +
+ self.m12 * self.m21 * self.m43 - self.m11 * self.m22 * self.m43),
+ m44: det *
+ (self.m12 * self.m23 * self.m31 - self.m13 * self.m22 * self.m31 +
+ self.m13 * self.m21 * self.m32 - self.m11 * self.m23 * self.m32 -
+ self.m12 * self.m21 * self.m33 + self.m11 * self.m22 * self.m33),
+ };
+
+ Ok(x)
+ }
+
+ /// Multiply `pin * self`.
+ #[inline]
+ pub fn pre_mul_point4(&self, pin: &[f32; 4]) -> [f32; 4] {
+ [
+ pin[0] * self.m11 + pin[1] * self.m21 + pin[2] * self.m31 + pin[3] * self.m41,
+ pin[0] * self.m12 + pin[1] * self.m22 + pin[2] * self.m32 + pin[3] * self.m42,
+ pin[0] * self.m13 + pin[1] * self.m23 + pin[2] * self.m33 + pin[3] * self.m43,
+ pin[0] * self.m14 + pin[1] * self.m24 + pin[2] * self.m34 + pin[3] * self.m44,
+ ]
+ }
+
+ /// Return the multiplication of two 4x4 matrices.
+ #[inline]
+ pub fn multiply(&self, other: &Self) -> Self {
+ Matrix3D {
+ m11: self.m11 * other.m11 + self.m12 * other.m21 +
+ self.m13 * other.m31 + self.m14 * other.m41,
+ m12: self.m11 * other.m12 + self.m12 * other.m22 +
+ self.m13 * other.m32 + self.m14 * other.m42,
+ m13: self.m11 * other.m13 + self.m12 * other.m23 +
+ self.m13 * other.m33 + self.m14 * other.m43,
+ m14: self.m11 * other.m14 + self.m12 * other.m24 +
+ self.m13 * other.m34 + self.m14 * other.m44,
+ m21: self.m21 * other.m11 + self.m22 * other.m21 +
+ self.m23 * other.m31 + self.m24 * other.m41,
+ m22: self.m21 * other.m12 + self.m22 * other.m22 +
+ self.m23 * other.m32 + self.m24 * other.m42,
+ m23: self.m21 * other.m13 + self.m22 * other.m23 +
+ self.m23 * other.m33 + self.m24 * other.m43,
+ m24: self.m21 * other.m14 + self.m22 * other.m24 +
+ self.m23 * other.m34 + self.m24 * other.m44,
+ m31: self.m31 * other.m11 + self.m32 * other.m21 +
+ self.m33 * other.m31 + self.m34 * other.m41,
+ m32: self.m31 * other.m12 + self.m32 * other.m22 +
+ self.m33 * other.m32 + self.m34 * other.m42,
+ m33: self.m31 * other.m13 + self.m32 * other.m23 +
+ self.m33 * other.m33 + self.m34 * other.m43,
+ m34: self.m31 * other.m14 + self.m32 * other.m24 +
+ self.m33 * other.m34 + self.m34 * other.m44,
+ m41: self.m41 * other.m11 + self.m42 * other.m21 +
+ self.m43 * other.m31 + self.m44 * other.m41,
+ m42: self.m41 * other.m12 + self.m42 * other.m22 +
+ self.m43 * other.m32 + self.m44 * other.m42,
+ m43: self.m41 * other.m13 + self.m42 * other.m23 +
+ self.m43 * other.m33 + self.m44 * other.m43,
+ m44: self.m41 * other.m14 + self.m42 * other.m24 +
+ self.m43 * other.m34 + self.m44 * other.m44,
+ }
+ }
+
+ /// Scale the matrix by a factor.
+ #[inline]
+ pub fn scale_by_factor(&mut self, scaling_factor: CSSFloat) {
+ self.m11 *= scaling_factor;
+ self.m12 *= scaling_factor;
+ self.m13 *= scaling_factor;
+ self.m14 *= scaling_factor;
+ self.m21 *= scaling_factor;
+ self.m22 *= scaling_factor;
+ self.m23 *= scaling_factor;
+ self.m24 *= scaling_factor;
+ self.m31 *= scaling_factor;
+ self.m32 *= scaling_factor;
+ self.m33 *= scaling_factor;
+ self.m34 *= scaling_factor;
+ self.m41 *= scaling_factor;
+ self.m42 *= scaling_factor;
+ self.m43 *= scaling_factor;
+ self.m44 *= scaling_factor;
+ }
+
+ /// Return the matrix 3x3 part (top-left corner).
+ /// This is used by retrieving the scale and shear factors
+ /// during decomposing a 3d matrix.
+ #[inline]
+ pub fn get_matrix_3x3_part(&self) -> [[f32; 3]; 3] {
+ [
+ [ self.m11, self.m12, self.m13 ],
+ [ self.m21, self.m22, self.m23 ],
+ [ self.m31, self.m32, self.m33 ],
+ ]
+ }
+
+ /// Set perspective on the matrix.
+ #[inline]
+ pub fn set_perspective(&mut self, perspective: &Perspective) {
+ self.m14 = perspective.0;
+ self.m24 = perspective.1;
+ self.m34 = perspective.2;
+ self.m44 = perspective.3;
+ }
+
+ /// Apply translate on the matrix.
+ #[inline]
+ pub fn apply_translate(&mut self, translate: &Translate3D) {
+ self.m41 += translate.0 * self.m11 + translate.1 * self.m21 + translate.2 * self.m31;
+ self.m42 += translate.0 * self.m12 + translate.1 * self.m22 + translate.2 * self.m32;
+ self.m43 += translate.0 * self.m13 + translate.1 * self.m23 + translate.2 * self.m33;
+ self.m44 += translate.0 * self.m14 + translate.1 * self.m24 + translate.2 * self.m34;
+ }
+
+ /// Apply scale on the matrix.
+ #[inline]
+ pub fn apply_scale(&mut self, scale: &Scale3D) {
+ self.m11 *= scale.0;
+ self.m12 *= scale.0;
+ self.m13 *= scale.0;
+ self.m14 *= scale.0;
+ self.m21 *= scale.1;
+ self.m22 *= scale.1;
+ self.m23 *= scale.1;
+ self.m24 *= scale.1;
+ self.m31 *= scale.2;
+ self.m32 *= scale.2;
+ self.m33 *= scale.2;
+ self.m34 *= scale.2;
+ }
+}
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl Matrix {
+ #[inline]
+ /// Get an identity matrix
+ pub fn identity() -> Self {
+ Self {
+ a: 1., c: 0., /* 0 0*/
+ b: 0., d: 1., /* 0 0*/
+ /* 0 0 1 0 */
+ e: 0., f: 0., /* 0 1 */
+ }
+ }
+}
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl From<Matrix> for Matrix3D {
+ fn from(m: Matrix) -> Self {
+ Self {
+ m11: m.a, m12: m.b, m13: 0.0, m14: 0.0,
+ m21: m.c, m22: m.d, m23: 0.0, m24: 0.0,
+ m31: 0.0, m32: 0.0, m33: 1.0, m34: 0.0,
+ m41: m.e, m42: m.f, m43: 0.0, m44: 1.0
+ }
+ }
+}
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl From<Transform3D<CSSFloat>> for Matrix3D {
+ #[inline]
+ fn from(m: Transform3D<CSSFloat>) -> Self {
+ Matrix3D {
+ m11: m.m11, m12: m.m12, m13: m.m13, m14: m.m14,
+ m21: m.m21, m22: m.m22, m23: m.m23, m24: m.m24,
+ m31: m.m31, m32: m.m32, m33: m.m33, m34: m.m34,
+ m41: m.m41, m42: m.m42, m43: m.m43, m44: m.m44
+ }
+ }
+}
+
+impl TransformOperation {
+ /// Convert to a Translate3D.
+ ///
+ /// Must be called on a Translate function
+ pub fn to_translate_3d(&self) -> Self {
+ match *self {
+ generic::TransformOperation::Translate3D(..) => self.clone(),
+ generic::TransformOperation::TranslateX(ref x) => {
+ generic::TransformOperation::Translate3D(
+ x.clone(),
+ LengthPercentage::zero(),
+ Length::zero(),
+ )
+ },
+ generic::TransformOperation::Translate(ref x, ref y) => {
+ generic::TransformOperation::Translate3D(x.clone(), y.clone(), Length::zero())
+ },
+ generic::TransformOperation::TranslateY(ref y) => {
+ generic::TransformOperation::Translate3D(
+ LengthPercentage::zero(),
+ y.clone(),
+ Length::zero(),
+ )
+ },
+ generic::TransformOperation::TranslateZ(ref z) => {
+ generic::TransformOperation::Translate3D(
+ LengthPercentage::zero(),
+ LengthPercentage::zero(),
+ z.clone(),
+ )
+ },
+ _ => unreachable!(),
+ }
+ }
+
+ /// Convert to a Rotate3D.
+ ///
+ /// Must be called on a Rotate function.
+ pub fn to_rotate_3d(&self) -> Self {
+ match *self {
+ generic::TransformOperation::Rotate3D(..) => self.clone(),
+ generic::TransformOperation::RotateZ(ref angle) |
+ generic::TransformOperation::Rotate(ref angle) => {
+ generic::TransformOperation::Rotate3D(0., 0., 1., angle.clone())
+ },
+ generic::TransformOperation::RotateX(ref angle) => {
+ generic::TransformOperation::Rotate3D(1., 0., 0., angle.clone())
+ },
+ generic::TransformOperation::RotateY(ref angle) => {
+ generic::TransformOperation::Rotate3D(0., 1., 0., angle.clone())
+ },
+ _ => unreachable!(),
+ }
+ }
+
+ /// Convert to a Scale3D.
+ ///
+ /// Must be called on a Scale function
+ pub fn to_scale_3d(&self) -> Self {
+ match *self {
+ generic::TransformOperation::Scale3D(..) => self.clone(),
+ generic::TransformOperation::Scale(x, y) => {
+ generic::TransformOperation::Scale3D(x, y, 1.)
+ },
+ generic::TransformOperation::ScaleX(x) => {
+ generic::TransformOperation::Scale3D(x, 1., 1.)
+ },
+ generic::TransformOperation::ScaleY(y) => {
+ generic::TransformOperation::Scale3D(1., y, 1.)
+ },
+ generic::TransformOperation::ScaleZ(z) => {
+ generic::TransformOperation::Scale3D(1., 1., z)
+ },
+ _ => unreachable!(),
+ }
+ }
+}
+
+/// Build an equivalent 'identity transform function list' based
+/// on an existing transform list.
+/// http://dev.w3.org/csswg/css-transforms/#none-transform-animation
+impl ToAnimatedZero for TransformOperation {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ match *self {
+ generic::TransformOperation::Matrix3D(..) => {
+ Ok(generic::TransformOperation::Matrix3D(Matrix3D::identity()))
+ },
+ generic::TransformOperation::Matrix(..) => {
+ Ok(generic::TransformOperation::Matrix(Matrix::identity()))
+ },
+ generic::TransformOperation::Skew(sx, sy) => Ok(generic::TransformOperation::Skew(
+ sx.to_animated_zero()?,
+ sy.to_animated_zero()?,
+ )),
+ generic::TransformOperation::SkewX(s) => {
+ Ok(generic::TransformOperation::SkewX(s.to_animated_zero()?))
+ },
+ generic::TransformOperation::SkewY(s) => {
+ Ok(generic::TransformOperation::SkewY(s.to_animated_zero()?))
+ },
+ generic::TransformOperation::Translate3D(ref tx, ref ty, ref tz) => {
+ Ok(generic::TransformOperation::Translate3D(
+ tx.to_animated_zero()?,
+ ty.to_animated_zero()?,
+ tz.to_animated_zero()?,
+ ))
+ },
+ generic::TransformOperation::Translate(ref tx, ref ty) => {
+ Ok(generic::TransformOperation::Translate(
+ tx.to_animated_zero()?,
+ ty.to_animated_zero()?,
+ ))
+ },
+ generic::TransformOperation::TranslateX(ref t) => Ok(
+ generic::TransformOperation::TranslateX(t.to_animated_zero()?),
+ ),
+ generic::TransformOperation::TranslateY(ref t) => Ok(
+ generic::TransformOperation::TranslateY(t.to_animated_zero()?),
+ ),
+ generic::TransformOperation::TranslateZ(ref t) => Ok(
+ generic::TransformOperation::TranslateZ(t.to_animated_zero()?),
+ ),
+ generic::TransformOperation::Scale3D(..) => {
+ Ok(generic::TransformOperation::Scale3D(1.0, 1.0, 1.0))
+ },
+ generic::TransformOperation::Scale(_, _) => {
+ Ok(generic::TransformOperation::Scale(1.0, 1.0))
+ },
+ generic::TransformOperation::ScaleX(..) => Ok(generic::TransformOperation::ScaleX(1.0)),
+ generic::TransformOperation::ScaleY(..) => Ok(generic::TransformOperation::ScaleY(1.0)),
+ generic::TransformOperation::ScaleZ(..) => Ok(generic::TransformOperation::ScaleZ(1.0)),
+ generic::TransformOperation::Rotate3D(x, y, z, a) => {
+ let (x, y, z, _) = generic::get_normalized_vector_and_angle(x, y, z, a);
+ Ok(generic::TransformOperation::Rotate3D(
+ x,
+ y,
+ z,
+ Angle::zero(),
+ ))
+ },
+ generic::TransformOperation::RotateX(_) => {
+ Ok(generic::TransformOperation::RotateX(Angle::zero()))
+ },
+ generic::TransformOperation::RotateY(_) => {
+ Ok(generic::TransformOperation::RotateY(Angle::zero()))
+ },
+ generic::TransformOperation::RotateZ(_) => {
+ Ok(generic::TransformOperation::RotateZ(Angle::zero()))
+ },
+ generic::TransformOperation::Rotate(_) => {
+ Ok(generic::TransformOperation::Rotate(Angle::zero()))
+ },
+ generic::TransformOperation::Perspective(_) => Ok(
+ generic::TransformOperation::Perspective(generic::PerspectiveFunction::None),
+ ),
+ generic::TransformOperation::AccumulateMatrix { .. } |
+ generic::TransformOperation::InterpolateMatrix { .. } => {
+ // AccumulateMatrix/InterpolateMatrix: We do interpolation on
+ // AccumulateMatrix/InterpolateMatrix by reading it as a ComputedMatrix
+ // (with layout information), and then do matrix interpolation.
+ //
+ // Therefore, we use an identity matrix to represent the identity transform list.
+ // http://dev.w3.org/csswg/css-transforms/#identity-transform-function
+ Ok(generic::TransformOperation::Matrix3D(Matrix3D::identity()))
+ },
+ }
+ }
+}
+
+impl ToAnimatedZero for Transform {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(generic::Transform(
+ self.0
+ .iter()
+ .map(|op| op.to_animated_zero())
+ .collect::<Result<crate::OwnedSlice<_>, _>>()?,
+ ))
+ }
+}
+
+/// A computed CSS `rotate`
+pub type Rotate = generic::GenericRotate<Number, Angle>;
+
+/// A computed CSS `translate`
+pub type Translate = generic::GenericTranslate<LengthPercentage, Length>;
+
+/// A computed CSS `scale`
+pub type Scale = generic::GenericScale<Number>;
diff --git a/servo/components/style/values/computed/ui.rs b/servo/components/style/values/computed/ui.rs
new file mode 100644
index 0000000000..f285c0626b
--- /dev/null
+++ b/servo/components/style/values/computed/ui.rs
@@ -0,0 +1,21 @@
+/* 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/. */
+
+//! Computed values for UI properties
+
+use crate::values::computed::color::Color;
+use crate::values::computed::image::Image;
+use crate::values::computed::Number;
+use crate::values::generics::ui as generics;
+
+pub use crate::values::specified::ui::{BoolInteger, CursorKind, MozTheme, UserSelect};
+
+/// A computed value for the `cursor` property.
+pub type Cursor = generics::GenericCursor<CursorImage>;
+
+/// A computed value for item of `image cursors`.
+pub type CursorImage = generics::GenericCursorImage<Image, Number>;
+
+/// A computed value for `scrollbar-color` property.
+pub type ScrollbarColor = generics::GenericScrollbarColor<Color>;
diff --git a/servo/components/style/values/computed/url.rs b/servo/components/style/values/computed/url.rs
new file mode 100644
index 0000000000..9f0d8f5bb3
--- /dev/null
+++ b/servo/components/style/values/computed/url.rs
@@ -0,0 +1,15 @@
+/* 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/. */
+
+//! Common handling for the computed value CSS url() values.
+
+use crate::values::generics::url::UrlOrNone as GenericUrlOrNone;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::url::{ComputedImageUrl, ComputedUrl};
+#[cfg(feature = "servo")]
+pub use crate::servo::url::{ComputedImageUrl, ComputedUrl};
+
+/// Computed <url> | <none>
+pub type UrlOrNone = GenericUrlOrNone<ComputedUrl>;
diff --git a/servo/components/style/values/distance.rs b/servo/components/style/values/distance.rs
new file mode 100644
index 0000000000..fef376cf5f
--- /dev/null
+++ b/servo/components/style/values/distance.rs
@@ -0,0 +1,138 @@
+/* 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/. */
+
+//! Machinery to compute distances between animatable values.
+
+use app_units::Au;
+use euclid::default::Size2D;
+use std::iter::Sum;
+use std::ops::Add;
+
+/// A trait to compute squared distances between two animatable values.
+///
+/// This trait is derivable with `#[derive(ComputeSquaredDistance)]`. The derived
+/// implementation uses a `match` expression with identical patterns for both
+/// `self` and `other`, calling `ComputeSquaredDistance::compute_squared_distance`
+/// on each fields of the values.
+///
+/// If a variant is annotated with `#[animation(error)]`, the corresponding
+/// `match` arm returns an error.
+///
+/// Trait bounds for type parameter `Foo` can be opted out of with
+/// `#[animation(no_bound(Foo))]` on the type definition, trait bounds for
+/// fields can be opted into with `#[distance(field_bound)]` on the field.
+pub trait ComputeSquaredDistance {
+ /// Computes the squared distance between two animatable values.
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()>;
+}
+
+/// A distance between two animatable values.
+#[derive(Add, Clone, Copy, Debug, From)]
+pub struct SquaredDistance {
+ value: f64,
+}
+
+impl SquaredDistance {
+ /// Returns a squared distance from its square root.
+ #[inline]
+ pub fn from_sqrt(sqrt: f64) -> Self {
+ Self { value: sqrt * sqrt }
+ }
+}
+
+impl ComputeSquaredDistance for u16 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt(
+ ((*self as f64) - (*other as f64)).abs(),
+ ))
+ }
+}
+
+impl ComputeSquaredDistance for i16 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64))
+ }
+}
+
+impl ComputeSquaredDistance for i32 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64))
+ }
+}
+
+impl ComputeSquaredDistance for f32 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64))
+ }
+}
+
+impl ComputeSquaredDistance for f64 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt((*self - *other).abs()))
+ }
+}
+
+impl ComputeSquaredDistance for Au {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ self.0.compute_squared_distance(&other.0)
+ }
+}
+
+impl<T> ComputeSquaredDistance for Box<T>
+where
+ T: ComputeSquaredDistance,
+{
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ (**self).compute_squared_distance(&**other)
+ }
+}
+
+impl<T> ComputeSquaredDistance for Option<T>
+where
+ T: ComputeSquaredDistance,
+{
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ match (self.as_ref(), other.as_ref()) {
+ (Some(this), Some(other)) => this.compute_squared_distance(other),
+ (None, None) => Ok(SquaredDistance::from_sqrt(0.)),
+ _ => Err(()),
+ }
+ }
+}
+
+impl<T> ComputeSquaredDistance for Size2D<T>
+where
+ T: ComputeSquaredDistance,
+{
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(self.width.compute_squared_distance(&other.width)? +
+ self.height.compute_squared_distance(&other.height)?)
+ }
+}
+
+impl SquaredDistance {
+ /// Returns the square root of this squared distance.
+ #[inline]
+ pub fn sqrt(self) -> f64 {
+ self.value.sqrt()
+ }
+}
+
+impl Sum for SquaredDistance {
+ fn sum<I>(iter: I) -> Self
+ where
+ I: Iterator<Item = Self>,
+ {
+ iter.fold(SquaredDistance::from_sqrt(0.), Add::add)
+ }
+}
diff --git a/servo/components/style/values/generics/animation.rs b/servo/components/style/values/generics/animation.rs
new file mode 100644
index 0000000000..edee9e9f25
--- /dev/null
+++ b/servo/components/style/values/generics/animation.rs
@@ -0,0 +1,140 @@
+/* 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/. */
+
+//! Generic values for properties related to animations and transitions.
+
+use crate::values::generics::length::GenericLengthPercentageOrAuto;
+use crate::values::specified::animation::{ScrollAxis, ScrollFunction};
+use crate::values::TimelineName;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// The view() notation.
+/// https://drafts.csswg.org/scroll-animations-1/#view-notation
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "view")]
+#[repr(C)]
+pub struct GenericViewFunction<LengthPercent> {
+ /// The axis of scrolling that drives the progress of the timeline.
+ #[css(skip_if = "ScrollAxis::is_default")]
+ pub axis: ScrollAxis,
+ /// An adjustment of the view progress visibility range.
+ #[css(skip_if = "GenericViewTimelineInset::is_auto")]
+ #[css(field_bound)]
+ pub inset: GenericViewTimelineInset<LengthPercent>,
+}
+
+pub use self::GenericViewFunction as ViewFunction;
+
+/// A value for the <single-animation-timeline>.
+///
+/// https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericAnimationTimeline<LengthPercent> {
+ /// Use default timeline. The animation’s timeline is a DocumentTimeline.
+ Auto,
+ /// The scroll-timeline name or view-timeline-name.
+ /// https://drafts.csswg.org/scroll-animations-1/#scroll-timelines-named
+ /// https://drafts.csswg.org/scroll-animations-1/#view-timeline-name
+ Timeline(TimelineName),
+ /// The scroll() notation.
+ /// https://drafts.csswg.org/scroll-animations-1/#scroll-notation
+ Scroll(ScrollFunction),
+ /// The view() notation.
+ /// https://drafts.csswg.org/scroll-animations-1/#view-notation
+ View(#[css(field_bound)] GenericViewFunction<LengthPercent>),
+}
+
+pub use self::GenericAnimationTimeline as AnimationTimeline;
+
+impl<LengthPercent> AnimationTimeline<LengthPercent> {
+ /// Returns the `auto` value.
+ pub fn auto() -> Self {
+ Self::Auto
+ }
+
+ /// Returns true if it is auto (i.e. the default value).
+ pub fn is_auto(&self) -> bool {
+ matches!(self, Self::Auto)
+ }
+}
+
+/// A generic value for the `[ [ auto | <length-percentage> ]{1,2} ]`.
+///
+/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericViewTimelineInset<LengthPercent> {
+ /// The start inset in the relevant axis.
+ pub start: GenericLengthPercentageOrAuto<LengthPercent>,
+ /// The end inset.
+ pub end: GenericLengthPercentageOrAuto<LengthPercent>,
+}
+
+pub use self::GenericViewTimelineInset as ViewTimelineInset;
+
+impl<LengthPercent> ViewTimelineInset<LengthPercent> {
+ /// Returns true if it is auto.
+ #[inline]
+ fn is_auto(&self) -> bool {
+ self.start.is_auto() && self.end.is_auto()
+ }
+}
+
+impl<LengthPercent> ToCss for ViewTimelineInset<LengthPercent>
+where
+ LengthPercent: PartialEq + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.start.to_css(dest)?;
+ if self.end != self.start {
+ dest.write_char(' ')?;
+ self.end.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl<LengthPercent> Default for ViewTimelineInset<LengthPercent> {
+ fn default() -> Self {
+ Self {
+ start: GenericLengthPercentageOrAuto::auto(),
+ end: GenericLengthPercentageOrAuto::auto(),
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/background.rs b/servo/components/style/values/generics/background.rs
new file mode 100644
index 0000000000..d9b6624595
--- /dev/null
+++ b/servo/components/style/values/generics/background.rs
@@ -0,0 +1,54 @@
+/* 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/. */
+
+//! Generic types for CSS values related to backgrounds.
+
+use crate::values::generics::length::{GenericLengthPercentageOrAuto, LengthPercentageOrAuto};
+
+/// A generic value for the `background-size` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericBackgroundSize<LengthPercent> {
+ /// `<width> <height>`
+ ExplicitSize {
+ /// Explicit width.
+ width: GenericLengthPercentageOrAuto<LengthPercent>,
+ /// Explicit height.
+ #[css(skip_if = "GenericLengthPercentageOrAuto::is_auto")]
+ height: GenericLengthPercentageOrAuto<LengthPercent>,
+ },
+ /// `cover`
+ #[animation(error)]
+ Cover,
+ /// `contain`
+ #[animation(error)]
+ Contain,
+}
+
+pub use self::GenericBackgroundSize as BackgroundSize;
+
+impl<LengthPercentage> BackgroundSize<LengthPercentage> {
+ /// Returns `auto auto`.
+ pub fn auto() -> Self {
+ GenericBackgroundSize::ExplicitSize {
+ width: LengthPercentageOrAuto::Auto,
+ height: LengthPercentageOrAuto::Auto,
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/basic_shape.rs b/servo/components/style/values/generics/basic_shape.rs
new file mode 100644
index 0000000000..13d27995c1
--- /dev/null
+++ b/servo/components/style/values/generics/basic_shape.rs
@@ -0,0 +1,567 @@
+/* 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/. */
+
+//! CSS handling for the [`basic-shape`](https://drafts.csswg.org/css-shapes/#typedef-basic-shape)
+//! types that are generic over their `ToCss` implementations.
+
+use crate::values::animated::{lists, Animate, Procedure, ToAnimatedZero};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::border::GenericBorderRadius;
+use crate::values::generics::position::GenericPositionOrAuto;
+use crate::values::generics::rect::Rect;
+use crate::values::specified::SVGPathData;
+use crate::Zero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// <https://drafts.fxtf.org/css-masking-1/#typedef-geometry-box>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ShapeGeometryBox {
+ /// Depending on which kind of element this style value applied on, the
+ /// default value of the reference-box can be different. For an HTML
+ /// element, the default value of reference-box is border-box; for an SVG
+ /// element, the default value is fill-box. Since we can not determine the
+ /// default value at parsing time, we keep this value to make a decision on
+ /// it.
+ #[css(skip)]
+ ElementDependent,
+ FillBox,
+ StrokeBox,
+ ViewBox,
+ ShapeBox(ShapeBox),
+}
+
+impl Default for ShapeGeometryBox {
+ fn default() -> Self {
+ Self::ElementDependent
+ }
+}
+
+/// Skip the serialization if the author omits the box or specifies border-box.
+#[inline]
+fn is_default_box_for_clip_path(b: &ShapeGeometryBox) -> bool {
+ // Note: for clip-path, ElementDependent is always border-box, so we have to check both of them
+ // for serialization.
+ matches!(b, ShapeGeometryBox::ElementDependent) ||
+ matches!(b, ShapeGeometryBox::ShapeBox(ShapeBox::BorderBox))
+}
+
+/// https://drafts.csswg.org/css-shapes-1/#typedef-shape-box
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ Copy,
+ ComputeSquaredDistance,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ShapeBox {
+ MarginBox,
+ BorderBox,
+ PaddingBox,
+ ContentBox,
+}
+
+impl Default for ShapeBox {
+ fn default() -> Self {
+ ShapeBox::MarginBox
+ }
+}
+
+/// A value for the `clip-path` property.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(U))]
+#[repr(u8)]
+pub enum GenericClipPath<BasicShape, U> {
+ #[animation(error)]
+ None,
+ #[animation(error)]
+ Url(U),
+ Shape(
+ Box<BasicShape>,
+ #[css(skip_if = "is_default_box_for_clip_path")] ShapeGeometryBox,
+ ),
+ #[animation(error)]
+ Box(ShapeGeometryBox),
+}
+
+pub use self::GenericClipPath as ClipPath;
+
+/// A value for the `shape-outside` property.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(I))]
+#[repr(u8)]
+pub enum GenericShapeOutside<BasicShape, I> {
+ #[animation(error)]
+ None,
+ #[animation(error)]
+ Image(I),
+ Shape(Box<BasicShape>, #[css(skip_if = "is_default")] ShapeBox),
+ #[animation(error)]
+ Box(ShapeBox),
+}
+
+pub use self::GenericShapeOutside as ShapeOutside;
+
+/// The <basic-shape>.
+///
+/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercentage, BasicShapeRect>
+{
+ /// The <basic-shape-rect>.
+ Rect(BasicShapeRect),
+ /// Defines a circle with a center and a radius.
+ Circle(
+ #[css(field_bound)]
+ #[shmem(field_bound)]
+ Circle<Position, NonNegativeLengthPercentage>,
+ ),
+ /// Defines an ellipse with a center and x-axis/y-axis radii.
+ Ellipse(
+ #[css(field_bound)]
+ #[shmem(field_bound)]
+ Ellipse<Position, NonNegativeLengthPercentage>,
+ ),
+ /// Defines a polygon with pair arguments.
+ Polygon(GenericPolygon<LengthPercentage>),
+ /// Defines a path with SVG path syntax.
+ Path(Path),
+ // TODO: Bug 1823463. Add shape().
+ // https://drafts.csswg.org/css-shapes-2/#shape-function
+}
+
+pub use self::GenericBasicShape as BasicShape;
+
+/// <https://drafts.csswg.org/css-shapes/#funcdef-inset>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "inset")]
+#[repr(C)]
+pub struct GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage> {
+ pub rect: Rect<LengthPercentage>,
+ #[shmem(field_bound)]
+ pub round: GenericBorderRadius<NonNegativeLengthPercentage>,
+}
+
+pub use self::GenericInsetRect as InsetRect;
+
+/// <https://drafts.csswg.org/css-shapes/#funcdef-circle>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function)]
+#[repr(C)]
+pub struct Circle<Position, NonNegativeLengthPercentage> {
+ pub position: GenericPositionOrAuto<Position>,
+ pub radius: GenericShapeRadius<NonNegativeLengthPercentage>,
+}
+
+/// <https://drafts.csswg.org/css-shapes/#funcdef-ellipse>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function)]
+#[repr(C)]
+pub struct Ellipse<Position, NonNegativeLengthPercentage> {
+ pub position: GenericPositionOrAuto<Position>,
+ pub semiaxis_x: GenericShapeRadius<NonNegativeLengthPercentage>,
+ pub semiaxis_y: GenericShapeRadius<NonNegativeLengthPercentage>,
+}
+
+/// <https://drafts.csswg.org/css-shapes/#typedef-shape-radius>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericShapeRadius<NonNegativeLengthPercentage> {
+ Length(NonNegativeLengthPercentage),
+ #[animation(error)]
+ ClosestSide,
+ #[animation(error)]
+ FarthestSide,
+}
+
+pub use self::GenericShapeRadius as ShapeRadius;
+
+/// A generic type for representing the `polygon()` function
+///
+/// <https://drafts.csswg.org/css-shapes/#funcdef-polygon>
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma, function = "polygon")]
+#[repr(C)]
+pub struct GenericPolygon<LengthPercentage> {
+ /// The filling rule for a polygon.
+ #[css(skip_if = "is_default")]
+ pub fill: FillRule,
+ /// A collection of (x, y) coordinates to draw the polygon.
+ #[css(iterable)]
+ pub coordinates: crate::OwnedSlice<PolygonCoord<LengthPercentage>>,
+}
+
+pub use self::GenericPolygon as Polygon;
+
+/// Coordinates for Polygon.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct PolygonCoord<LengthPercentage>(pub LengthPercentage, pub LengthPercentage);
+
+// https://drafts.csswg.org/css-shapes/#typedef-fill-rule
+// NOTE: Basic shapes spec says that these are the only two values, however
+// https://www.w3.org/TR/SVG/painting.html#FillRuleProperty
+// says that it can also be `inherit`
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum FillRule {
+ Nonzero,
+ Evenodd,
+}
+
+/// The path function defined in css-shape-2.
+///
+/// https://drafts.csswg.org/css-shapes-2/#funcdef-path
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma, function = "path")]
+#[repr(C)]
+pub struct Path {
+ /// The filling rule for the svg path.
+ #[css(skip_if = "is_default")]
+ pub fill: FillRule,
+ /// The svg path data.
+ pub path: SVGPathData,
+}
+
+impl<B, U> ToAnimatedZero for ClipPath<B, U> {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+impl<B, U> ToAnimatedZero for ShapeOutside<B, U> {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+impl<Length, NonNegativeLength> ToCss for InsetRect<Length, NonNegativeLength>
+where
+ Length: ToCss + PartialEq,
+ NonNegativeLength: ToCss + PartialEq + Zero,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("inset(")?;
+ self.rect.to_css(dest)?;
+ if !self.round.is_zero() {
+ dest.write_str(" round ")?;
+ self.round.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<Position, NonNegativeLengthPercentage> ToCss for Circle<Position, NonNegativeLengthPercentage>
+where
+ Position: ToCss,
+ NonNegativeLengthPercentage: ToCss + PartialEq,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let has_radius = self.radius != Default::default();
+
+ dest.write_str("circle(")?;
+ if has_radius {
+ self.radius.to_css(dest)?;
+ }
+
+ // Preserve the `at <position>` even if it specified the default value.
+ // https://github.com/w3c/csswg-drafts/issues/8695
+ if !matches!(self.position, GenericPositionOrAuto::Auto) {
+ if has_radius {
+ dest.write_char(' ')?;
+ }
+ dest.write_str("at ")?;
+ self.position.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<Position, NonNegativeLengthPercentage> ToCss for Ellipse<Position, NonNegativeLengthPercentage>
+where
+ Position: ToCss,
+ NonNegativeLengthPercentage: ToCss + PartialEq,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let has_radii =
+ self.semiaxis_x != Default::default() || self.semiaxis_y != Default::default();
+
+ dest.write_str("ellipse(")?;
+ if has_radii {
+ self.semiaxis_x.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.semiaxis_y.to_css(dest)?;
+ }
+
+ // Preserve the `at <position>` even if it specified the default value.
+ // https://github.com/w3c/csswg-drafts/issues/8695
+ if !matches!(self.position, GenericPositionOrAuto::Auto) {
+ if has_radii {
+ dest.write_char(' ')?;
+ }
+ dest.write_str("at ")?;
+ self.position.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<L> Default for ShapeRadius<L> {
+ #[inline]
+ fn default() -> Self {
+ ShapeRadius::ClosestSide
+ }
+}
+
+impl<L> Animate for Polygon<L>
+where
+ L: Animate,
+{
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.fill != other.fill {
+ return Err(());
+ }
+ let coordinates =
+ lists::by_computed_value::animate(&self.coordinates, &other.coordinates, procedure)?;
+ Ok(Polygon {
+ fill: self.fill,
+ coordinates,
+ })
+ }
+}
+
+impl<L> ComputeSquaredDistance for Polygon<L>
+where
+ L: ComputeSquaredDistance,
+{
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ if self.fill != other.fill {
+ return Err(());
+ }
+ lists::by_computed_value::squared_distance(&self.coordinates, &other.coordinates)
+ }
+}
+
+impl Default for FillRule {
+ #[inline]
+ fn default() -> Self {
+ FillRule::Nonzero
+ }
+}
+
+#[inline]
+fn is_default<T: Default + PartialEq>(fill: &T) -> bool {
+ *fill == Default::default()
+}
diff --git a/servo/components/style/values/generics/border.rs b/servo/components/style/values/generics/border.rs
new file mode 100644
index 0000000000..feb80998d1
--- /dev/null
+++ b/servo/components/style/values/generics/border.rs
@@ -0,0 +1,261 @@
+/* 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/. */
+
+//! Generic types for CSS values related to borders.
+
+use crate::values::generics::rect::Rect;
+use crate::values::generics::size::Size2D;
+use crate::Zero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A generic value for a single side of a `border-image-width` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericBorderImageSideWidth<LP, N> {
+ /// `<number>`
+ ///
+ /// NOTE: Numbers need to be before length-percentagess, in order to parse
+ /// them first, since `0` should be a number, not the `0px` length.
+ Number(N),
+ /// `<length-or-percentage>`
+ LengthPercentage(LP),
+ /// `auto`
+ Auto,
+}
+
+pub use self::GenericBorderImageSideWidth as BorderImageSideWidth;
+
+/// A generic value for the `border-image-slice` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericBorderImageSlice<NumberOrPercentage> {
+ /// The offsets.
+ #[css(field_bound)]
+ pub offsets: Rect<NumberOrPercentage>,
+ /// Whether to fill the middle part.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub fill: bool,
+}
+
+pub use self::GenericBorderImageSlice as BorderImageSlice;
+
+/// A generic value for the `border-*-radius` longhand properties.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericBorderCornerRadius<L>(
+ #[css(field_bound)]
+ #[shmem(field_bound)]
+ pub Size2D<L>,
+);
+
+pub use self::GenericBorderCornerRadius as BorderCornerRadius;
+
+impl<L> BorderCornerRadius<L> {
+ /// Trivially create a `BorderCornerRadius`.
+ pub fn new(w: L, h: L) -> Self {
+ BorderCornerRadius(Size2D::new(w, h))
+ }
+}
+
+impl<L: Zero> Zero for BorderCornerRadius<L> {
+ fn zero() -> Self {
+ BorderCornerRadius(Size2D::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+/// A generic value for the `border-spacing` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct BorderSpacing<L>(
+ #[css(field_bound)]
+ #[shmem(field_bound)]
+ pub Size2D<L>,
+);
+
+impl<L> BorderSpacing<L> {
+ /// Trivially create a `BorderCornerRadius`.
+ pub fn new(w: L, h: L) -> Self {
+ BorderSpacing(Size2D::new(w, h))
+ }
+}
+
+/// A generic value for `border-radius` and `inset()`.
+///
+/// <https://drafts.csswg.org/css-backgrounds-3/#border-radius>
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericBorderRadius<LengthPercentage> {
+ /// The top left radius.
+ #[shmem(field_bound)]
+ pub top_left: GenericBorderCornerRadius<LengthPercentage>,
+ /// The top right radius.
+ pub top_right: GenericBorderCornerRadius<LengthPercentage>,
+ /// The bottom right radius.
+ pub bottom_right: GenericBorderCornerRadius<LengthPercentage>,
+ /// The bottom left radius.
+ pub bottom_left: GenericBorderCornerRadius<LengthPercentage>,
+}
+
+pub use self::GenericBorderRadius as BorderRadius;
+
+impl<L> BorderRadius<L> {
+ /// Returns a new `BorderRadius<L>`.
+ #[inline]
+ pub fn new(
+ tl: BorderCornerRadius<L>,
+ tr: BorderCornerRadius<L>,
+ br: BorderCornerRadius<L>,
+ bl: BorderCornerRadius<L>,
+ ) -> Self {
+ BorderRadius {
+ top_left: tl,
+ top_right: tr,
+ bottom_right: br,
+ bottom_left: bl,
+ }
+ }
+
+ /// Serialises two given rects following the syntax of the `border-radius``
+ /// property.
+ pub fn serialize_rects<W>(
+ widths: Rect<&L>,
+ heights: Rect<&L>,
+ dest: &mut CssWriter<W>,
+ ) -> fmt::Result
+ where
+ L: PartialEq + ToCss,
+ W: Write,
+ {
+ widths.to_css(dest)?;
+ if widths != heights {
+ dest.write_str(" / ")?;
+ heights.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl<L: Zero> Zero for BorderRadius<L> {
+ fn zero() -> Self {
+ Self::new(
+ BorderCornerRadius::<L>::zero(),
+ BorderCornerRadius::<L>::zero(),
+ BorderCornerRadius::<L>::zero(),
+ BorderCornerRadius::<L>::zero(),
+ )
+ }
+
+ fn is_zero(&self) -> bool {
+ self.top_left.is_zero() &&
+ self.top_right.is_zero() &&
+ self.bottom_right.is_zero() &&
+ self.bottom_left.is_zero()
+ }
+}
+
+impl<L> ToCss for BorderRadius<L>
+where
+ L: PartialEq + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let BorderRadius {
+ top_left: BorderCornerRadius(ref tl),
+ top_right: BorderCornerRadius(ref tr),
+ bottom_right: BorderCornerRadius(ref br),
+ bottom_left: BorderCornerRadius(ref bl),
+ } = *self;
+
+ let widths = Rect::new(&tl.width, &tr.width, &br.width, &bl.width);
+ let heights = Rect::new(&tl.height, &tr.height, &br.height, &bl.height);
+
+ Self::serialize_rects(widths, heights, dest)
+ }
+}
diff --git a/servo/components/style/values/generics/box.rs b/servo/components/style/values/generics/box.rs
new file mode 100644
index 0000000000..12c5f28bfb
--- /dev/null
+++ b/servo/components/style/values/generics/box.rs
@@ -0,0 +1,211 @@
+/* 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/. */
+
+//! Generic types for box properties.
+
+use crate::values::animated::ToAnimatedZero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ FromPrimitive,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum VerticalAlignKeyword {
+ Baseline,
+ Sub,
+ Super,
+ Top,
+ TextTop,
+ Middle,
+ Bottom,
+ TextBottom,
+ #[cfg(feature = "gecko")]
+ MozMiddleWithBaseline,
+}
+
+/// A generic value for the `vertical-align` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericVerticalAlign<LengthPercentage> {
+ /// One of the vertical-align keywords.
+ Keyword(VerticalAlignKeyword),
+ /// `<length-percentage>`
+ Length(LengthPercentage),
+}
+
+pub use self::GenericVerticalAlign as VerticalAlign;
+
+impl<L> VerticalAlign<L> {
+ /// Returns `baseline`.
+ #[inline]
+ pub fn baseline() -> Self {
+ VerticalAlign::Keyword(VerticalAlignKeyword::Baseline)
+ }
+}
+
+impl<L> ToAnimatedZero for VerticalAlign<L> {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+/// https://drafts.csswg.org/css-sizing-4/#intrinsic-size-override
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "auto")]
+#[repr(C, u8)]
+pub enum GenericContainIntrinsicSize<L> {
+ /// The keyword `none`.
+ None,
+ /// The keywords 'auto none',
+ AutoNone,
+ /// A non-negative length.
+ Length(L),
+ /// "auto <Length>"
+ AutoLength(L),
+}
+
+pub use self::GenericContainIntrinsicSize as ContainIntrinsicSize;
+
+impl<L: ToCss> ToCss for ContainIntrinsicSize<L> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Self::None => dest.write_str("none"),
+ Self::AutoNone => dest.write_str("auto none"),
+ Self::Length(ref l) => l.to_css(dest),
+ Self::AutoLength(ref l) => {
+ dest.write_str("auto ")?;
+ l.to_css(dest)
+ },
+ }
+ }
+}
+
+/// Note that we only implement -webkit-line-clamp as a single, longhand
+/// property for now, but the spec defines line-clamp as a shorthand for
+/// separate max-lines, block-ellipsis, and continue properties.
+///
+/// https://drafts.csswg.org/css-overflow-3/#line-clamp
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+#[value_info(other_values = "none")]
+pub struct GenericLineClamp<I>(pub I);
+
+pub use self::GenericLineClamp as LineClamp;
+
+impl<I: crate::Zero> LineClamp<I> {
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ Self(crate::Zero::zero())
+ }
+
+ /// Returns whether we're the `none` value.
+ pub fn is_none(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+impl<I: crate::Zero + ToCss> ToCss for LineClamp<I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_none() {
+ return dest.write_str("none");
+ }
+ self.0.to_css(dest)
+ }
+}
+
+/// A generic value for the `perspective` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericPerspective<NonNegativeLength> {
+ /// A non-negative length.
+ Length(NonNegativeLength),
+ /// The keyword `none`.
+ None,
+}
+
+pub use self::GenericPerspective as Perspective;
+
+impl<L> Perspective<L> {
+ /// Returns `none`.
+ #[inline]
+ pub fn none() -> Self {
+ Perspective::None
+ }
+}
diff --git a/servo/components/style/values/generics/calc.rs b/servo/components/style/values/generics/calc.rs
new file mode 100644
index 0000000000..abcb5fe6eb
--- /dev/null
+++ b/servo/components/style/values/generics/calc.rs
@@ -0,0 +1,1820 @@
+/* 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/. */
+
+//! [Calc expressions][calc].
+//!
+//! [calc]: https://drafts.csswg.org/css-values/#calc-notation
+
+use num_traits::Zero;
+use smallvec::SmallVec;
+use std::fmt::{self, Write};
+use std::ops::{Add, Mul, Neg, Rem, Sub};
+use std::{cmp, mem};
+use style_traits::{CssWriter, ToCss};
+
+/// Whether we're a `min` or `max` function.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum MinMaxOp {
+ /// `min()`
+ Min,
+ /// `max()`
+ Max,
+}
+
+/// Whether we're a `mod` or `rem` function.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ModRemOp {
+ /// `mod()`
+ Mod,
+ /// `rem()`
+ Rem,
+}
+
+impl ModRemOp {
+ fn apply(self, dividend: f32, divisor: f32) -> f32 {
+ // In mod(A, B) only, if B is infinite and A has opposite sign to B
+ // (including an oppositely-signed zero), the result is NaN.
+ // https://drafts.csswg.org/css-values/#round-infinities
+ if matches!(self, Self::Mod) &&
+ divisor.is_infinite() &&
+ dividend.is_sign_negative() != divisor.is_sign_negative()
+ {
+ return f32::NAN;
+ }
+
+ let (r, same_sign_as) = match self {
+ Self::Mod => (dividend - divisor * (dividend / divisor).floor(), divisor),
+ Self::Rem => (dividend - divisor * (dividend / divisor).trunc(), dividend),
+ };
+ if r == 0.0 && same_sign_as.is_sign_negative() {
+ -0.0
+ } else {
+ r
+ }
+ }
+}
+
+/// The strategy used in `round()`
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum RoundingStrategy {
+ /// `round(nearest, a, b)`
+ /// round a to the nearest multiple of b
+ Nearest,
+ /// `round(up, a, b)`
+ /// round a up to the nearest multiple of b
+ Up,
+ /// `round(down, a, b)`
+ /// round a down to the nearest multiple of b
+ Down,
+ /// `round(to-zero, a, b)`
+ /// round a to the nearest multiple of b that is towards zero
+ ToZero,
+}
+
+/// This determines the order in which we serialize members of a calc() sum.
+///
+/// See https://drafts.csswg.org/css-values-4/#sort-a-calculations-children
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+#[allow(missing_docs)]
+pub enum SortKey {
+ Number,
+ Percentage,
+ Cap,
+ Ch,
+ Cqb,
+ Cqh,
+ Cqi,
+ Cqmax,
+ Cqmin,
+ Cqw,
+ Deg,
+ Dppx,
+ Dvb,
+ Dvh,
+ Dvi,
+ Dvmax,
+ Dvmin,
+ Dvw,
+ Em,
+ Ex,
+ Ic,
+ Lh,
+ Lvb,
+ Lvh,
+ Lvi,
+ Lvmax,
+ Lvmin,
+ Lvw,
+ Px,
+ Rem,
+ Rlh,
+ Sec,
+ Svb,
+ Svh,
+ Svi,
+ Svmax,
+ Svmin,
+ Svw,
+ Vb,
+ Vh,
+ Vi,
+ Vmax,
+ Vmin,
+ Vw,
+ Other,
+}
+
+/// A generic node in a calc expression.
+///
+/// FIXME: This would be much more elegant if we used `Self` in the types below,
+/// but we can't because of https://github.com/serde-rs/serde/issues/1565.
+///
+/// FIXME: The following annotations are to workaround an LLVM inlining bug, see
+/// bug 1631929.
+///
+/// cbindgen:destructor-attributes=MOZ_NEVER_INLINE
+/// cbindgen:copy-constructor-attributes=MOZ_NEVER_INLINE
+/// cbindgen:eq-attributes=MOZ_NEVER_INLINE
+#[repr(u8)]
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum GenericCalcNode<L> {
+ /// A leaf node.
+ Leaf(L),
+ /// A node that negates its child, e.g. Negate(1) == -1.
+ Negate(Box<GenericCalcNode<L>>),
+ /// A node that inverts its child, e.g. Invert(10) == 1 / 10 == 0.1. The child must always
+ /// resolve to a number unit.
+ Invert(Box<GenericCalcNode<L>>),
+ /// A sum node, representing `a + b + c` where a, b, and c are the
+ /// arguments.
+ Sum(crate::OwnedSlice<GenericCalcNode<L>>),
+ /// A product node, representing `a * b * c` where a, b, and c are the
+ /// arguments.
+ Product(crate::OwnedSlice<GenericCalcNode<L>>),
+ /// A `min` or `max` function.
+ MinMax(crate::OwnedSlice<GenericCalcNode<L>>, MinMaxOp),
+ /// A `clamp()` function.
+ Clamp {
+ /// The minimum value.
+ min: Box<GenericCalcNode<L>>,
+ /// The central value.
+ center: Box<GenericCalcNode<L>>,
+ /// The maximum value.
+ max: Box<GenericCalcNode<L>>,
+ },
+ /// A `round()` function.
+ Round {
+ /// The rounding strategy.
+ strategy: RoundingStrategy,
+ /// The value to round.
+ value: Box<GenericCalcNode<L>>,
+ /// The step value.
+ step: Box<GenericCalcNode<L>>,
+ },
+ /// A `mod()` or `rem()` function.
+ ModRem {
+ /// The dividend calculation.
+ dividend: Box<GenericCalcNode<L>>,
+ /// The divisor calculation.
+ divisor: Box<GenericCalcNode<L>>,
+ /// Is the function mod or rem?
+ op: ModRemOp,
+ },
+ /// A `hypot()` function
+ Hypot(crate::OwnedSlice<GenericCalcNode<L>>),
+ /// An `abs()` function.
+ Abs(Box<GenericCalcNode<L>>),
+ /// A `sign()` function.
+ Sign(Box<GenericCalcNode<L>>),
+}
+
+pub use self::GenericCalcNode as CalcNode;
+
+bitflags! {
+ /// Expected units we allow parsing within a `calc()` expression.
+ ///
+ /// This is used as a hint for the parser to fast-reject invalid
+ /// expressions. Numbers are always allowed because they multiply other
+ /// units.
+ #[derive(Clone, Copy, PartialEq, Eq)]
+ pub struct CalcUnits: u8 {
+ /// <length>
+ const LENGTH = 1 << 0;
+ /// <percentage>
+ const PERCENTAGE = 1 << 1;
+ /// <angle>
+ const ANGLE = 1 << 2;
+ /// <time>
+ const TIME = 1 << 3;
+ /// <resolution>
+ const RESOLUTION = 1 << 4;
+
+ /// <length-percentage>
+ const LENGTH_PERCENTAGE = Self::LENGTH.bits() | Self::PERCENTAGE.bits();
+ // NOTE: When you add to this, make sure to make Atan2 deal with these.
+ /// Allow all units.
+ const ALL = Self::LENGTH.bits() | Self::PERCENTAGE.bits() | Self::ANGLE.bits() | Self::TIME.bits() | Self::RESOLUTION.bits();
+ }
+}
+
+impl CalcUnits {
+ /// Returns whether the flags only represent a single unit. This will return true for 0, which
+ /// is a "number" this is also fine.
+ #[inline]
+ fn is_single_unit(&self) -> bool {
+ self.bits() == 0 || self.bits() & (self.bits() - 1) == 0
+ }
+
+ /// Returns true if this unit is allowed to be summed with the given unit, otherwise false.
+ #[inline]
+ fn can_sum_with(&self, other: Self) -> bool {
+ match *self {
+ Self::LENGTH => other.intersects(Self::LENGTH | Self::PERCENTAGE),
+ Self::PERCENTAGE => other.intersects(Self::LENGTH | Self::PERCENTAGE),
+ Self::LENGTH_PERCENTAGE => other.intersects(Self::LENGTH | Self::PERCENTAGE),
+ u => u.is_single_unit() && other == u,
+ }
+ }
+}
+
+/// For percentage resolution, sometimes we can't assume that the percentage basis is positive (so
+/// we don't know whether a percentage is larger than another).
+pub enum PositivePercentageBasis {
+ /// The percent basis is not known-positive, we can't compare percentages.
+ Unknown,
+ /// The percent basis is known-positive, we assume larger percentages are larger.
+ Yes,
+}
+
+macro_rules! compare_helpers {
+ () => {
+ /// Return whether a leaf is greater than another.
+ #[allow(unused)]
+ fn gt(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool {
+ self.compare(other, basis_positive) == Some(cmp::Ordering::Greater)
+ }
+
+ /// Return whether a leaf is less than another.
+ fn lt(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool {
+ self.compare(other, basis_positive) == Some(cmp::Ordering::Less)
+ }
+
+ /// Return whether a leaf is smaller or equal than another.
+ fn lte(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool {
+ match self.compare(other, basis_positive) {
+ Some(cmp::Ordering::Less) => true,
+ Some(cmp::Ordering::Equal) => true,
+ Some(cmp::Ordering::Greater) => false,
+ None => false,
+ }
+ }
+ };
+}
+
+/// A trait that represents all the stuff a valid leaf of a calc expression.
+pub trait CalcNodeLeaf: Clone + Sized + PartialEq + ToCss {
+ /// Returns the unit of the leaf.
+ fn unit(&self) -> CalcUnits;
+
+ /// Returns the unitless value of this leaf.
+ fn unitless_value(&self) -> f32;
+
+ /// Return true if the units of both leaves are equal. (NOTE: Does not take
+ /// the values into account)
+ fn is_same_unit_as(&self, other: &Self) -> bool {
+ std::mem::discriminant(self) == std::mem::discriminant(other)
+ }
+
+ /// Do a partial comparison of these values.
+ fn compare(
+ &self,
+ other: &Self,
+ base_is_positive: PositivePercentageBasis,
+ ) -> Option<cmp::Ordering>;
+ compare_helpers!();
+
+ /// Create a new leaf with a number value.
+ fn new_number(value: f32) -> Self;
+
+ /// Returns a float value if the leaf is a number.
+ fn as_number(&self) -> Option<f32>;
+
+ /// Whether this value is known-negative.
+ fn is_negative(&self) -> bool {
+ self.unitless_value().is_sign_negative()
+ }
+
+ /// Whether this value is infinite.
+ fn is_infinite(&self) -> bool {
+ self.unitless_value().is_infinite()
+ }
+
+ /// Whether this value is zero.
+ fn is_zero(&self) -> bool {
+ self.unitless_value().is_zero()
+ }
+
+ /// Whether this value is NaN.
+ fn is_nan(&self) -> bool {
+ self.unitless_value().is_nan()
+ }
+
+ /// Tries to merge one leaf into another using the sum, that is, perform `x` + `y`.
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()>;
+
+ /// Try to merge the right leaf into the left by using a multiplication. Return true if the
+ /// merge was successful, otherwise false.
+ fn try_product_in_place(&mut self, other: &mut Self) -> bool;
+
+ /// Tries a generic arithmetic operation.
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32;
+
+ /// Map the value of this node with the given operation.
+ fn map(&mut self, op: impl FnMut(f32) -> f32);
+
+ /// Negates the leaf.
+ fn negate(&mut self) {
+ self.map(std::ops::Neg::neg);
+ }
+
+ /// Canonicalizes the expression if necessary.
+ fn simplify(&mut self);
+
+ /// Returns the sort key for simplification.
+ fn sort_key(&self) -> SortKey;
+
+ /// Create a new leaf containing the sign() result of the given leaf.
+ fn sign_from(leaf: &impl CalcNodeLeaf) -> Self {
+ Self::new_number(if leaf.is_nan() {
+ f32::NAN
+ } else if leaf.is_zero() {
+ leaf.unitless_value()
+ } else if leaf.is_negative() {
+ -1.0
+ } else {
+ 1.0
+ })
+ }
+}
+
+/// The level of any argument being serialized in `to_css_impl`.
+enum ArgumentLevel {
+ /// The root of a calculation tree.
+ CalculationRoot,
+ /// The root of an operand node's argument, e.g. `min(10, 20)`, `10` and `20` will have this
+ /// level, but min in this case will have `TopMost`.
+ ArgumentRoot,
+ /// Any other values serialized in the tree.
+ Nested,
+}
+
+impl<L: CalcNodeLeaf> CalcNode<L> {
+ /// Create a dummy CalcNode that can be used to do replacements of other nodes.
+ fn dummy() -> Self {
+ Self::MinMax(Default::default(), MinMaxOp::Max)
+ }
+
+ /// Change all the leaf nodes to have the given value. This is useful when
+ /// you have `calc(1px * nan)` and you want to replace the product node with
+ /// `calc(nan)`, in which case the unit will be retained.
+ fn coerce_to_value(&mut self, value: f32) {
+ self.map(|_| value);
+ }
+
+ /// Return true if a product is distributive over this node.
+ /// Is distributive: (2 + 3) * 4 = 8 + 12
+ /// Not distributive: sign(2 + 3) * 4 != sign(8 + 12)
+ #[inline]
+ pub fn is_product_distributive(&self) -> bool {
+ match self {
+ Self::Leaf(_) => true,
+ Self::Sum(children) => children.iter().all(|c| c.is_product_distributive()),
+ _ => false,
+ }
+ }
+
+ /// If the node has a valid unit outcome, then return it, otherwise fail.
+ pub fn unit(&self) -> Result<CalcUnits, ()> {
+ Ok(match self {
+ CalcNode::Leaf(l) => l.unit(),
+ CalcNode::Negate(child) | CalcNode::Invert(child) | CalcNode::Abs(child) => {
+ child.unit()?
+ },
+ CalcNode::Sum(children) => {
+ let mut unit = children.first().unwrap().unit()?;
+ for child in children.iter().skip(1) {
+ let child_unit = child.unit()?;
+ if !child_unit.can_sum_with(unit) {
+ return Err(());
+ }
+ unit |= child_unit;
+ }
+ unit
+ },
+ CalcNode::Product(children) => {
+ // Only one node is allowed to have a unit, the rest must be numbers.
+ let mut unit = None;
+ for child in children.iter() {
+ let child_unit = child.unit()?;
+ if child_unit.is_empty() {
+ // Numbers are always allowed in a product, so continue with the next.
+ continue;
+ }
+
+ if unit.is_some() {
+ // We already have a unit for the node, so another unit node is invalid.
+ return Err(());
+ }
+
+ // We have the unit for the node.
+ unit = Some(child_unit);
+ }
+ // We only keep track of specified units, so if we end up with a None and no failure
+ // so far, then we have a number.
+ unit.unwrap_or(CalcUnits::empty())
+ },
+ CalcNode::MinMax(children, _) | CalcNode::Hypot(children) => {
+ let mut unit = children.first().unwrap().unit()?;
+ for child in children.iter().skip(1) {
+ let child_unit = child.unit()?;
+ if !child_unit.can_sum_with(unit) {
+ return Err(());
+ }
+ unit |= child_unit;
+ }
+ unit
+ },
+ CalcNode::Clamp { min, center, max } => {
+ let min_unit = min.unit()?;
+ let center_unit = center.unit()?;
+
+ if !min_unit.can_sum_with(center_unit) {
+ return Err(());
+ }
+
+ let max_unit = max.unit()?;
+
+ if !center_unit.can_sum_with(max_unit) {
+ return Err(());
+ }
+
+ min_unit | center_unit | max_unit
+ },
+ CalcNode::Round { value, step, .. } => {
+ let value_unit = value.unit()?;
+ let step_unit = step.unit()?;
+ if !step_unit.can_sum_with(value_unit) {
+ return Err(());
+ }
+ value_unit | step_unit
+ },
+ CalcNode::ModRem {
+ dividend, divisor, ..
+ } => {
+ let dividend_unit = dividend.unit()?;
+ let divisor_unit = divisor.unit()?;
+ if !divisor_unit.can_sum_with(dividend_unit) {
+ return Err(());
+ }
+ dividend_unit | divisor_unit
+ },
+ CalcNode::Sign(ref child) => {
+ // sign() always resolves to a number, but we still need to make sure that the
+ // child units make sense.
+ let _ = child.unit()?;
+ CalcUnits::empty()
+ },
+ })
+ }
+
+ /// Negate the node inline. If the node is distributive, it is replaced by the result,
+ /// otherwise the node is wrapped in a [`Negate`] node.
+ pub fn negate(&mut self) {
+ /// Node(params) -> Negate(Node(params))
+ fn wrap_self_in_negate<L: CalcNodeLeaf>(s: &mut CalcNode<L>) {
+ let result = mem::replace(s, CalcNode::dummy());
+ *s = CalcNode::Negate(Box::new(result));
+ }
+
+ match *self {
+ CalcNode::Leaf(ref mut leaf) => leaf.negate(),
+ CalcNode::Negate(ref mut value) => {
+ // Don't negate the value here. Replace `self` with it's child.
+ let result = mem::replace(value.as_mut(), Self::dummy());
+ *self = result;
+ },
+ CalcNode::Invert(_) => {
+ // -(1 / -10) == -(-0.1) == 0.1
+ wrap_self_in_negate(self)
+ },
+ CalcNode::Sum(ref mut children) => {
+ for child in children.iter_mut() {
+ child.negate();
+ }
+ },
+ CalcNode::Product(_) => {
+ // -(2 * 3 / 4) == -(1.5)
+ wrap_self_in_negate(self);
+ },
+ CalcNode::MinMax(ref mut children, ref mut op) => {
+ for child in children.iter_mut() {
+ child.negate();
+ }
+
+ // Negating min-max means the operation is swapped.
+ *op = match *op {
+ MinMaxOp::Min => MinMaxOp::Max,
+ MinMaxOp::Max => MinMaxOp::Min,
+ };
+ },
+ CalcNode::Clamp {
+ ref mut min,
+ ref mut center,
+ ref mut max,
+ } => {
+ if min.lte(max, PositivePercentageBasis::Unknown) {
+ min.negate();
+ center.negate();
+ max.negate();
+
+ mem::swap(min, max);
+ } else {
+ wrap_self_in_negate(self);
+ }
+ },
+ CalcNode::Round {
+ ref mut strategy,
+ ref mut value,
+ ref mut step,
+ } => {
+ match *strategy {
+ RoundingStrategy::Nearest => {
+ // Nearest is tricky because we'd have to swap the
+ // behavior at the half-way point from using the upper
+ // to lower bound.
+ // Simpler to just wrap self in a negate node.
+ wrap_self_in_negate(self);
+ return;
+ },
+ RoundingStrategy::Up => *strategy = RoundingStrategy::Down,
+ RoundingStrategy::Down => *strategy = RoundingStrategy::Up,
+ RoundingStrategy::ToZero => (),
+ }
+ value.negate();
+ step.negate();
+ },
+ CalcNode::ModRem {
+ ref mut dividend,
+ ref mut divisor,
+ ..
+ } => {
+ dividend.negate();
+ divisor.negate();
+ },
+ CalcNode::Hypot(ref mut children) => {
+ for child in children.iter_mut() {
+ child.negate();
+ }
+ },
+ CalcNode::Abs(_) => {
+ wrap_self_in_negate(self);
+ },
+ CalcNode::Sign(ref mut child) => {
+ child.negate();
+ },
+ }
+ }
+
+ fn sort_key(&self) -> SortKey {
+ match *self {
+ Self::Leaf(ref l) => l.sort_key(),
+ _ => SortKey::Other,
+ }
+ }
+
+ /// Returns the leaf if we can (if simplification has allowed it).
+ pub fn as_leaf(&self) -> Option<&L> {
+ match *self {
+ Self::Leaf(ref l) => Some(l),
+ _ => None,
+ }
+ }
+
+ /// Tries to merge one node into another using the sum, that is, perform `x` + `y`.
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> {
+ match (self, other) {
+ (&mut CalcNode::Leaf(ref mut one), &CalcNode::Leaf(ref other)) => {
+ one.try_sum_in_place(other)
+ },
+ _ => Err(()),
+ }
+ }
+
+ /// Tries to merge one node into another using the product, that is, perform `x` * `y`.
+ pub fn try_product_in_place(&mut self, other: &mut Self) -> bool {
+ if let Ok(resolved) = other.resolve() {
+ if let Some(number) = resolved.as_number() {
+ if number == 1.0 {
+ return true;
+ }
+
+ if self.is_product_distributive() {
+ self.map(|v| v * number);
+ return true;
+ }
+ }
+ }
+
+ if let Ok(resolved) = self.resolve() {
+ if let Some(number) = resolved.as_number() {
+ if number == 1.0 {
+ std::mem::swap(self, other);
+ return true;
+ }
+
+ if other.is_product_distributive() {
+ other.map(|v| v * number);
+ std::mem::swap(self, other);
+ return true;
+ }
+ }
+ }
+
+ false
+ }
+
+ /// Tries to apply a generic arithmetic operator
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ match (self, other) {
+ (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => {
+ Ok(CalcNode::Leaf(one.try_op(other, op)?))
+ },
+ _ => Err(()),
+ }
+ }
+
+ /// Map the value of this node with the given operation.
+ pub fn map(&mut self, mut op: impl FnMut(f32) -> f32) {
+ fn map_internal<L: CalcNodeLeaf>(node: &mut CalcNode<L>, op: &mut impl FnMut(f32) -> f32) {
+ match node {
+ CalcNode::Leaf(l) => l.map(op),
+ CalcNode::Negate(v) | CalcNode::Invert(v) => map_internal(v, op),
+ CalcNode::Sum(children) | CalcNode::Product(children) => {
+ for node in &mut **children {
+ map_internal(node, op);
+ }
+ },
+ CalcNode::MinMax(children, _) => {
+ for node in &mut **children {
+ map_internal(node, op);
+ }
+ },
+ CalcNode::Clamp { min, center, max } => {
+ map_internal(min, op);
+ map_internal(center, op);
+ map_internal(max, op);
+ },
+ CalcNode::Round { value, step, .. } => {
+ map_internal(value, op);
+ map_internal(step, op);
+ },
+ CalcNode::ModRem {
+ dividend, divisor, ..
+ } => {
+ map_internal(dividend, op);
+ map_internal(divisor, op);
+ },
+ CalcNode::Hypot(children) => {
+ for node in &mut **children {
+ map_internal(node, op);
+ }
+ },
+ CalcNode::Abs(child) | CalcNode::Sign(child) => {
+ map_internal(child, op);
+ },
+ }
+ }
+
+ map_internal(self, &mut op);
+ }
+
+ /// Convert this `CalcNode` into a `CalcNode` with a different leaf kind.
+ pub fn map_leaves<O, F>(&self, mut map: F) -> CalcNode<O>
+ where
+ O: CalcNodeLeaf,
+ F: FnMut(&L) -> O,
+ {
+ self.map_leaves_internal(&mut map)
+ }
+
+ fn map_leaves_internal<O, F>(&self, map: &mut F) -> CalcNode<O>
+ where
+ O: CalcNodeLeaf,
+ F: FnMut(&L) -> O,
+ {
+ fn map_children<L, O, F>(
+ children: &[CalcNode<L>],
+ map: &mut F,
+ ) -> crate::OwnedSlice<CalcNode<O>>
+ where
+ L: CalcNodeLeaf,
+ O: CalcNodeLeaf,
+ F: FnMut(&L) -> O,
+ {
+ children
+ .iter()
+ .map(|c| c.map_leaves_internal(map))
+ .collect()
+ }
+
+ match *self {
+ Self::Leaf(ref l) => CalcNode::Leaf(map(l)),
+ Self::Negate(ref c) => CalcNode::Negate(Box::new(c.map_leaves_internal(map))),
+ Self::Invert(ref c) => CalcNode::Invert(Box::new(c.map_leaves_internal(map))),
+ Self::Sum(ref c) => CalcNode::Sum(map_children(c, map)),
+ Self::Product(ref c) => CalcNode::Product(map_children(c, map)),
+ Self::MinMax(ref c, op) => CalcNode::MinMax(map_children(c, map), op),
+ Self::Clamp {
+ ref min,
+ ref center,
+ ref max,
+ } => {
+ let min = Box::new(min.map_leaves_internal(map));
+ let center = Box::new(center.map_leaves_internal(map));
+ let max = Box::new(max.map_leaves_internal(map));
+ CalcNode::Clamp { min, center, max }
+ },
+ Self::Round {
+ strategy,
+ ref value,
+ ref step,
+ } => {
+ let value = Box::new(value.map_leaves_internal(map));
+ let step = Box::new(step.map_leaves_internal(map));
+ CalcNode::Round {
+ strategy,
+ value,
+ step,
+ }
+ },
+ Self::ModRem {
+ ref dividend,
+ ref divisor,
+ op,
+ } => {
+ let dividend = Box::new(dividend.map_leaves_internal(map));
+ let divisor = Box::new(divisor.map_leaves_internal(map));
+ CalcNode::ModRem {
+ dividend,
+ divisor,
+ op,
+ }
+ },
+ Self::Hypot(ref c) => CalcNode::Hypot(map_children(c, map)),
+ Self::Abs(ref c) => CalcNode::Abs(Box::new(c.map_leaves_internal(map))),
+ Self::Sign(ref c) => CalcNode::Sign(Box::new(c.map_leaves_internal(map))),
+ }
+ }
+
+ /// Resolve this node into a value.
+ pub fn resolve(&self) -> Result<L, ()> {
+ self.resolve_map(|l| Ok(l.clone()))
+ }
+
+ /// Resolve this node into a value, given a function that maps the leaf values.
+ pub fn resolve_map<F>(&self, mut leaf_to_output_fn: F) -> Result<L, ()>
+ where
+ F: FnMut(&L) -> Result<L, ()>,
+ {
+ self.resolve_internal(&mut leaf_to_output_fn)
+ }
+
+ fn resolve_internal<F>(&self, leaf_to_output_fn: &mut F) -> Result<L, ()>
+ where
+ F: FnMut(&L) -> Result<L, ()>,
+ {
+ match self {
+ Self::Leaf(l) => leaf_to_output_fn(l),
+ Self::Negate(child) => {
+ let mut result = child.resolve_internal(leaf_to_output_fn)?;
+ result.map(|v| v.neg());
+ Ok(result)
+ },
+ Self::Invert(child) => {
+ let mut result = child.resolve_internal(leaf_to_output_fn)?;
+ result.map(|v| 1.0 / v);
+ Ok(result)
+ },
+ Self::Sum(children) => {
+ let mut result = children[0].resolve_internal(leaf_to_output_fn)?;
+
+ for child in children.iter().skip(1) {
+ let right = child.resolve_internal(leaf_to_output_fn)?;
+ // try_op will make sure we only sum leaves with the same type.
+ result = result.try_op(&right, |left, right| left + right)?;
+ }
+
+ Ok(result)
+ },
+ Self::Product(children) => {
+ let mut result = children[0].resolve_internal(leaf_to_output_fn)?;
+
+ for child in children.iter().skip(1) {
+ let right = child.resolve_internal(leaf_to_output_fn)?;
+ // Mutliply only allowed when either side is a number.
+ match result.as_number() {
+ Some(left) => {
+ // Left side is a number, so we use the right node as the result.
+ result = right;
+ result.map(|v| v * left);
+ },
+ None => {
+ // Left side is not a number, so check if the right side is.
+ match right.as_number() {
+ Some(right) => {
+ result.map(|v| v * right);
+ },
+ None => {
+ // Multiplying with both sides having units.
+ return Err(());
+ },
+ }
+ },
+ }
+ }
+
+ Ok(result)
+ },
+ Self::MinMax(children, op) => {
+ let mut result = children[0].resolve_internal(leaf_to_output_fn)?;
+
+ if result.is_nan() {
+ return Ok(result);
+ }
+
+ for child in children.iter().skip(1) {
+ let candidate = child.resolve_internal(leaf_to_output_fn)?;
+
+ // Leave types must match for each child.
+ if !result.is_same_unit_as(&candidate) {
+ return Err(());
+ }
+
+ if candidate.is_nan() {
+ result = candidate;
+ break;
+ }
+
+ let candidate_wins = match op {
+ MinMaxOp::Min => candidate.lt(&result, PositivePercentageBasis::Yes),
+ MinMaxOp::Max => candidate.gt(&result, PositivePercentageBasis::Yes),
+ };
+
+ if candidate_wins {
+ result = candidate;
+ }
+ }
+
+ Ok(result)
+ },
+ Self::Clamp { min, center, max } => {
+ let min = min.resolve_internal(leaf_to_output_fn)?;
+ let center = center.resolve_internal(leaf_to_output_fn)?;
+ let max = max.resolve_internal(leaf_to_output_fn)?;
+
+ if !min.is_same_unit_as(&center) || !max.is_same_unit_as(&center) {
+ return Err(());
+ }
+
+ if min.is_nan() {
+ return Ok(min);
+ }
+
+ if center.is_nan() {
+ return Ok(center);
+ }
+
+ if max.is_nan() {
+ return Ok(max);
+ }
+
+ let mut result = center;
+ if result.gt(&max, PositivePercentageBasis::Yes) {
+ result = max;
+ }
+ if result.lt(&min, PositivePercentageBasis::Yes) {
+ result = min
+ }
+
+ Ok(result)
+ },
+ Self::Round {
+ strategy,
+ value,
+ step,
+ } => {
+ let mut value = value.resolve_internal(leaf_to_output_fn)?;
+ let step = step.resolve_internal(leaf_to_output_fn)?;
+
+ if !value.is_same_unit_as(&step) {
+ return Err(());
+ }
+
+ let step = step.unitless_value().abs();
+
+ value.map(|value| {
+ // TODO(emilio): Seems like at least a few of these
+ // special-cases could be removed if we do the math in a
+ // particular order.
+ if step.is_zero() {
+ return f32::NAN;
+ }
+
+ if value.is_infinite() {
+ if step.is_infinite() {
+ return f32::NAN;
+ }
+ return value;
+ }
+
+ if step.is_infinite() {
+ match strategy {
+ RoundingStrategy::Nearest | RoundingStrategy::ToZero => {
+ return if value.is_sign_negative() { -0.0 } else { 0.0 }
+ },
+ RoundingStrategy::Up => {
+ return if !value.is_sign_negative() && !value.is_zero() {
+ f32::INFINITY
+ } else if !value.is_sign_negative() && value.is_zero() {
+ value
+ } else {
+ -0.0
+ }
+ },
+ RoundingStrategy::Down => {
+ return if value.is_sign_negative() && !value.is_zero() {
+ -f32::INFINITY
+ } else if value.is_sign_negative() && value.is_zero() {
+ value
+ } else {
+ 0.0
+ }
+ },
+ }
+ }
+
+ let div = value / step;
+ let lower_bound = div.floor() * step;
+ let upper_bound = div.ceil() * step;
+
+ match strategy {
+ RoundingStrategy::Nearest => {
+ // In case of a tie, use the upper bound
+ if value - lower_bound < upper_bound - value {
+ lower_bound
+ } else {
+ upper_bound
+ }
+ },
+ RoundingStrategy::Up => upper_bound,
+ RoundingStrategy::Down => lower_bound,
+ RoundingStrategy::ToZero => {
+ // In case of a tie, use the upper bound
+ if lower_bound.abs() < upper_bound.abs() {
+ lower_bound
+ } else {
+ upper_bound
+ }
+ },
+ }
+ });
+
+ Ok(value)
+ },
+ Self::ModRem {
+ dividend,
+ divisor,
+ op,
+ } => {
+ let mut dividend = dividend.resolve_internal(leaf_to_output_fn)?;
+ let divisor = divisor.resolve_internal(leaf_to_output_fn)?;
+
+ if !dividend.is_same_unit_as(&divisor) {
+ return Err(());
+ }
+
+ let divisor = divisor.unitless_value();
+ dividend.map(|dividend| op.apply(dividend, divisor));
+ Ok(dividend)
+ },
+ Self::Hypot(children) => {
+ let mut result = children[0].resolve_internal(leaf_to_output_fn)?;
+ result.map(|v| v.powi(2));
+
+ for child in children.iter().skip(1) {
+ let child_value = child.resolve_internal(leaf_to_output_fn)?;
+
+ if !result.is_same_unit_as(&child_value) {
+ return Err(());
+ }
+
+ result.map(|v| v + child_value.unitless_value().powi(2));
+ }
+
+ result.map(|v| v.sqrt());
+ Ok(result)
+ },
+ Self::Abs(ref c) => {
+ let mut result = c.resolve_internal(leaf_to_output_fn)?;
+
+ result.map(|v| v.abs());
+
+ Ok(result)
+ },
+ Self::Sign(ref c) => {
+ let result = c.resolve_internal(leaf_to_output_fn)?;
+ Ok(L::sign_from(&result))
+ },
+ }
+ }
+
+ fn is_negative_leaf(&self) -> bool {
+ match *self {
+ Self::Leaf(ref l) => l.is_negative(),
+ _ => false,
+ }
+ }
+
+ fn is_zero_leaf(&self) -> bool {
+ match *self {
+ Self::Leaf(ref l) => l.is_zero(),
+ _ => false,
+ }
+ }
+
+ fn is_infinite_leaf(&self) -> bool {
+ match *self {
+ Self::Leaf(ref l) => l.is_infinite(),
+ _ => false,
+ }
+ }
+
+ /// Visits all the nodes in this calculation tree recursively, starting by
+ /// the leaves and bubbling all the way up.
+ ///
+ /// This is useful for simplification, but can also be used for validation
+ /// and such.
+ pub fn visit_depth_first(&mut self, mut f: impl FnMut(&mut Self)) {
+ self.visit_depth_first_internal(&mut f)
+ }
+
+ fn visit_depth_first_internal(&mut self, f: &mut impl FnMut(&mut Self)) {
+ match *self {
+ Self::Clamp {
+ ref mut min,
+ ref mut center,
+ ref mut max,
+ } => {
+ min.visit_depth_first_internal(f);
+ center.visit_depth_first_internal(f);
+ max.visit_depth_first_internal(f);
+ },
+ Self::Round {
+ ref mut value,
+ ref mut step,
+ ..
+ } => {
+ value.visit_depth_first_internal(f);
+ step.visit_depth_first_internal(f);
+ },
+ Self::ModRem {
+ ref mut dividend,
+ ref mut divisor,
+ ..
+ } => {
+ dividend.visit_depth_first_internal(f);
+ divisor.visit_depth_first_internal(f);
+ },
+ Self::Sum(ref mut children) |
+ Self::Product(ref mut children) |
+ Self::MinMax(ref mut children, _) |
+ Self::Hypot(ref mut children) => {
+ for child in &mut **children {
+ child.visit_depth_first_internal(f);
+ }
+ },
+ Self::Negate(ref mut value) | Self::Invert(ref mut value) => {
+ value.visit_depth_first_internal(f);
+ },
+ Self::Abs(ref mut value) | Self::Sign(ref mut value) => {
+ value.visit_depth_first_internal(f);
+ },
+ Self::Leaf(..) => {},
+ }
+ f(self);
+ }
+
+ /// This function simplifies and sorts the calculation of the specified node. It simplifies
+ /// directly nested nodes while assuming that all nodes below it have already been simplified.
+ /// It is recommended to use this function in combination with `visit_depth_first()`.
+ ///
+ /// This function is necessary only if the node needs to be preserved after parsing,
+ /// specifically for `<length-percentage>` cases where the calculation contains percentages or
+ /// relative units. Otherwise, the node can be evaluated using `resolve()`, which will
+ /// automatically provide a simplified value.
+ ///
+ /// <https://drafts.csswg.org/css-values-4/#calc-simplification>
+ pub fn simplify_and_sort_direct_children(&mut self) {
+ macro_rules! replace_self_with {
+ ($slot:expr) => {{
+ let result = mem::replace($slot, Self::dummy());
+ *self = result;
+ }};
+ }
+
+ macro_rules! value_or_stop {
+ ($op:expr) => {{
+ match $op {
+ Ok(value) => value,
+ Err(_) => return,
+ }
+ }};
+ }
+
+ match *self {
+ Self::Clamp {
+ ref mut min,
+ ref mut center,
+ ref mut max,
+ } => {
+ // NOTE: clamp() is max(min, min(center, max))
+ let min_cmp_center = match min.compare(&center, PositivePercentageBasis::Unknown) {
+ Some(o) => o,
+ None => return,
+ };
+
+ // So if we can prove that min is more than center, then we won,
+ // as that's what we should always return.
+ if matches!(min_cmp_center, cmp::Ordering::Greater) {
+ replace_self_with!(&mut **min);
+ return;
+ }
+
+ // Otherwise try with max.
+ let max_cmp_center = match max.compare(&center, PositivePercentageBasis::Unknown) {
+ Some(o) => o,
+ None => return,
+ };
+
+ if matches!(max_cmp_center, cmp::Ordering::Less) {
+ // max is less than center, so we need to return effectively
+ // `max(min, max)`.
+ let max_cmp_min = match max.compare(&min, PositivePercentageBasis::Unknown) {
+ Some(o) => o,
+ None => {
+ debug_assert!(
+ false,
+ "We compared center with min and max, how are \
+ min / max not comparable with each other?"
+ );
+ return;
+ },
+ };
+
+ if matches!(max_cmp_min, cmp::Ordering::Less) {
+ replace_self_with!(&mut **min);
+ return;
+ }
+
+ replace_self_with!(&mut **max);
+ return;
+ }
+
+ // Otherwise we're the center node.
+ replace_self_with!(&mut **center);
+ },
+ Self::Round {
+ strategy,
+ ref mut value,
+ ref mut step,
+ } => {
+ if step.is_zero_leaf() {
+ value.coerce_to_value(f32::NAN);
+ replace_self_with!(&mut **value);
+ return;
+ }
+
+ if value.is_infinite_leaf() && step.is_infinite_leaf() {
+ value.coerce_to_value(f32::NAN);
+ replace_self_with!(&mut **value);
+ return;
+ }
+
+ if value.is_infinite_leaf() {
+ replace_self_with!(&mut **value);
+ return;
+ }
+
+ if step.is_infinite_leaf() {
+ match strategy {
+ RoundingStrategy::Nearest | RoundingStrategy::ToZero => {
+ value.coerce_to_value(0.0);
+ replace_self_with!(&mut **value);
+ return;
+ },
+ RoundingStrategy::Up => {
+ if !value.is_negative_leaf() && !value.is_zero_leaf() {
+ value.coerce_to_value(f32::INFINITY);
+ replace_self_with!(&mut **value);
+ return;
+ } else if !value.is_negative_leaf() && value.is_zero_leaf() {
+ replace_self_with!(&mut **value);
+ return;
+ } else {
+ value.coerce_to_value(0.0);
+ replace_self_with!(&mut **value);
+ return;
+ }
+ },
+ RoundingStrategy::Down => {
+ if value.is_negative_leaf() && !value.is_zero_leaf() {
+ value.coerce_to_value(f32::INFINITY);
+ replace_self_with!(&mut **value);
+ return;
+ } else if value.is_negative_leaf() && value.is_zero_leaf() {
+ replace_self_with!(&mut **value);
+ return;
+ } else {
+ value.coerce_to_value(0.0);
+ replace_self_with!(&mut **value);
+ return;
+ }
+ },
+ }
+ }
+
+ if step.is_negative_leaf() {
+ step.negate();
+ }
+
+ let remainder = value_or_stop!(value.try_op(step, Rem::rem));
+ if remainder.is_zero_leaf() {
+ replace_self_with!(&mut **value);
+ return;
+ }
+
+ let (mut lower_bound, mut upper_bound) = if value.is_negative_leaf() {
+ let upper_bound = value_or_stop!(value.try_op(&remainder, Sub::sub));
+ let lower_bound = value_or_stop!(upper_bound.try_op(&step, Sub::sub));
+
+ (lower_bound, upper_bound)
+ } else {
+ let lower_bound = value_or_stop!(value.try_op(&remainder, Sub::sub));
+ let upper_bound = value_or_stop!(lower_bound.try_op(&step, Add::add));
+
+ (lower_bound, upper_bound)
+ };
+
+ match strategy {
+ RoundingStrategy::Nearest => {
+ let lower_diff = value_or_stop!(value.try_op(&lower_bound, Sub::sub));
+ let upper_diff = value_or_stop!(upper_bound.try_op(value, Sub::sub));
+ // In case of a tie, use the upper bound
+ if lower_diff.lt(&upper_diff, PositivePercentageBasis::Unknown) {
+ replace_self_with!(&mut lower_bound);
+ } else {
+ replace_self_with!(&mut upper_bound);
+ }
+ },
+ RoundingStrategy::Up => {
+ replace_self_with!(&mut upper_bound);
+ },
+ RoundingStrategy::Down => {
+ replace_self_with!(&mut lower_bound);
+ },
+ RoundingStrategy::ToZero => {
+ let mut lower_diff = lower_bound.clone();
+ let mut upper_diff = upper_bound.clone();
+
+ if lower_diff.is_negative_leaf() {
+ lower_diff.negate();
+ }
+
+ if upper_diff.is_negative_leaf() {
+ upper_diff.negate();
+ }
+
+ // In case of a tie, use the upper bound
+ if lower_diff.lt(&upper_diff, PositivePercentageBasis::Unknown) {
+ replace_self_with!(&mut lower_bound);
+ } else {
+ replace_self_with!(&mut upper_bound);
+ }
+ },
+ };
+ },
+ Self::ModRem {
+ ref dividend,
+ ref divisor,
+ op,
+ } => {
+ let mut result = value_or_stop!(dividend.try_op(divisor, |a, b| op.apply(a, b)));
+ replace_self_with!(&mut result);
+ },
+ Self::MinMax(ref mut children, op) => {
+ let winning_order = match op {
+ MinMaxOp::Min => cmp::Ordering::Less,
+ MinMaxOp::Max => cmp::Ordering::Greater,
+ };
+
+ let mut result = 0;
+ for i in 1..children.len() {
+ let o = match children[i]
+ .compare(&children[result], PositivePercentageBasis::Unknown)
+ {
+ // We can't compare all the children, so we can't
+ // know which one will actually win. Bail out and
+ // keep ourselves as a min / max function.
+ //
+ // TODO: Maybe we could simplify compatible children,
+ // see https://github.com/w3c/csswg-drafts/issues/4756
+ None => return,
+ Some(o) => o,
+ };
+
+ if o == winning_order {
+ result = i;
+ }
+ }
+
+ replace_self_with!(&mut children[result]);
+ },
+ Self::Sum(ref mut children_slot) => {
+ let mut sums_to_merge = SmallVec::<[_; 3]>::new();
+ let mut extra_kids = 0;
+ for (i, child) in children_slot.iter().enumerate() {
+ if let Self::Sum(ref children) = *child {
+ extra_kids += children.len();
+ sums_to_merge.push(i);
+ }
+ }
+
+ // If we only have one kid, we've already simplified it, and it
+ // doesn't really matter whether it's a sum already or not, so
+ // lift it up and continue.
+ if children_slot.len() == 1 {
+ replace_self_with!(&mut children_slot[0]);
+ return;
+ }
+
+ let mut children = mem::take(children_slot).into_vec();
+
+ if !sums_to_merge.is_empty() {
+ children.reserve(extra_kids - sums_to_merge.len());
+ // Merge all our nested sums, in reverse order so that the
+ // list indices are not invalidated.
+ for i in sums_to_merge.drain(..).rev() {
+ let kid_children = match children.swap_remove(i) {
+ Self::Sum(c) => c,
+ _ => unreachable!(),
+ };
+
+ // This would be nicer with
+ // https://github.com/rust-lang/rust/issues/59878 fixed.
+ children.extend(kid_children.into_vec());
+ }
+ }
+
+ debug_assert!(children.len() >= 2, "Should still have multiple kids!");
+
+ // Sort by spec order.
+ children.sort_unstable_by_key(|c| c.sort_key());
+
+ // NOTE: if the function returns true, by the docs of dedup_by,
+ // a is removed.
+ children.dedup_by(|a, b| b.try_sum_in_place(a).is_ok());
+
+ if children.len() == 1 {
+ // If only one children remains, lift it up, and carry on.
+ replace_self_with!(&mut children[0]);
+ } else {
+ // Else put our simplified children back.
+ *children_slot = children.into_boxed_slice().into();
+ }
+ },
+ Self::Product(ref mut children_slot) => {
+ let mut products_to_merge = SmallVec::<[_; 3]>::new();
+ let mut extra_kids = 0;
+ for (i, child) in children_slot.iter().enumerate() {
+ if let Self::Product(ref children) = *child {
+ extra_kids += children.len();
+ products_to_merge.push(i);
+ }
+ }
+
+ // If we only have one kid, we've already simplified it, and it
+ // doesn't really matter whether it's a product already or not,
+ // so lift it up and continue.
+ if children_slot.len() == 1 {
+ replace_self_with!(&mut children_slot[0]);
+ return;
+ }
+
+ let mut children = mem::take(children_slot).into_vec();
+
+ if !products_to_merge.is_empty() {
+ children.reserve(extra_kids - products_to_merge.len());
+ // Merge all our nested sums, in reverse order so that the
+ // list indices are not invalidated.
+ for i in products_to_merge.drain(..).rev() {
+ let kid_children = match children.swap_remove(i) {
+ Self::Product(c) => c,
+ _ => unreachable!(),
+ };
+
+ // This would be nicer with
+ // https://github.com/rust-lang/rust/issues/59878 fixed.
+ children.extend(kid_children.into_vec());
+ }
+ }
+
+ debug_assert!(children.len() >= 2, "Should still have multiple kids!");
+
+ // NOTE: if the function returns true, by the docs of dedup_by,
+ // a is removed.
+ children.dedup_by(|right, left| left.try_product_in_place(right));
+
+ if children.len() == 1 {
+ // If only one children remains, lift it up, and carry on.
+ replace_self_with!(&mut children[0]);
+ } else {
+ // Else put our simplified children back.
+ *children_slot = children.into_boxed_slice().into();
+ }
+ },
+ Self::Hypot(ref children) => {
+ let mut result = value_or_stop!(children[0].try_op(&children[0], Mul::mul));
+
+ for child in children.iter().skip(1) {
+ let square = value_or_stop!(child.try_op(&child, Mul::mul));
+ result = value_or_stop!(result.try_op(&square, Add::add));
+ }
+
+ result = value_or_stop!(result.try_op(&result, |a, _| a.sqrt()));
+
+ replace_self_with!(&mut result);
+ },
+ Self::Abs(ref mut child) => {
+ if let CalcNode::Leaf(leaf) = child.as_mut() {
+ leaf.map(|v| v.abs());
+ replace_self_with!(&mut **child);
+ }
+ },
+ Self::Sign(ref mut child) => {
+ if let CalcNode::Leaf(leaf) = child.as_mut() {
+ let mut result = Self::Leaf(L::sign_from(leaf));
+ replace_self_with!(&mut result);
+ }
+ },
+ Self::Negate(ref mut child) => {
+ // Step 6.
+ match &mut **child {
+ CalcNode::Leaf(_) => {
+ // 1. If root’s child is a numeric value, return an equivalent numeric value, but
+ // with the value negated (0 - value).
+ child.negate();
+ replace_self_with!(&mut **child);
+ },
+ CalcNode::Negate(value) => {
+ // 2. If root’s child is a Negate node, return the child’s child.
+ replace_self_with!(&mut **value);
+ },
+ _ => {
+ // 3. Return root.
+ },
+ }
+ },
+ Self::Invert(ref mut child) => {
+ // Step 7.
+ match &mut **child {
+ CalcNode::Leaf(leaf) => {
+ // 1. If root’s child is a number (not a percentage or dimension) return the
+ // reciprocal of the child’s value.
+ if leaf.unit().is_empty() {
+ child.map(|v| 1.0 / v);
+ replace_self_with!(&mut **child);
+ }
+ },
+ CalcNode::Invert(value) => {
+ // 2. If root’s child is an Invert node, return the child’s child.
+ replace_self_with!(&mut **value);
+ },
+ _ => {
+ // 3. Return root.
+ },
+ }
+ },
+ Self::Leaf(ref mut l) => {
+ l.simplify();
+ },
+ }
+ }
+
+ /// Simplifies and sorts the kids in the whole calculation subtree.
+ pub fn simplify_and_sort(&mut self) {
+ self.visit_depth_first(|node| node.simplify_and_sort_direct_children())
+ }
+
+ fn to_css_impl<W>(&self, dest: &mut CssWriter<W>, level: ArgumentLevel) -> fmt::Result
+ where
+ W: Write,
+ {
+ let write_closing_paren = match *self {
+ Self::MinMax(_, op) => {
+ dest.write_str(match op {
+ MinMaxOp::Max => "max(",
+ MinMaxOp::Min => "min(",
+ })?;
+ true
+ },
+ Self::Clamp { .. } => {
+ dest.write_str("clamp(")?;
+ true
+ },
+ Self::Round { strategy, .. } => {
+ match strategy {
+ RoundingStrategy::Nearest => dest.write_str("round("),
+ RoundingStrategy::Up => dest.write_str("round(up, "),
+ RoundingStrategy::Down => dest.write_str("round(down, "),
+ RoundingStrategy::ToZero => dest.write_str("round(to-zero, "),
+ }?;
+
+ true
+ },
+ Self::ModRem { op, .. } => {
+ dest.write_str(match op {
+ ModRemOp::Mod => "mod(",
+ ModRemOp::Rem => "rem(",
+ })?;
+
+ true
+ },
+ Self::Hypot(_) => {
+ dest.write_str("hypot(")?;
+ true
+ },
+ Self::Abs(_) => {
+ dest.write_str("abs(")?;
+ true
+ },
+ Self::Sign(_) => {
+ dest.write_str("sign(")?;
+ true
+ },
+ Self::Negate(_) => {
+ // We never generate a [`Negate`] node as the root of a calculation, only inside
+ // [`Sum`] nodes as a child. Because negate nodes are handled by the [`Sum`] node
+ // directly (see below), this node will never be serialized.
+ debug_assert!(
+ false,
+ "We never serialize Negate nodes as they are handled inside Sum nodes."
+ );
+ dest.write_str("(-1 * ")?;
+ true
+ },
+ Self::Invert(_) => {
+ dest.write_str("(1 / ")?;
+ true
+ },
+ Self::Sum(_) | Self::Product(_) => match level {
+ ArgumentLevel::CalculationRoot => {
+ dest.write_str("calc(")?;
+ true
+ },
+ ArgumentLevel::ArgumentRoot => false,
+ ArgumentLevel::Nested => {
+ dest.write_str("(")?;
+ true
+ },
+ },
+ Self::Leaf(_) => match level {
+ ArgumentLevel::CalculationRoot => {
+ dest.write_str("calc(")?;
+ true
+ },
+ ArgumentLevel::ArgumentRoot | ArgumentLevel::Nested => false,
+ },
+ };
+
+ match *self {
+ Self::MinMax(ref children, _) | Self::Hypot(ref children) => {
+ let mut first = true;
+ for child in &**children {
+ if !first {
+ dest.write_str(", ")?;
+ }
+ first = false;
+ child.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ }
+ },
+ Self::Negate(ref value) | Self::Invert(ref value) => {
+ value.to_css_impl(dest, ArgumentLevel::Nested)?
+ },
+ Self::Sum(ref children) => {
+ let mut first = true;
+ for child in &**children {
+ if !first {
+ match child {
+ Self::Leaf(l) => {
+ if l.is_negative() {
+ dest.write_str(" - ")?;
+ let mut negated = l.clone();
+ negated.negate();
+ negated.to_css(dest)?;
+ } else {
+ dest.write_str(" + ")?;
+ l.to_css(dest)?;
+ }
+ },
+ Self::Negate(n) => {
+ dest.write_str(" - ")?;
+ n.to_css_impl(dest, ArgumentLevel::Nested)?;
+ },
+ _ => {
+ dest.write_str(" + ")?;
+ child.to_css_impl(dest, ArgumentLevel::Nested)?;
+ },
+ }
+ } else {
+ first = false;
+ child.to_css_impl(dest, ArgumentLevel::Nested)?;
+ }
+ }
+ },
+ Self::Product(ref children) => {
+ let mut first = true;
+ for child in &**children {
+ if !first {
+ match child {
+ Self::Invert(n) => {
+ dest.write_str(" / ")?;
+ n.to_css_impl(dest, ArgumentLevel::Nested)?;
+ },
+ _ => {
+ dest.write_str(" * ")?;
+ child.to_css_impl(dest, ArgumentLevel::Nested)?;
+ },
+ }
+ } else {
+ first = false;
+ child.to_css_impl(dest, ArgumentLevel::Nested)?;
+ }
+ }
+ },
+ Self::Clamp {
+ ref min,
+ ref center,
+ ref max,
+ } => {
+ min.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ dest.write_str(", ")?;
+ center.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ dest.write_str(", ")?;
+ max.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ },
+ Self::Round {
+ ref value,
+ ref step,
+ ..
+ } => {
+ value.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ dest.write_str(", ")?;
+ step.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ },
+ Self::ModRem {
+ ref dividend,
+ ref divisor,
+ ..
+ } => {
+ dividend.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ dest.write_str(", ")?;
+ divisor.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ },
+ Self::Abs(ref v) | Self::Sign(ref v) => {
+ v.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?
+ },
+ Self::Leaf(ref l) => l.to_css(dest)?,
+ }
+
+ if write_closing_paren {
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+
+ fn compare(
+ &self,
+ other: &Self,
+ basis_positive: PositivePercentageBasis,
+ ) -> Option<cmp::Ordering> {
+ match (self, other) {
+ (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => {
+ one.compare(other, basis_positive)
+ },
+ _ => None,
+ }
+ }
+
+ compare_helpers!();
+}
+
+impl<L: CalcNodeLeaf> ToCss for CalcNode<L> {
+ /// <https://drafts.csswg.org/css-values/#calc-serialize>
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.to_css_impl(dest, ArgumentLevel::CalculationRoot)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn can_sum_with_checks() {
+ assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::LENGTH));
+ assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::PERCENTAGE));
+ assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::LENGTH_PERCENTAGE));
+
+ assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::LENGTH));
+ assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::PERCENTAGE));
+ assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::LENGTH_PERCENTAGE));
+
+ assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::LENGTH));
+ assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::PERCENTAGE));
+ assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::LENGTH_PERCENTAGE));
+
+ assert!(!CalcUnits::ANGLE.can_sum_with(CalcUnits::TIME));
+ assert!(CalcUnits::ANGLE.can_sum_with(CalcUnits::ANGLE));
+
+ assert!(!(CalcUnits::ANGLE | CalcUnits::TIME).can_sum_with(CalcUnits::ANGLE));
+ assert!(!CalcUnits::ANGLE.can_sum_with(CalcUnits::ANGLE | CalcUnits::TIME));
+ assert!(
+ !(CalcUnits::ANGLE | CalcUnits::TIME).can_sum_with(CalcUnits::ANGLE | CalcUnits::TIME)
+ );
+ }
+}
diff --git a/servo/components/style/values/generics/color.rs b/servo/components/style/values/generics/color.rs
new file mode 100644
index 0000000000..e37cabdc59
--- /dev/null
+++ b/servo/components/style/values/generics/color.rs
@@ -0,0 +1,209 @@
+/* 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/. */
+
+//! Generic types for color properties.
+
+use crate::color::mix::ColorInterpolationMethod;
+use crate::color::AbsoluteColor;
+use crate::values::specified::percentage::ToPercentage;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// This struct represents a combined color from a numeric color and
+/// the current foreground color (currentcolor keyword).
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
+#[repr(C)]
+pub enum GenericColor<Percentage> {
+ /// The actual numeric color.
+ Absolute(AbsoluteColor),
+ /// The `CurrentColor` keyword.
+ CurrentColor,
+ /// The color-mix() function.
+ ColorMix(Box<GenericColorMix<Self, Percentage>>),
+}
+
+/// Flags used to modify the calculation of a color mix result.
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
+#[repr(C)]
+pub struct ColorMixFlags(u8);
+bitflags! {
+ impl ColorMixFlags : u8 {
+ /// Normalize the weights of the mix.
+ const NORMALIZE_WEIGHTS = 1 << 0;
+ /// The result should always be converted to the modern color syntax.
+ const RESULT_IN_MODERN_SYNTAX = 1 << 1;
+ }
+}
+
+/// A restricted version of the css `color-mix()` function, which only supports
+/// percentages.
+///
+/// https://drafts.csswg.org/css-color-5/#color-mix
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(C)]
+pub struct GenericColorMix<Color, Percentage> {
+ pub interpolation: ColorInterpolationMethod,
+ pub left: Color,
+ pub left_percentage: Percentage,
+ pub right: Color,
+ pub right_percentage: Percentage,
+ pub flags: ColorMixFlags,
+}
+
+pub use self::GenericColorMix as ColorMix;
+
+impl<Color: ToCss, Percentage: ToCss + ToPercentage> ToCss for ColorMix<Color, Percentage> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ fn can_omit<Percentage: ToPercentage>(
+ percent: &Percentage,
+ other: &Percentage,
+ is_left: bool,
+ ) -> bool {
+ if percent.is_calc() {
+ return false;
+ }
+ if percent.to_percentage() == 0.5 {
+ return other.to_percentage() == 0.5;
+ }
+ if is_left {
+ return false;
+ }
+ (1.0 - percent.to_percentage() - other.to_percentage()).abs() <= f32::EPSILON
+ }
+
+ dest.write_str("color-mix(")?;
+ self.interpolation.to_css(dest)?;
+ dest.write_str(", ")?;
+ self.left.to_css(dest)?;
+ if !can_omit(&self.left_percentage, &self.right_percentage, true) {
+ dest.write_char(' ')?;
+ self.left_percentage.to_css(dest)?;
+ }
+ dest.write_str(", ")?;
+ self.right.to_css(dest)?;
+ if !can_omit(&self.right_percentage, &self.left_percentage, false) {
+ dest.write_char(' ')?;
+ self.right_percentage.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<Percentage> ColorMix<GenericColor<Percentage>, Percentage> {
+ /// Mix the colors so that we get a single color. If any of the 2 colors are
+ /// not mixable (perhaps not absolute?), then return None.
+ pub fn mix_to_absolute(&self) -> Option<AbsoluteColor>
+ where
+ Percentage: ToPercentage,
+ {
+ let left = self.left.as_absolute()?;
+ let right = self.right.as_absolute()?;
+
+ Some(crate::color::mix::mix(
+ self.interpolation,
+ &left,
+ self.left_percentage.to_percentage(),
+ &right,
+ self.right_percentage.to_percentage(),
+ self.flags,
+ ))
+ }
+}
+
+pub use self::GenericColor as Color;
+
+impl<Percentage> Color<Percentage> {
+ /// If this color is absolute return it's value, otherwise return None.
+ pub fn as_absolute(&self) -> Option<&AbsoluteColor> {
+ match *self {
+ Self::Absolute(ref absolute) => Some(absolute),
+ _ => None,
+ }
+ }
+
+ /// Returns a color value representing currentcolor.
+ pub fn currentcolor() -> Self {
+ Self::CurrentColor
+ }
+
+ /// Whether it is a currentcolor value (no numeric color component).
+ pub fn is_currentcolor(&self) -> bool {
+ matches!(*self, Self::CurrentColor)
+ }
+
+ /// Whether this color is an absolute color.
+ pub fn is_absolute(&self) -> bool {
+ matches!(*self, Self::Absolute(..))
+ }
+}
+
+/// Either `<color>` or `auto`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToCss,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericColorOrAuto<C> {
+ /// A `<color>`.
+ Color(C),
+ /// `auto`
+ Auto,
+}
+
+pub use self::GenericColorOrAuto as ColorOrAuto;
+
+/// Caret color is effectively a ColorOrAuto, but resolves `auto` to
+/// currentColor.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCaretColor<C>(pub GenericColorOrAuto<C>);
+
+impl<C> GenericCaretColor<C> {
+ /// Returns the `auto` value.
+ pub fn auto() -> Self {
+ GenericCaretColor(GenericColorOrAuto::Auto)
+ }
+}
+
+pub use self::GenericCaretColor as CaretColor;
diff --git a/servo/components/style/values/generics/column.rs b/servo/components/style/values/generics/column.rs
new file mode 100644
index 0000000000..4b5f0e0399
--- /dev/null
+++ b/servo/components/style/values/generics/column.rs
@@ -0,0 +1,45 @@
+/* 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/. */
+
+//! Generic types for the column properties.
+
+/// A generic type for `column-count` values.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum ColumnCount<PositiveInteger> {
+ /// A positive integer.
+ Integer(PositiveInteger),
+ /// The keyword `auto`.
+ #[animation(error)]
+ Auto,
+}
+
+impl<I> ColumnCount<I> {
+ /// Returns `auto`.
+ #[inline]
+ pub fn auto() -> Self {
+ ColumnCount::Auto
+ }
+
+ /// Returns whether this value is `auto`.
+ #[inline]
+ pub fn is_auto(self) -> bool {
+ matches!(self, ColumnCount::Auto)
+ }
+}
diff --git a/servo/components/style/values/generics/counters.rs b/servo/components/style/values/generics/counters.rs
new file mode 100644
index 0000000000..1d4518c57b
--- /dev/null
+++ b/servo/components/style/values/generics/counters.rs
@@ -0,0 +1,295 @@
+/* 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/. */
+
+//! Generic types for counters-related CSS values.
+
+#[cfg(feature = "servo-layout-2013")]
+use crate::computed_values::list_style_type::T as ListStyleType;
+#[cfg(feature = "gecko")]
+use crate::values::generics::CounterStyle;
+#[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+use crate::values::specified::Attr;
+use crate::values::CustomIdent;
+use std::fmt::{self, Write};
+use std::ops::Deref;
+use style_traits::{CssWriter, ToCss};
+
+/// A name / value pair for counters.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericCounterPair<Integer> {
+ /// The name of the counter.
+ pub name: CustomIdent,
+ /// The value of the counter / increment / etc.
+ pub value: Integer,
+ /// If true, then this represents `reversed(name)`.
+ /// NOTE: It can only be true on `counter-reset` values.
+ pub is_reversed: bool,
+}
+pub use self::GenericCounterPair as CounterPair;
+
+impl<Integer> ToCss for CounterPair<Integer>
+where
+ Integer: ToCss + PartialEq<i32>,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_reversed {
+ dest.write_str("reversed(")?;
+ }
+ self.name.to_css(dest)?;
+ if self.is_reversed {
+ dest.write_char(')')?;
+ if self.value == i32::min_value() {
+ return Ok(());
+ }
+ }
+ dest.write_char(' ')?;
+ self.value.to_css(dest)
+ }
+}
+
+/// A generic value for the `counter-increment` property.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCounterIncrement<I>(#[css(field_bound)] pub GenericCounters<I>);
+pub use self::GenericCounterIncrement as CounterIncrement;
+
+impl<I> CounterIncrement<I> {
+ /// Returns a new value for `counter-increment`.
+ #[inline]
+ pub fn new(counters: Vec<CounterPair<I>>) -> Self {
+ CounterIncrement(Counters(counters.into()))
+ }
+}
+
+impl<I> Deref for CounterIncrement<I> {
+ type Target = [CounterPair<I>];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &(self.0).0
+ }
+}
+
+/// A generic value for the `counter-set` property.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCounterSet<I>(#[css(field_bound)] pub GenericCounters<I>);
+pub use self::GenericCounterSet as CounterSet;
+
+impl<I> CounterSet<I> {
+ /// Returns a new value for `counter-set`.
+ #[inline]
+ pub fn new(counters: Vec<CounterPair<I>>) -> Self {
+ CounterSet(Counters(counters.into()))
+ }
+}
+
+impl<I> Deref for CounterSet<I> {
+ type Target = [CounterPair<I>];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &(self.0).0
+ }
+}
+
+/// A generic value for the `counter-reset` property.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCounterReset<I>(#[css(field_bound)] pub GenericCounters<I>);
+pub use self::GenericCounterReset as CounterReset;
+
+impl<I> CounterReset<I> {
+ /// Returns a new value for `counter-reset`.
+ #[inline]
+ pub fn new(counters: Vec<CounterPair<I>>) -> Self {
+ CounterReset(Counters(counters.into()))
+ }
+}
+
+impl<I> Deref for CounterReset<I> {
+ type Target = [CounterPair<I>];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &(self.0).0
+ }
+}
+
+/// A generic value for lists of counters.
+///
+/// Keyword `none` is represented by an empty vector.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCounters<I>(
+ #[css(field_bound)]
+ #[css(iterable, if_empty = "none")]
+ crate::OwnedSlice<GenericCounterPair<I>>,
+);
+pub use self::GenericCounters as Counters;
+
+#[cfg(feature = "servo-layout-2013")]
+type CounterStyleType = ListStyleType;
+
+#[cfg(feature = "gecko")]
+type CounterStyleType = CounterStyle;
+
+#[cfg(feature = "servo-layout-2013")]
+#[inline]
+fn is_decimal(counter_type: &CounterStyleType) -> bool {
+ *counter_type == ListStyleType::Decimal
+}
+
+#[cfg(feature = "gecko")]
+#[inline]
+fn is_decimal(counter_type: &CounterStyleType) -> bool {
+ *counter_type == CounterStyle::decimal()
+}
+
+/// The specified value for the `content` property.
+///
+/// https://drafts.csswg.org/css-content/#propdef-content
+#[derive(
+ Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToShmem,
+)]
+#[repr(u8)]
+pub enum GenericContent<Image> {
+ /// `normal` reserved keyword.
+ Normal,
+ /// `none` reserved keyword.
+ None,
+ /// Content items.
+ Items(#[css(iterable)] crate::OwnedSlice<GenericContentItem<Image>>),
+}
+
+pub use self::GenericContent as Content;
+
+impl<Image> Content<Image> {
+ /// Whether `self` represents list of items.
+ #[inline]
+ pub fn is_items(&self) -> bool {
+ matches!(*self, Self::Items(..))
+ }
+
+ /// Set `content` property to `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ Content::Normal
+ }
+}
+
+/// Items for the `content` property.
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ SpecifiedValueInfo,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum GenericContentItem<I> {
+ /// Literal string content.
+ String(crate::OwnedStr),
+ /// `counter(name, style)`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ #[css(comma, function)]
+ Counter(CustomIdent, #[css(skip_if = "is_decimal")] CounterStyleType),
+ /// `counters(name, separator, style)`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ #[css(comma, function)]
+ Counters(
+ CustomIdent,
+ crate::OwnedStr,
+ #[css(skip_if = "is_decimal")] CounterStyleType,
+ ),
+ /// `open-quote`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ OpenQuote,
+ /// `close-quote`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ CloseQuote,
+ /// `no-open-quote`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ NoOpenQuote,
+ /// `no-close-quote`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ NoCloseQuote,
+ /// `-moz-alt-content`.
+ #[cfg(feature = "gecko")]
+ MozAltContent,
+ /// `-moz-label-content`.
+ /// This is needed to make `accesskey` work for XUL labels. It's basically
+ /// attr(value) otherwise.
+ #[cfg(feature = "gecko")]
+ MozLabelContent,
+ /// `attr([namespace? `|`]? ident)`
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+ Attr(Attr),
+ /// image-set(url) | url(url)
+ Image(I),
+}
+
+pub use self::GenericContentItem as ContentItem;
diff --git a/servo/components/style/values/generics/easing.rs b/servo/components/style/values/generics/easing.rs
new file mode 100644
index 0000000000..e04b49a4be
--- /dev/null
+++ b/servo/components/style/values/generics/easing.rs
@@ -0,0 +1,143 @@
+/* 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/. */
+
+//! Generic types for CSS Easing Functions.
+//! https://drafts.csswg.org/css-easing/#timing-functions
+
+use crate::parser::ParserContext;
+
+/// A generic easing function.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[value_info(ty = "TIMING_FUNCTION")]
+#[repr(u8, C)]
+pub enum TimingFunction<Integer, Number, LinearStops> {
+ /// `linear | ease | ease-in | ease-out | ease-in-out`
+ Keyword(TimingKeyword),
+ /// `cubic-bezier(<number>, <number>, <number>, <number>)`
+ #[allow(missing_docs)]
+ #[css(comma, function)]
+ CubicBezier {
+ x1: Number,
+ y1: Number,
+ x2: Number,
+ y2: Number,
+ },
+ /// `step-start | step-end | steps(<integer>, [ <step-position> ]?)`
+ /// `<step-position> = jump-start | jump-end | jump-none | jump-both | start | end`
+ #[css(comma, function)]
+ #[value_info(other_values = "step-start,step-end")]
+ Steps(Integer, #[css(skip_if = "is_end")] StepPosition),
+ /// linear([<linear-stop>]#)
+ /// <linear-stop> = <output> && <linear-stop-length>?
+ /// <linear-stop-length> = <percentage>{1, 2}
+ #[css(function = "linear")]
+ LinearFunction(LinearStops),
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[repr(u8)]
+pub enum TimingKeyword {
+ Linear,
+ Ease,
+ EaseIn,
+ EaseOut,
+ EaseInOut,
+}
+
+/// Before flag, defined as per https://drafts.csswg.org/css-easing/#before-flag
+/// This flag is never user-specified.
+#[allow(missing_docs)]
+#[derive(PartialEq)]
+#[repr(u8)]
+pub enum BeforeFlag {
+ Unset,
+ Set,
+}
+
+#[cfg(feature = "gecko")]
+fn step_position_jump_enabled(_context: &ParserContext) -> bool {
+ true
+}
+
+#[cfg(feature = "servo")]
+fn step_position_jump_enabled(_context: &ParserContext) -> bool {
+ false
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[repr(u8)]
+pub enum StepPosition {
+ #[parse(condition = "step_position_jump_enabled")]
+ JumpStart,
+ #[parse(condition = "step_position_jump_enabled")]
+ JumpEnd,
+ #[parse(condition = "step_position_jump_enabled")]
+ JumpNone,
+ #[parse(condition = "step_position_jump_enabled")]
+ JumpBoth,
+ Start,
+ End,
+}
+
+#[inline]
+fn is_end(position: &StepPosition) -> bool {
+ *position == StepPosition::JumpEnd || *position == StepPosition::End
+}
+
+impl<Integer, Number, LinearStops> TimingFunction<Integer, Number, LinearStops> {
+ /// `ease`
+ #[inline]
+ pub fn ease() -> Self {
+ TimingFunction::Keyword(TimingKeyword::Ease)
+ }
+
+ /// Returns true if it is `ease`.
+ #[inline]
+ pub fn is_ease(&self) -> bool {
+ matches!(*self, TimingFunction::Keyword(TimingKeyword::Ease))
+ }
+}
diff --git a/servo/components/style/values/generics/effects.rs b/servo/components/style/values/generics/effects.rs
new file mode 100644
index 0000000000..f5666f3055
--- /dev/null
+++ b/servo/components/style/values/generics/effects.rs
@@ -0,0 +1,121 @@
+/* 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/. */
+
+//! Generic types for CSS values related to effects.
+
+/// A generic value for a single `box-shadow`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericBoxShadow<Color, SizeLength, BlurShapeLength, ShapeLength> {
+ /// The base shadow.
+ pub base: GenericSimpleShadow<Color, SizeLength, BlurShapeLength>,
+ /// The spread radius.
+ pub spread: ShapeLength,
+ /// Whether this is an inset box shadow.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub inset: bool,
+}
+
+pub use self::GenericBoxShadow as BoxShadow;
+
+/// A generic value for a single `filter`.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(U))]
+#[repr(C, u8)]
+pub enum GenericFilter<Angle, NonNegativeFactor, ZeroToOneFactor, Length, Shadow, U> {
+ /// `blur(<length>)`
+ #[css(function)]
+ Blur(Length),
+ /// `brightness(<factor>)`
+ #[css(function)]
+ Brightness(NonNegativeFactor),
+ /// `contrast(<factor>)`
+ #[css(function)]
+ Contrast(NonNegativeFactor),
+ /// `grayscale(<factor>)`
+ #[css(function)]
+ Grayscale(ZeroToOneFactor),
+ /// `hue-rotate(<angle>)`
+ #[css(function)]
+ HueRotate(Angle),
+ /// `invert(<factor>)`
+ #[css(function)]
+ Invert(ZeroToOneFactor),
+ /// `opacity(<factor>)`
+ #[css(function)]
+ Opacity(ZeroToOneFactor),
+ /// `saturate(<factor>)`
+ #[css(function)]
+ Saturate(NonNegativeFactor),
+ /// `sepia(<factor>)`
+ #[css(function)]
+ Sepia(ZeroToOneFactor),
+ /// `drop-shadow(...)`
+ #[css(function)]
+ DropShadow(Shadow),
+ /// `<url>`
+ #[animation(error)]
+ Url(U),
+}
+
+pub use self::GenericFilter as Filter;
+
+/// A generic value for the `drop-shadow()` filter and the `text-shadow` property.
+///
+/// Contrary to the canonical order from the spec, the color is serialised
+/// first, like in Gecko and Webkit.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericSimpleShadow<Color, SizeLength, ShapeLength> {
+ /// Color.
+ pub color: Color,
+ /// Horizontal radius.
+ pub horizontal: SizeLength,
+ /// Vertical radius.
+ pub vertical: SizeLength,
+ /// Blur radius.
+ pub blur: ShapeLength,
+}
+
+pub use self::GenericSimpleShadow as SimpleShadow;
diff --git a/servo/components/style/values/generics/flex.rs b/servo/components/style/values/generics/flex.rs
new file mode 100644
index 0000000000..85b64000f2
--- /dev/null
+++ b/servo/components/style/values/generics/flex.rs
@@ -0,0 +1,33 @@
+/* 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/. */
+
+//! Generic types for CSS values related to flexbox.
+
+/// A generic value for the `flex-basis` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub enum GenericFlexBasis<S> {
+ /// `content`
+ Content,
+ /// `<width>`
+ Size(S),
+}
+
+pub use self::GenericFlexBasis as FlexBasis;
diff --git a/servo/components/style/values/generics/font.rs b/servo/components/style/values/generics/font.rs
new file mode 100644
index 0000000000..91dd2d8515
--- /dev/null
+++ b/servo/components/style/values/generics/font.rs
@@ -0,0 +1,316 @@
+/* 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/. */
+
+//! Generic types for font stuff.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::animated::ToAnimatedZero;
+use crate::One;
+use byteorder::{BigEndian, ReadBytesExt};
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use std::io::Cursor;
+use style_traits::{CssWriter, ParseError};
+use style_traits::{StyleParseErrorKind, ToCss};
+
+/// A trait for values that are labelled with a FontTag (for feature and
+/// variation settings).
+pub trait TaggedFontValue {
+ /// The value's tag.
+ fn tag(&self) -> FontTag;
+}
+
+/// https://drafts.csswg.org/css-fonts-4/#feature-tag-value
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct FeatureTagValue<Integer> {
+ /// A four-character tag, packed into a u32 (one byte per character).
+ pub tag: FontTag,
+ /// The actual value.
+ pub value: Integer,
+}
+
+impl<T> TaggedFontValue for FeatureTagValue<T> {
+ fn tag(&self) -> FontTag {
+ self.tag
+ }
+}
+
+impl<Integer> ToCss for FeatureTagValue<Integer>
+where
+ Integer: One + ToCss + PartialEq,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.tag.to_css(dest)?;
+ // Don't serialize the default value.
+ if !self.value.is_one() {
+ dest.write_char(' ')?;
+ self.value.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+}
+
+/// Variation setting for a single feature, see:
+///
+/// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct VariationValue<Number> {
+ /// A four-character tag, packed into a u32 (one byte per character).
+ #[animation(constant)]
+ pub tag: FontTag,
+ /// The actual value.
+ pub value: Number,
+}
+
+impl<T> TaggedFontValue for VariationValue<T> {
+ fn tag(&self) -> FontTag {
+ self.tag
+ }
+}
+
+/// A value both for font-variation-settings and font-feature-settings.
+#[derive(
+ Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToResolvedValue, ToShmem,
+)]
+#[css(comma)]
+pub struct FontSettings<T>(#[css(if_empty = "normal", iterable)] pub Box<[T]>);
+
+impl<T> FontSettings<T> {
+ /// Default value of font settings as `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ FontSettings(vec![].into_boxed_slice())
+ }
+}
+
+impl<T: Parse> Parse for FontSettings<T> {
+ /// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-feature-settings
+ /// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(Self::normal());
+ }
+
+ Ok(FontSettings(
+ input
+ .parse_comma_separated(|i| T::parse(context, i))?
+ .into_boxed_slice(),
+ ))
+ }
+}
+
+/// A font four-character tag, represented as a u32 for convenience.
+///
+/// See:
+/// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def
+/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-feature-settings
+///
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct FontTag(pub u32);
+
+impl ToCss for FontTag {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ use byteorder::ByteOrder;
+ use std::str;
+
+ let mut raw = [0u8; 4];
+ BigEndian::write_u32(&mut raw, self.0);
+ str::from_utf8(&raw).unwrap_or_default().to_css(dest)
+ }
+}
+
+impl Parse for FontTag {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let tag = input.expect_string()?;
+
+ // allowed strings of length 4 containing chars: <U+20, U+7E>
+ if tag.len() != 4 || tag.as_bytes().iter().any(|c| *c < b' ' || *c > b'~') {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let mut raw = Cursor::new(tag.as_bytes());
+ Ok(FontTag(raw.read_u32::<BigEndian>().unwrap()))
+ }
+}
+
+/// A generic value for the `font-style` property.
+///
+/// https://drafts.csswg.org/css-fonts-4/#font-style-prop
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum FontStyle<Angle> {
+ #[animation(error)]
+ Normal,
+ #[animation(error)]
+ Italic,
+ #[value_info(starts_with_keyword)]
+ Oblique(Angle),
+}
+
+/// A generic value for the `font-size-adjust` property.
+///
+/// https://drafts.csswg.org/css-fonts-5/#font-size-adjust-prop
+#[allow(missing_docs)]
+#[repr(u8)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum GenericFontSizeAdjust<Factor> {
+ #[animation(error)]
+ None,
+ #[value_info(starts_with_keyword)]
+ ExHeight(Factor),
+ #[value_info(starts_with_keyword)]
+ CapHeight(Factor),
+ #[value_info(starts_with_keyword)]
+ ChWidth(Factor),
+ #[value_info(starts_with_keyword)]
+ IcWidth(Factor),
+ #[value_info(starts_with_keyword)]
+ IcHeight(Factor),
+}
+
+impl<Factor: ToCss> ToCss for GenericFontSizeAdjust<Factor> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let (prefix, value) = match self {
+ Self::None => return dest.write_str("none"),
+ Self::ExHeight(v) => ("", v),
+ Self::CapHeight(v) => ("cap-height ", v),
+ Self::ChWidth(v) => ("ch-width ", v),
+ Self::IcWidth(v) => ("ic-width ", v),
+ Self::IcHeight(v) => ("ic-height ", v),
+ };
+
+ dest.write_str(prefix)?;
+ value.to_css(dest)
+ }
+}
+
+/// A generic value for the `line-height` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToCss,
+ ToShmem,
+ Parse,
+)]
+#[repr(C, u8)]
+pub enum GenericLineHeight<N, L> {
+ /// `normal`
+ Normal,
+ /// `-moz-block-height`
+ #[cfg(feature = "gecko")]
+ #[parse(condition = "ParserContext::in_ua_sheet")]
+ MozBlockHeight,
+ /// `<number>`
+ Number(N),
+ /// `<length-percentage>`
+ Length(L),
+}
+
+pub use self::GenericLineHeight as LineHeight;
+
+impl<N, L> ToAnimatedZero for LineHeight<N, L> {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+impl<N, L> LineHeight<N, L> {
+ /// Returns `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ LineHeight::Normal
+ }
+}
diff --git a/servo/components/style/values/generics/grid.rs b/servo/components/style/values/generics/grid.rs
new file mode 100644
index 0000000000..22fe249c83
--- /dev/null
+++ b/servo/components/style/values/generics/grid.rs
@@ -0,0 +1,867 @@
+/* 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/. */
+
+//! Generic types for the handling of
+//! [grids](https://drafts.csswg.org/css-grid/).
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::specified;
+use crate::values::{CSSFloat, CustomIdent};
+use crate::{One, Zero};
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use std::{cmp, usize};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// These are the limits that we choose to clamp grid line numbers to.
+/// http://drafts.csswg.org/css-grid/#overlarge-grids
+/// line_num is clamped to this range at parse time.
+pub const MIN_GRID_LINE: i32 = -10000;
+/// See above.
+pub const MAX_GRID_LINE: i32 = 10000;
+
+/// A `<grid-line>` type.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-grid-row-start-grid-line>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericGridLine<Integer> {
+ /// A custom identifier for named lines, or the empty atom otherwise.
+ ///
+ /// <https://drafts.csswg.org/css-grid/#grid-placement-slot>
+ pub ident: CustomIdent,
+ /// Denotes the nth grid line from grid item's placement.
+ ///
+ /// This is clamped by MIN_GRID_LINE and MAX_GRID_LINE.
+ ///
+ /// NOTE(emilio): If we ever allow animating these we need to either do
+ /// something more complicated for the clamping, or do this clamping at
+ /// used-value time.
+ pub line_num: Integer,
+ /// Flag to check whether it's a `span` keyword.
+ pub is_span: bool,
+}
+
+pub use self::GenericGridLine as GridLine;
+
+impl<Integer> GridLine<Integer>
+where
+ Integer: PartialEq + Zero,
+{
+ /// The `auto` value.
+ pub fn auto() -> Self {
+ Self {
+ is_span: false,
+ line_num: Zero::zero(),
+ ident: CustomIdent(atom!("")),
+ }
+ }
+
+ /// Check whether this `<grid-line>` represents an `auto` value.
+ pub fn is_auto(&self) -> bool {
+ self.ident.0 == atom!("") && self.line_num.is_zero() && !self.is_span
+ }
+
+ /// Check whether this `<grid-line>` represents a `<custom-ident>` value.
+ pub fn is_ident_only(&self) -> bool {
+ self.ident.0 != atom!("") && self.line_num.is_zero() && !self.is_span
+ }
+
+ /// Check if `self` makes `other` omittable according to the rules at:
+ /// https://drafts.csswg.org/css-grid/#propdef-grid-column
+ /// https://drafts.csswg.org/css-grid/#propdef-grid-area
+ pub fn can_omit(&self, other: &Self) -> bool {
+ if self.is_ident_only() {
+ self == other
+ } else {
+ other.is_auto()
+ }
+ }
+}
+
+impl<Integer> ToCss for GridLine<Integer>
+where
+ Integer: ToCss + PartialEq + Zero + One,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ // 1. `auto`
+ if self.is_auto() {
+ return dest.write_str("auto");
+ }
+
+ // 2. `<custom-ident>`
+ if self.is_ident_only() {
+ return self.ident.to_css(dest);
+ }
+
+ // 3. `[ span && [ <integer [1,∞]> || <custom-ident> ] ]`
+ let has_ident = self.ident.0 != atom!("");
+ if self.is_span {
+ dest.write_str("span")?;
+ debug_assert!(!self.line_num.is_zero() || has_ident);
+
+ // We omit `line_num` if
+ // 1. we don't specify it, or
+ // 2. it is the default value, i.e. 1.0, and the ident is specified.
+ // https://drafts.csswg.org/css-grid/#grid-placement-span-int
+ if !self.line_num.is_zero() && !(self.line_num.is_one() && has_ident) {
+ dest.write_char(' ')?;
+ self.line_num.to_css(dest)?;
+ }
+
+ if has_ident {
+ dest.write_char(' ')?;
+ self.ident.to_css(dest)?;
+ }
+ return Ok(());
+ }
+
+ // 4. `[ <integer [-∞,-1]> | <integer [1,∞]> ] && <custom-ident>? ]`
+ debug_assert!(!self.line_num.is_zero());
+ self.line_num.to_css(dest)?;
+ if has_ident {
+ dest.write_char(' ')?;
+ self.ident.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl Parse for GridLine<specified::Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut grid_line = Self::auto();
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ return Ok(grid_line);
+ }
+
+ // <custom-ident> | [ <integer> && <custom-ident>? ] | [ span && [ <integer> || <custom-ident> ] ]
+ // This <grid-line> horror is simply,
+ // [ span? && [ <custom-ident> || <integer> ] ]
+ // And, for some magical reason, "span" should be the first or last value and not in-between.
+ let mut val_before_span = false;
+
+ for _ in 0..3 {
+ // Maximum possible entities for <grid-line>
+ let location = input.current_source_location();
+ if input.try_parse(|i| i.expect_ident_matching("span")).is_ok() {
+ if grid_line.is_span {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if !grid_line.line_num.is_zero() || grid_line.ident.0 != atom!("") {
+ val_before_span = true;
+ }
+
+ grid_line.is_span = true;
+ } else if let Ok(i) = input.try_parse(|i| specified::Integer::parse(context, i)) {
+ // FIXME(emilio): Probably shouldn't reject if it's calc()...
+ let value = i.value();
+ if value == 0 || val_before_span || !grid_line.line_num.is_zero() {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ grid_line.line_num = specified::Integer::new(cmp::max(
+ MIN_GRID_LINE,
+ cmp::min(value, MAX_GRID_LINE),
+ ));
+ } else if let Ok(name) = input.try_parse(|i| CustomIdent::parse(i, &["auto"])) {
+ if val_before_span || grid_line.ident.0 != atom!("") {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ // NOTE(emilio): `span` is consumed above, so we only need to
+ // reject `auto`.
+ grid_line.ident = name;
+ } else {
+ break;
+ }
+ }
+
+ if grid_line.is_auto() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if grid_line.is_span {
+ if !grid_line.line_num.is_zero() {
+ if grid_line.line_num.value() <= 0 {
+ // disallow negative integers for grid spans
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ } else if grid_line.ident.0 == atom!("") {
+ // integer could be omitted
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ Ok(grid_line)
+ }
+}
+
+/// A track breadth for explicit grid track sizing. It's generic solely to
+/// avoid re-implementing it for the computed type.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-breadth>
+#[derive(
+ Animate,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericTrackBreadth<L> {
+ /// The generic type is almost always a non-negative `<length-percentage>`
+ Breadth(L),
+ /// A flex fraction specified in `fr` units.
+ #[css(dimension)]
+ Fr(CSSFloat),
+ /// `auto`
+ Auto,
+ /// `min-content`
+ MinContent,
+ /// `max-content`
+ MaxContent,
+}
+
+pub use self::GenericTrackBreadth as TrackBreadth;
+
+impl<L> TrackBreadth<L> {
+ /// Check whether this is a `<fixed-breadth>` (i.e., it only has `<length-percentage>`)
+ ///
+ /// <https://drafts.csswg.org/css-grid/#typedef-fixed-breadth>
+ #[inline]
+ pub fn is_fixed(&self) -> bool {
+ matches!(*self, TrackBreadth::Breadth(..))
+ }
+}
+
+/// A `<track-size>` type for explicit grid track sizing. Like `<track-breadth>`, this is
+/// generic only to avoid code bloat. It only takes `<length-percentage>`
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-size>
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericTrackSize<L> {
+ /// A flexible `<track-breadth>`
+ Breadth(GenericTrackBreadth<L>),
+ /// A `minmax` function for a range over an inflexible `<track-breadth>`
+ /// and a flexible `<track-breadth>`
+ ///
+ /// <https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-minmax>
+ #[css(function)]
+ Minmax(GenericTrackBreadth<L>, GenericTrackBreadth<L>),
+ /// A `fit-content` function.
+ ///
+ /// This stores a TrackBreadth<L> for convenience, but it can only be a
+ /// LengthPercentage.
+ ///
+ /// <https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-fit-content>
+ #[css(function)]
+ FitContent(GenericTrackBreadth<L>),
+}
+
+pub use self::GenericTrackSize as TrackSize;
+
+impl<L> TrackSize<L> {
+ /// The initial value.
+ const INITIAL_VALUE: Self = TrackSize::Breadth(TrackBreadth::Auto);
+
+ /// Returns the initial value.
+ pub const fn initial_value() -> Self {
+ Self::INITIAL_VALUE
+ }
+
+ /// Returns true if `self` is the initial value.
+ pub fn is_initial(&self) -> bool {
+ matches!(*self, TrackSize::Breadth(TrackBreadth::Auto)) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585
+ }
+
+ /// Check whether this is a `<fixed-size>`
+ ///
+ /// <https://drafts.csswg.org/css-grid/#typedef-fixed-size>
+ pub fn is_fixed(&self) -> bool {
+ match *self {
+ TrackSize::Breadth(ref breadth) => breadth.is_fixed(),
+ // For minmax function, it could be either
+ // minmax(<fixed-breadth>, <track-breadth>) or minmax(<inflexible-breadth>, <fixed-breadth>),
+ // and since both variants are a subset of minmax(<inflexible-breadth>, <track-breadth>), we only
+ // need to make sure that they're fixed. So, we don't have to modify the parsing function.
+ TrackSize::Minmax(ref breadth_1, ref breadth_2) => {
+ if breadth_1.is_fixed() {
+ return true; // the second value is always a <track-breadth>
+ }
+
+ match *breadth_1 {
+ TrackBreadth::Fr(_) => false, // should be <inflexible-breadth> at this point
+ _ => breadth_2.is_fixed(),
+ }
+ },
+ TrackSize::FitContent(_) => false,
+ }
+ }
+}
+
+impl<L> Default for TrackSize<L> {
+ fn default() -> Self {
+ Self::initial_value()
+ }
+}
+
+impl<L: ToCss> ToCss for TrackSize<L> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ TrackSize::Breadth(ref breadth) => breadth.to_css(dest),
+ TrackSize::Minmax(ref min, ref max) => {
+ // According to gecko minmax(auto, <flex>) is equivalent to <flex>,
+ // and both are serialized as <flex>.
+ if let TrackBreadth::Auto = *min {
+ if let TrackBreadth::Fr(_) = *max {
+ return max.to_css(dest);
+ }
+ }
+
+ dest.write_str("minmax(")?;
+ min.to_css(dest)?;
+ dest.write_str(", ")?;
+ max.to_css(dest)?;
+ dest.write_char(')')
+ },
+ TrackSize::FitContent(ref lp) => {
+ dest.write_str("fit-content(")?;
+ lp.to_css(dest)?;
+ dest.write_char(')')
+ },
+ }
+ }
+}
+
+/// A `<track-size>+`.
+/// We use the empty slice as `auto`, and always parse `auto` as an empty slice.
+/// This means it's impossible to have a slice containing only one auto item.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericImplicitGridTracks<T>(
+ #[css(if_empty = "auto", iterable)] pub crate::OwnedSlice<T>,
+);
+
+pub use self::GenericImplicitGridTracks as ImplicitGridTracks;
+
+impl<T: fmt::Debug + Default + PartialEq> ImplicitGridTracks<T> {
+ /// Returns true if current value is same as its initial value (i.e. auto).
+ pub fn is_initial(&self) -> bool {
+ debug_assert_ne!(
+ *self,
+ ImplicitGridTracks(crate::OwnedSlice::from(vec![Default::default()]))
+ );
+ self.0.is_empty()
+ }
+}
+
+/// Helper function for serializing identifiers with a prefix and suffix, used
+/// for serializing <line-names> (in grid).
+pub fn concat_serialize_idents<W>(
+ prefix: &str,
+ suffix: &str,
+ slice: &[CustomIdent],
+ sep: &str,
+ dest: &mut CssWriter<W>,
+) -> fmt::Result
+where
+ W: Write,
+{
+ if let Some((ref first, rest)) = slice.split_first() {
+ dest.write_str(prefix)?;
+ first.to_css(dest)?;
+ for thing in rest {
+ dest.write_str(sep)?;
+ thing.to_css(dest)?;
+ }
+
+ dest.write_str(suffix)?;
+ }
+
+ Ok(())
+}
+
+/// The initial argument of the `repeat` function.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-repeat>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum RepeatCount<Integer> {
+ /// A positive integer. This is allowed only for `<track-repeat>` and `<fixed-repeat>`
+ Number(Integer),
+ /// An `<auto-fill>` keyword allowed only for `<auto-repeat>`
+ AutoFill,
+ /// An `<auto-fit>` keyword allowed only for `<auto-repeat>`
+ AutoFit,
+}
+
+impl Parse for RepeatCount<specified::Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(mut i) = input.try_parse(|i| specified::Integer::parse_positive(context, i)) {
+ if i.value() > MAX_GRID_LINE {
+ i = specified::Integer::new(MAX_GRID_LINE);
+ }
+ return Ok(RepeatCount::Number(i));
+ }
+ try_match_ident_ignore_ascii_case! { input,
+ "auto-fill" => Ok(RepeatCount::AutoFill),
+ "auto-fit" => Ok(RepeatCount::AutoFit),
+ }
+ }
+}
+
+/// The structure containing `<line-names>` and `<track-size>` values.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "repeat")]
+#[repr(C)]
+pub struct GenericTrackRepeat<L, I> {
+ /// The number of times for the value to be repeated (could also be `auto-fit` or `auto-fill`)
+ pub count: RepeatCount<I>,
+ /// `<line-names>` accompanying `<track_size>` values.
+ ///
+ /// If there's no `<line-names>`, then it's represented by an empty vector.
+ /// For N `<track-size>` values, there will be N+1 `<line-names>`, and so this vector's
+ /// length is always one value more than that of the `<track-size>`.
+ pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>,
+ /// `<track-size>` values.
+ pub track_sizes: crate::OwnedSlice<GenericTrackSize<L>>,
+}
+
+pub use self::GenericTrackRepeat as TrackRepeat;
+
+impl<L: ToCss, I: ToCss> ToCss for TrackRepeat<L, I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("repeat(")?;
+ self.count.to_css(dest)?;
+ dest.write_str(", ")?;
+
+ let mut line_names_iter = self.line_names.iter();
+ for (i, (ref size, ref names)) in self
+ .track_sizes
+ .iter()
+ .zip(&mut line_names_iter)
+ .enumerate()
+ {
+ if i > 0 {
+ dest.write_char(' ')?;
+ }
+
+ concat_serialize_idents("[", "] ", names, " ", dest)?;
+ size.to_css(dest)?;
+ }
+
+ if let Some(line_names_last) = line_names_iter.next() {
+ concat_serialize_idents(" [", "]", line_names_last, " ", dest)?;
+ }
+
+ dest.write_char(')')?;
+
+ Ok(())
+ }
+}
+
+/// Track list values. Can be <track-size> or <track-repeat>
+#[derive(
+ Animate,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericTrackListValue<LengthPercentage, Integer> {
+ /// A <track-size> value.
+ TrackSize(#[animation(field_bound)] GenericTrackSize<LengthPercentage>),
+ /// A <track-repeat> value.
+ TrackRepeat(#[animation(field_bound)] GenericTrackRepeat<LengthPercentage, Integer>),
+}
+
+pub use self::GenericTrackListValue as TrackListValue;
+
+impl<L, I> TrackListValue<L, I> {
+ // FIXME: can't use TrackSize::initial_value() here b/c rustc error "is not yet stable as a const fn"
+ const INITIAL_VALUE: Self = TrackListValue::TrackSize(TrackSize::Breadth(TrackBreadth::Auto));
+
+ fn is_repeat(&self) -> bool {
+ matches!(*self, TrackListValue::TrackRepeat(..))
+ }
+
+ /// Returns true if `self` is the initial value.
+ pub fn is_initial(&self) -> bool {
+ matches!(
+ *self,
+ TrackListValue::TrackSize(TrackSize::Breadth(TrackBreadth::Auto))
+ ) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585
+ }
+}
+
+impl<L, I> Default for TrackListValue<L, I> {
+ #[inline]
+ fn default() -> Self {
+ Self::INITIAL_VALUE
+ }
+}
+
+/// A grid `<track-list>` type.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-list>
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericTrackList<LengthPercentage, Integer> {
+ /// The index in `values` where our `<auto-repeat>` value is, if in bounds.
+ #[css(skip)]
+ pub auto_repeat_index: usize,
+ /// A vector of `<track-size> | <track-repeat>` values.
+ pub values: crate::OwnedSlice<GenericTrackListValue<LengthPercentage, Integer>>,
+ /// `<line-names>` accompanying `<track-size> | <track-repeat>` values.
+ ///
+ /// If there's no `<line-names>`, then it's represented by an empty vector.
+ /// For N values, there will be N+1 `<line-names>`, and so this vector's
+ /// length is always one value more than that of the `<track-size>`.
+ pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>,
+}
+
+pub use self::GenericTrackList as TrackList;
+
+impl<L, I> TrackList<L, I> {
+ /// Whether this track list is an explicit track list (that is, doesn't have
+ /// any repeat values).
+ pub fn is_explicit(&self) -> bool {
+ !self.values.iter().any(|v| v.is_repeat())
+ }
+
+ /// Whether this track list has an `<auto-repeat>` value.
+ pub fn has_auto_repeat(&self) -> bool {
+ self.auto_repeat_index < self.values.len()
+ }
+}
+
+impl<L: ToCss, I: ToCss> ToCss for TrackList<L, I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let mut values_iter = self.values.iter().peekable();
+ let mut line_names_iter = self.line_names.iter().peekable();
+
+ for idx in 0.. {
+ let names = line_names_iter.next().unwrap(); // This should exist!
+ concat_serialize_idents("[", "]", names, " ", dest)?;
+
+ match values_iter.next() {
+ Some(value) => {
+ if !names.is_empty() {
+ dest.write_char(' ')?;
+ }
+
+ value.to_css(dest)?;
+ },
+ None => break,
+ }
+
+ if values_iter.peek().is_some() ||
+ line_names_iter.peek().map_or(false, |v| !v.is_empty()) ||
+ (idx + 1 == self.auto_repeat_index)
+ {
+ dest.write_char(' ')?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// The `<name-repeat>` for subgrids.
+///
+/// <name-repeat> = repeat( [ <integer [1,∞]> | auto-fill ], <line-names>+)
+///
+/// https://drafts.csswg.org/css-grid/#typedef-name-repeat
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericNameRepeat<I> {
+ /// The number of times for the value to be repeated (could also be `auto-fill`).
+ /// Note: `RepeatCount` accepts `auto-fit`, so we should reject it after parsing it.
+ pub count: RepeatCount<I>,
+ /// This represents `<line-names>+`. The length of the outer vector is at least one.
+ pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>,
+}
+
+pub use self::GenericNameRepeat as NameRepeat;
+
+impl<I: ToCss> ToCss for NameRepeat<I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("repeat(")?;
+ self.count.to_css(dest)?;
+ dest.write_char(',')?;
+
+ for ref names in self.line_names.iter() {
+ if names.is_empty() {
+ // Note: concat_serialize_idents() skip the empty list so we have to handle it
+ // manually for NameRepeat.
+ dest.write_str(" []")?;
+ } else {
+ concat_serialize_idents(" [", "]", names, " ", dest)?;
+ }
+ }
+
+ dest.write_char(')')
+ }
+}
+
+impl<I> NameRepeat<I> {
+ /// Returns true if it is auto-fill.
+ #[inline]
+ pub fn is_auto_fill(&self) -> bool {
+ matches!(self.count, RepeatCount::AutoFill)
+ }
+}
+
+/// A single value for `<line-names>` or `<name-repeat>`.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericLineNameListValue<I> {
+ /// `<line-names>`.
+ LineNames(crate::OwnedSlice<CustomIdent>),
+ /// `<name-repeat>`.
+ Repeat(GenericNameRepeat<I>),
+}
+
+pub use self::GenericLineNameListValue as LineNameListValue;
+
+impl<I: ToCss> ToCss for LineNameListValue<I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Self::Repeat(ref r) => r.to_css(dest),
+ Self::LineNames(ref names) => {
+ dest.write_char('[')?;
+
+ if let Some((ref first, rest)) = names.split_first() {
+ first.to_css(dest)?;
+ for name in rest {
+ dest.write_char(' ')?;
+ name.to_css(dest)?;
+ }
+ }
+
+ dest.write_char(']')
+ },
+ }
+ }
+}
+
+/// The `<line-name-list>` for subgrids.
+///
+/// <line-name-list> = [ <line-names> | <name-repeat> ]+
+/// <name-repeat> = repeat( [ <integer [1,∞]> | auto-fill ], <line-names>+)
+///
+/// https://drafts.csswg.org/css-grid/#typedef-line-name-list
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericLineNameList<I> {
+ /// The pre-computed length of line_names, without the length of repeat(auto-fill, ...).
+ // We precomputed this at parsing time, so we can avoid an extra loop when expanding
+ // repeat(auto-fill).
+ pub expanded_line_names_length: usize,
+ /// The line name list.
+ pub line_names: crate::OwnedSlice<GenericLineNameListValue<I>>,
+}
+
+pub use self::GenericLineNameList as LineNameList;
+
+impl<I: ToCss> ToCss for LineNameList<I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("subgrid")?;
+
+ for value in self.line_names.iter() {
+ dest.write_char(' ')?;
+ value.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+}
+
+/// Variants for `<grid-template-rows> | <grid-template-columns>`
+#[derive(
+ Animate,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "subgrid")]
+#[repr(C, u8)]
+pub enum GenericGridTemplateComponent<L, I> {
+ /// `none` value.
+ None,
+ /// The grid `<track-list>`
+ TrackList(
+ #[animation(field_bound)]
+ #[compute(field_bound)]
+ #[resolve(field_bound)]
+ #[shmem(field_bound)]
+ Box<GenericTrackList<L, I>>,
+ ),
+ /// A `subgrid <line-name-list>?`
+ /// TODO: Support animations for this after subgrid is addressed in [grid-2] spec.
+ #[animation(error)]
+ Subgrid(Box<GenericLineNameList<I>>),
+ /// `masonry` value.
+ /// https://github.com/w3c/csswg-drafts/issues/4650
+ Masonry,
+}
+
+pub use self::GenericGridTemplateComponent as GridTemplateComponent;
+
+impl<L, I> GridTemplateComponent<L, I> {
+ /// The initial value.
+ const INITIAL_VALUE: Self = Self::None;
+
+ /// Returns length of the <track-list>s <track-size>
+ pub fn track_list_len(&self) -> usize {
+ match *self {
+ GridTemplateComponent::TrackList(ref tracklist) => tracklist.values.len(),
+ _ => 0,
+ }
+ }
+
+ /// Returns true if `self` is the initial value.
+ pub fn is_initial(&self) -> bool {
+ matches!(*self, Self::None) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585
+ }
+}
+
+impl<L, I> Default for GridTemplateComponent<L, I> {
+ #[inline]
+ fn default() -> Self {
+ Self::INITIAL_VALUE
+ }
+}
diff --git a/servo/components/style/values/generics/image.rs b/servo/components/style/values/generics/image.rs
new file mode 100644
index 0000000000..6fc0870e15
--- /dev/null
+++ b/servo/components/style/values/generics/image.rs
@@ -0,0 +1,631 @@
+/* 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/. */
+
+//! Generic types for the handling of [images].
+//!
+//! [images]: https://drafts.csswg.org/css-images/#image-values
+
+use crate::color::mix::ColorInterpolationMethod;
+use crate::custom_properties;
+use crate::values::generics::position::PositionComponent;
+use crate::values::generics::Optional;
+use crate::values::serialize_atom_identifier;
+use crate::Atom;
+use crate::Zero;
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// An `<image> | none` value.
+///
+/// https://drafts.csswg.org/css-images/#image-values
+#[derive(
+ Clone, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericImage<G, ImageUrl, Color, Percentage, Resolution> {
+ /// `none` variant.
+ None,
+ /// A `<url()>` image.
+ Url(ImageUrl),
+
+ /// A `<gradient>` image. Gradients are rather large, and not nearly as
+ /// common as urls, so we box them here to keep the size of this enum sane.
+ Gradient(Box<G>),
+
+ /// A `-moz-element(# <element-id>)`
+ #[cfg(feature = "gecko")]
+ #[css(function = "-moz-element")]
+ Element(Atom),
+
+ /// A paint worklet image.
+ /// <https://drafts.css-houdini.org/css-paint-api/>
+ #[cfg(feature = "servo-layout-2013")]
+ PaintWorklet(PaintWorklet),
+
+ /// A `<cross-fade()>` image. Storing this directly inside of
+ /// GenericImage increases the size by 8 bytes so we box it here
+ /// and store images directly inside of cross-fade instead of
+ /// boxing them there.
+ CrossFade(Box<GenericCrossFade<Self, Color, Percentage>>),
+
+ /// An `image-set()` function.
+ ImageSet(#[compute(field_bound)] Box<GenericImageSet<Self, Resolution>>),
+}
+
+pub use self::GenericImage as Image;
+
+/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
+#[derive(
+ Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem, ToCss, ToComputedValue,
+)]
+#[css(comma, function = "cross-fade")]
+#[repr(C)]
+pub struct GenericCrossFade<Image, Color, Percentage> {
+ /// All of the image percent pairings passed as arguments to
+ /// cross-fade.
+ #[css(iterable)]
+ pub elements: crate::OwnedSlice<GenericCrossFadeElement<Image, Color, Percentage>>,
+}
+
+/// An optional percent and a cross fade image.
+#[derive(
+ Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, ToCss,
+)]
+#[repr(C)]
+pub struct GenericCrossFadeElement<Image, Color, Percentage> {
+ /// The percent of the final image that `image` will be.
+ pub percent: Optional<Percentage>,
+ /// A color or image that will be blended when cross-fade is
+ /// evaluated.
+ pub image: GenericCrossFadeImage<Image, Color>,
+}
+
+/// An image or a color. `cross-fade` takes either when blending
+/// images together.
+#[derive(
+ Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, ToCss,
+)]
+#[repr(C, u8)]
+pub enum GenericCrossFadeImage<I, C> {
+ /// A boxed image value. Boxing provides indirection so images can
+ /// be cross-fades and cross-fades can be images.
+ Image(I),
+ /// A color value.
+ Color(C),
+}
+
+pub use self::GenericCrossFade as CrossFade;
+pub use self::GenericCrossFadeElement as CrossFadeElement;
+pub use self::GenericCrossFadeImage as CrossFadeImage;
+
+/// https://drafts.csswg.org/css-images-4/#image-set-notation
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)]
+#[css(comma, function = "image-set")]
+#[repr(C)]
+pub struct GenericImageSet<Image, Resolution> {
+ /// The index of the selected candidate. usize::MAX for specified values or invalid images.
+ #[css(skip)]
+ pub selected_index: usize,
+
+ /// All of the image and resolution pairs.
+ #[css(iterable)]
+ pub items: crate::OwnedSlice<GenericImageSetItem<Image, Resolution>>,
+}
+
+/// An optional percent and a cross fade image.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub struct GenericImageSetItem<Image, Resolution> {
+ /// `<image>`. `<string>` is converted to `Image::Url` at parse time.
+ pub image: Image,
+ /// The `<resolution>`.
+ ///
+ /// TODO: Skip serialization if it is 1x.
+ pub resolution: Resolution,
+
+ /// The `type(<string>)`
+ /// (Optional) Specify the image's MIME type
+ pub mime_type: crate::OwnedStr,
+
+ /// True if mime_type has been specified
+ pub has_mime_type: bool,
+}
+
+impl<I: style_traits::ToCss, R: style_traits::ToCss> ToCss for GenericImageSetItem<I, R> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.image.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.resolution.to_css(dest)?;
+
+ if self.has_mime_type {
+ dest.write_char(' ')?;
+ dest.write_str("type(")?;
+ self.mime_type.to_css(dest)?;
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+}
+
+pub use self::GenericImageSet as ImageSet;
+pub use self::GenericImageSetItem as ImageSetItem;
+
+/// State flags stored on each variant of a Gradient.
+#[derive(
+ Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(C)]
+pub struct GradientFlags(u8);
+bitflags! {
+ impl GradientFlags: u8 {
+ /// Set if this is a repeating gradient.
+ const REPEATING = 1 << 0;
+ /// Set if the color interpolation method matches the default for the items.
+ const HAS_DEFAULT_COLOR_INTERPOLATION_METHOD = 1 << 1;
+ }
+}
+
+/// A CSS gradient.
+/// <https://drafts.csswg.org/css-images/#gradients>
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub enum GenericGradient<
+ LineDirection,
+ LengthPercentage,
+ NonNegativeLength,
+ NonNegativeLengthPercentage,
+ Position,
+ Angle,
+ AngleOrPercentage,
+ Color,
+> {
+ /// A linear gradient.
+ Linear {
+ /// Line direction
+ direction: LineDirection,
+ /// Method to use for color interpolation.
+ color_interpolation_method: ColorInterpolationMethod,
+ /// The color stops and interpolation hints.
+ items: crate::OwnedSlice<GenericGradientItem<Color, LengthPercentage>>,
+ /// State flags for the gradient.
+ flags: GradientFlags,
+ /// Compatibility mode.
+ compat_mode: GradientCompatMode,
+ },
+ /// A radial gradient.
+ Radial {
+ /// Shape of gradient
+ shape: GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage>,
+ /// Center of gradient
+ position: Position,
+ /// Method to use for color interpolation.
+ color_interpolation_method: ColorInterpolationMethod,
+ /// The color stops and interpolation hints.
+ items: crate::OwnedSlice<GenericGradientItem<Color, LengthPercentage>>,
+ /// State flags for the gradient.
+ flags: GradientFlags,
+ /// Compatibility mode.
+ compat_mode: GradientCompatMode,
+ },
+ /// A conic gradient.
+ Conic {
+ /// Start angle of gradient
+ angle: Angle,
+ /// Center of gradient
+ position: Position,
+ /// Method to use for color interpolation.
+ color_interpolation_method: ColorInterpolationMethod,
+ /// The color stops and interpolation hints.
+ items: crate::OwnedSlice<GenericGradientItem<Color, AngleOrPercentage>>,
+ /// State flags for the gradient.
+ flags: GradientFlags,
+ },
+}
+
+pub use self::GenericGradient as Gradient;
+
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(u8)]
+/// Whether we used the modern notation or the compatibility `-webkit`, `-moz` prefixes.
+pub enum GradientCompatMode {
+ /// Modern syntax.
+ Modern,
+ /// `-webkit` prefix.
+ WebKit,
+ /// `-moz` prefix
+ Moz,
+}
+
+/// A radial gradient's ending shape.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage> {
+ /// A circular gradient.
+ Circle(GenericCircle<NonNegativeLength>),
+ /// An elliptic gradient.
+ Ellipse(GenericEllipse<NonNegativeLengthPercentage>),
+}
+
+pub use self::GenericEndingShape as EndingShape;
+
+/// A circle shape.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericCircle<NonNegativeLength> {
+ /// A circle radius.
+ Radius(NonNegativeLength),
+ /// A circle extent.
+ Extent(ShapeExtent),
+}
+
+pub use self::GenericCircle as Circle;
+
+/// An ellipse shape.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericEllipse<NonNegativeLengthPercentage> {
+ /// An ellipse pair of radii.
+ Radii(NonNegativeLengthPercentage, NonNegativeLengthPercentage),
+ /// An ellipse extent.
+ Extent(ShapeExtent),
+}
+
+pub use self::GenericEllipse as Ellipse;
+
+/// <https://drafts.csswg.org/css-images/#typedef-extent-keyword>
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ShapeExtent {
+ ClosestSide,
+ FarthestSide,
+ ClosestCorner,
+ FarthestCorner,
+ Contain,
+ Cover,
+}
+
+/// A gradient item.
+/// <https://drafts.csswg.org/css-images-4/#color-stop-syntax>
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericGradientItem<Color, T> {
+ /// A simple color stop, without position.
+ SimpleColorStop(Color),
+ /// A complex color stop, with a position.
+ ComplexColorStop {
+ /// The color for the stop.
+ color: Color,
+ /// The position for the stop.
+ position: T,
+ },
+ /// An interpolation hint.
+ InterpolationHint(T),
+}
+
+pub use self::GenericGradientItem as GradientItem;
+
+/// A color stop.
+/// <https://drafts.csswg.org/css-images/#typedef-color-stop-list>
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
+)]
+pub struct ColorStop<Color, T> {
+ /// The color of this stop.
+ pub color: Color,
+ /// The position of this stop.
+ pub position: Option<T>,
+}
+
+impl<Color, T> ColorStop<Color, T> {
+ /// Convert the color stop into an appropriate `GradientItem`.
+ #[inline]
+ pub fn into_item(self) -> GradientItem<Color, T> {
+ match self.position {
+ Some(position) => GradientItem::ComplexColorStop {
+ color: self.color,
+ position,
+ },
+ None => GradientItem::SimpleColorStop(self.color),
+ }
+ }
+}
+
+/// Specified values for a paint worklet.
+/// <https://drafts.css-houdini.org/css-paint-api/>
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[derive(Clone, Debug, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+pub struct PaintWorklet {
+ /// The name the worklet was registered with.
+ pub name: Atom,
+ /// The arguments for the worklet.
+ /// TODO: store a parsed representation of the arguments.
+ #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")]
+ #[compute(no_field_bound)]
+ #[resolve(no_field_bound)]
+ pub arguments: Vec<Arc<custom_properties::SpecifiedValue>>,
+}
+
+impl ::style_traits::SpecifiedValueInfo for PaintWorklet {}
+
+impl ToCss for PaintWorklet {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("paint(")?;
+ serialize_atom_identifier(&self.name, dest)?;
+ for argument in &self.arguments {
+ dest.write_str(", ")?;
+ argument.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<G, U, C, P, Resolution> fmt::Debug for Image<G, U, C, P, Resolution>
+where
+ Image<G, U, C, P, Resolution>: ToCss,
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.to_css(&mut CssWriter::new(f))
+ }
+}
+
+impl<G, U, C, P, Resolution> ToCss for Image<G, U, C, P, Resolution>
+where
+ G: ToCss,
+ U: ToCss,
+ C: ToCss,
+ P: ToCss,
+ Resolution: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Image::None => dest.write_str("none"),
+ Image::Url(ref url) => url.to_css(dest),
+ Image::Gradient(ref gradient) => gradient.to_css(dest),
+ #[cfg(feature = "servo-layout-2013")]
+ Image::PaintWorklet(ref paint_worklet) => paint_worklet.to_css(dest),
+ #[cfg(feature = "gecko")]
+ Image::Element(ref selector) => {
+ dest.write_str("-moz-element(#")?;
+ serialize_atom_identifier(selector, dest)?;
+ dest.write_char(')')
+ },
+ Image::ImageSet(ref is) => is.to_css(dest),
+ Image::CrossFade(ref cf) => cf.to_css(dest),
+ }
+ }
+}
+
+impl<D, LP, NL, NLP, P, A: Zero, AoP, C> ToCss for Gradient<D, LP, NL, NLP, P, A, AoP, C>
+where
+ D: LineDirection,
+ LP: ToCss,
+ NL: ToCss,
+ NLP: ToCss,
+ P: PositionComponent + ToCss,
+ A: ToCss,
+ AoP: ToCss,
+ C: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let (compat_mode, repeating, has_default_color_interpolation_method) = match *self {
+ Gradient::Linear {
+ compat_mode, flags, ..
+ } |
+ Gradient::Radial {
+ compat_mode, flags, ..
+ } => (
+ compat_mode,
+ flags.contains(GradientFlags::REPEATING),
+ flags.contains(GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD),
+ ),
+ Gradient::Conic { flags, .. } => (
+ GradientCompatMode::Modern,
+ flags.contains(GradientFlags::REPEATING),
+ flags.contains(GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD),
+ ),
+ };
+
+ match compat_mode {
+ GradientCompatMode::WebKit => dest.write_str("-webkit-")?,
+ GradientCompatMode::Moz => dest.write_str("-moz-")?,
+ _ => {},
+ }
+
+ if repeating {
+ dest.write_str("repeating-")?;
+ }
+
+ match *self {
+ Gradient::Linear {
+ ref direction,
+ ref color_interpolation_method,
+ ref items,
+ compat_mode,
+ ..
+ } => {
+ dest.write_str("linear-gradient(")?;
+ let mut skip_comma = true;
+ if !direction.points_downwards(compat_mode) {
+ direction.to_css(dest, compat_mode)?;
+ skip_comma = false;
+ }
+ if !has_default_color_interpolation_method {
+ if !skip_comma {
+ dest.write_char(' ')?;
+ }
+ color_interpolation_method.to_css(dest)?;
+ skip_comma = false;
+ }
+ for item in &**items {
+ if !skip_comma {
+ dest.write_str(", ")?;
+ }
+ skip_comma = false;
+ item.to_css(dest)?;
+ }
+ },
+ Gradient::Radial {
+ ref shape,
+ ref position,
+ ref color_interpolation_method,
+ ref items,
+ compat_mode,
+ ..
+ } => {
+ dest.write_str("radial-gradient(")?;
+ let omit_shape = match *shape {
+ EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::Cover)) |
+ EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner)) => true,
+ _ => false,
+ };
+ let omit_position = position.is_center();
+ if compat_mode == GradientCompatMode::Modern {
+ if !omit_shape {
+ shape.to_css(dest)?;
+ if !omit_position {
+ dest.write_char(' ')?;
+ }
+ }
+ if !omit_position {
+ dest.write_str("at ")?;
+ position.to_css(dest)?;
+ }
+ } else {
+ if !omit_position {
+ position.to_css(dest)?;
+ if !omit_shape {
+ dest.write_str(", ")?;
+ }
+ }
+ if !omit_shape {
+ shape.to_css(dest)?;
+ }
+ }
+ if !has_default_color_interpolation_method {
+ if !omit_shape || !omit_position {
+ dest.write_char(' ')?;
+ }
+ color_interpolation_method.to_css(dest)?;
+ }
+
+ let mut skip_comma =
+ omit_shape && omit_position && has_default_color_interpolation_method;
+ for item in &**items {
+ if !skip_comma {
+ dest.write_str(", ")?;
+ }
+ skip_comma = false;
+ item.to_css(dest)?;
+ }
+ },
+ Gradient::Conic {
+ ref angle,
+ ref position,
+ ref color_interpolation_method,
+ ref items,
+ ..
+ } => {
+ dest.write_str("conic-gradient(")?;
+ let omit_angle = angle.is_zero();
+ let omit_position = position.is_center();
+ if !omit_angle {
+ dest.write_str("from ")?;
+ angle.to_css(dest)?;
+ if !omit_position {
+ dest.write_char(' ')?;
+ }
+ }
+ if !omit_position {
+ dest.write_str("at ")?;
+ position.to_css(dest)?;
+ }
+ if !has_default_color_interpolation_method {
+ if !omit_angle || !omit_position {
+ dest.write_char(' ')?;
+ }
+ color_interpolation_method.to_css(dest)?;
+ }
+ let mut skip_comma =
+ omit_angle && omit_position && has_default_color_interpolation_method;
+ for item in &**items {
+ if !skip_comma {
+ dest.write_str(", ")?;
+ }
+ skip_comma = false;
+ item.to_css(dest)?;
+ }
+ },
+ }
+ dest.write_char(')')
+ }
+}
+
+/// The direction of a linear gradient.
+pub trait LineDirection {
+ /// Whether this direction points towards, and thus can be omitted.
+ fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool;
+
+ /// Serialises this direction according to the compatibility mode.
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
+ where
+ W: Write;
+}
+
+impl<L> ToCss for Circle<L>
+where
+ L: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Circle::Extent(ShapeExtent::FarthestCorner) | Circle::Extent(ShapeExtent::Cover) => {
+ dest.write_str("circle")
+ },
+ Circle::Extent(keyword) => {
+ dest.write_str("circle ")?;
+ keyword.to_css(dest)
+ },
+ Circle::Radius(ref length) => length.to_css(dest),
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/length.rs b/servo/components/style/values/generics/length.rs
new file mode 100644
index 0000000000..de0dd7fbc1
--- /dev/null
+++ b/servo/components/style/values/generics/length.rs
@@ -0,0 +1,304 @@
+/* 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/. */
+
+//! Generic types for CSS values related to length.
+
+use crate::parser::{Parse, ParserContext};
+#[cfg(feature = "gecko")]
+use crate::Zero;
+use cssparser::Parser;
+use style_traits::ParseError;
+
+/// A `<length-percentage> | auto` value.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericLengthPercentageOrAuto<LengthPercent> {
+ LengthPercentage(LengthPercent),
+ Auto,
+}
+
+pub use self::GenericLengthPercentageOrAuto as LengthPercentageOrAuto;
+
+impl<LengthPercentage> LengthPercentageOrAuto<LengthPercentage> {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ LengthPercentageOrAuto::Auto
+ }
+
+ /// Whether this is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, LengthPercentageOrAuto::Auto)
+ }
+
+ /// A helper function to parse this with quirks or not and so forth.
+ pub fn parse_with<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parser: impl FnOnce(
+ &ParserContext,
+ &mut Parser<'i, 't>,
+ ) -> Result<LengthPercentage, ParseError<'i>>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ return Ok(LengthPercentageOrAuto::Auto);
+ }
+
+ Ok(LengthPercentageOrAuto::LengthPercentage(parser(
+ context, input,
+ )?))
+ }
+}
+
+impl<LengthPercentage> LengthPercentageOrAuto<LengthPercentage>
+where
+ LengthPercentage: Clone,
+{
+ /// Resolves `auto` values by calling `f`.
+ #[inline]
+ pub fn auto_is(&self, f: impl FnOnce() -> LengthPercentage) -> LengthPercentage {
+ match self {
+ LengthPercentageOrAuto::LengthPercentage(length) => length.clone(),
+ LengthPercentageOrAuto::Auto => f(),
+ }
+ }
+
+ /// Returns the non-`auto` value, if any.
+ #[inline]
+ pub fn non_auto(&self) -> Option<LengthPercentage> {
+ match self {
+ LengthPercentageOrAuto::LengthPercentage(length) => Some(length.clone()),
+ LengthPercentageOrAuto::Auto => None,
+ }
+ }
+
+ /// Maps the length of this value.
+ pub fn map<T>(&self, f: impl FnOnce(LengthPercentage) -> T) -> LengthPercentageOrAuto<T> {
+ match self {
+ LengthPercentageOrAuto::LengthPercentage(l) => {
+ LengthPercentageOrAuto::LengthPercentage(f(l.clone()))
+ },
+ LengthPercentageOrAuto::Auto => LengthPercentageOrAuto::Auto,
+ }
+ }
+}
+
+impl<LengthPercentage: Zero> Zero for LengthPercentageOrAuto<LengthPercentage> {
+ fn zero() -> Self {
+ LengthPercentageOrAuto::LengthPercentage(Zero::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ match *self {
+ LengthPercentageOrAuto::LengthPercentage(ref l) => l.is_zero(),
+ LengthPercentageOrAuto::Auto => false,
+ }
+ }
+}
+
+impl<LengthPercentage: Parse> Parse for LengthPercentageOrAuto<LengthPercentage> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, LengthPercentage::parse)
+ }
+}
+
+/// A generic value for the `width`, `height`, `min-width`, or `min-height` property.
+///
+/// Unlike `max-width` or `max-height` properties, a Size can be `auto`,
+/// and cannot be `none`.
+///
+/// Note that it only accepts non-negative values.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSize<LengthPercent> {
+ LengthPercentage(LengthPercent),
+ Auto,
+ #[animation(error)]
+ MaxContent,
+ #[animation(error)]
+ MinContent,
+ #[animation(error)]
+ FitContent,
+ #[animation(error)]
+ MozAvailable,
+ #[animation(error)]
+ #[css(function = "fit-content")]
+ FitContentFunction(LengthPercent),
+}
+
+pub use self::GenericSize as Size;
+
+impl<LengthPercentage> Size<LengthPercentage> {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ Size::Auto
+ }
+
+ /// Returns whether we're the auto value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, Size::Auto)
+ }
+}
+
+/// A generic value for the `max-width` or `max-height` property.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericMaxSize<LengthPercent> {
+ LengthPercentage(LengthPercent),
+ None,
+ #[animation(error)]
+ MaxContent,
+ #[animation(error)]
+ MinContent,
+ #[animation(error)]
+ FitContent,
+ #[animation(error)]
+ MozAvailable,
+ #[animation(error)]
+ #[css(function = "fit-content")]
+ FitContentFunction(LengthPercent),
+}
+
+pub use self::GenericMaxSize as MaxSize;
+
+impl<LengthPercentage> MaxSize<LengthPercentage> {
+ /// `none` value.
+ #[inline]
+ pub fn none() -> Self {
+ MaxSize::None
+ }
+}
+
+/// A generic `<length>` | `<number>` value for the `tab-size` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericLengthOrNumber<L, N> {
+ /// A number.
+ ///
+ /// NOTE: Numbers need to be before lengths, in order to parse them
+ /// first, since `0` should be a number, not the `0px` length.
+ Number(N),
+ /// A length.
+ Length(L),
+}
+
+pub use self::GenericLengthOrNumber as LengthOrNumber;
+
+impl<L, N: Zero> Zero for LengthOrNumber<L, N> {
+ fn zero() -> Self {
+ LengthOrNumber::Number(Zero::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ match *self {
+ LengthOrNumber::Number(ref n) => n.is_zero(),
+ LengthOrNumber::Length(..) => false,
+ }
+ }
+}
+
+/// A generic `<length-percentage>` | normal` value.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+#[allow(missing_docs)]
+pub enum GenericLengthPercentageOrNormal<LengthPercent> {
+ LengthPercentage(LengthPercent),
+ Normal,
+}
+
+pub use self::GenericLengthPercentageOrNormal as LengthPercentageOrNormal;
+
+impl<LengthPercent> LengthPercentageOrNormal<LengthPercent> {
+ /// Returns the normal value.
+ #[inline]
+ pub fn normal() -> Self {
+ LengthPercentageOrNormal::Normal
+ }
+}
diff --git a/servo/components/style/values/generics/mod.rs b/servo/components/style/values/generics/mod.rs
new file mode 100644
index 0000000000..800d058170
--- /dev/null
+++ b/servo/components/style/values/generics/mod.rs
@@ -0,0 +1,388 @@
+/* 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/. */
+
+//! Generic types that share their serialization implementations
+//! for both specified and computed values.
+
+use super::CustomIdent;
+use crate::counter_style::{parse_counter_style_name, Symbols};
+use crate::parser::{Parse, ParserContext};
+use crate::Zero;
+use cssparser::Parser;
+use std::ops::Add;
+use style_traits::{KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind};
+
+pub mod animation;
+pub mod background;
+pub mod basic_shape;
+pub mod border;
+#[path = "box.rs"]
+pub mod box_;
+pub mod calc;
+pub mod color;
+pub mod column;
+pub mod counters;
+pub mod easing;
+pub mod effects;
+pub mod flex;
+pub mod font;
+pub mod grid;
+pub mod image;
+pub mod length;
+pub mod motion;
+pub mod page;
+pub mod position;
+pub mod ratio;
+pub mod rect;
+pub mod size;
+pub mod svg;
+pub mod text;
+pub mod transform;
+pub mod ui;
+pub mod url;
+
+/// https://drafts.csswg.org/css-counter-styles/#typedef-symbols-type
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum SymbolsType {
+ Cyclic,
+ Numeric,
+ Alphabetic,
+ Symbolic,
+ Fixed,
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#typedef-counter-style>
+///
+/// Note that 'none' is not a valid name.
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(Clone, Debug, Eq, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+pub enum CounterStyle {
+ /// `<counter-style-name>`
+ Name(CustomIdent),
+ /// `symbols()`
+ #[css(function)]
+ Symbols(#[css(skip_if = "is_symbolic")] SymbolsType, Symbols),
+}
+
+#[inline]
+fn is_symbolic(symbols_type: &SymbolsType) -> bool {
+ *symbols_type == SymbolsType::Symbolic
+}
+
+impl CounterStyle {
+ /// disc value
+ pub fn disc() -> Self {
+ CounterStyle::Name(CustomIdent(atom!("disc")))
+ }
+
+ /// decimal value
+ pub fn decimal() -> Self {
+ CounterStyle::Name(CustomIdent(atom!("decimal")))
+ }
+
+ /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`)
+ #[inline]
+ pub fn is_bullet(&self) -> bool {
+ match self {
+ CounterStyle::Name(CustomIdent(ref name)) => {
+ name == &atom!("disc") ||
+ name == &atom!("circle") ||
+ name == &atom!("square") ||
+ name == &atom!("disclosure-closed") ||
+ name == &atom!("disclosure-open")
+ },
+ _ => false,
+ }
+ }
+}
+
+impl Parse for CounterStyle {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(name) = input.try_parse(|i| parse_counter_style_name(i)) {
+ return Ok(CounterStyle::Name(name));
+ }
+ input.expect_function_matching("symbols")?;
+ input.parse_nested_block(|input| {
+ let symbols_type = input
+ .try_parse(SymbolsType::parse)
+ .unwrap_or(SymbolsType::Symbolic);
+ let symbols = Symbols::parse(context, input)?;
+ // There must be at least two symbols for alphabetic or
+ // numeric system.
+ if (symbols_type == SymbolsType::Alphabetic || symbols_type == SymbolsType::Numeric) &&
+ symbols.0.len() < 2
+ {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ // Identifier is not allowed in symbols() function.
+ if symbols.0.iter().any(|sym| !sym.is_allowed_in_symbols()) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(CounterStyle::Symbols(symbols_type, symbols))
+ })
+ }
+}
+
+impl SpecifiedValueInfo for CounterStyle {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ // XXX The best approach for implementing this is probably
+ // having a CounterStyleName type wrapping CustomIdent, and
+ // put the predefined list for that type in counter_style mod.
+ // But that's a non-trivial change itself, so we use a simpler
+ // approach here.
+ macro_rules! predefined {
+ ($($name:expr,)+) => {
+ f(&["symbols", $($name,)+])
+ }
+ }
+ include!("../../counter_style/predefined.rs");
+ }
+}
+
+/// A wrapper of Non-negative values.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct NonNegative<T>(pub T);
+
+impl<T: Add<Output = T>> Add<NonNegative<T>> for NonNegative<T> {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ NonNegative(self.0 + other.0)
+ }
+}
+
+impl<T: Zero> Zero for NonNegative<T> {
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+
+ fn zero() -> Self {
+ NonNegative(T::zero())
+ }
+}
+
+/// A wrapper of greater-than-or-equal-to-one values.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct GreaterThanOrEqualToOne<T>(pub T);
+
+/// A wrapper of values between zero and one.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct ZeroToOne<T>(pub T);
+
+/// A clip rect for clip and image-region
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "rect", comma)]
+#[repr(C)]
+pub struct GenericClipRect<LengthOrAuto> {
+ pub top: LengthOrAuto,
+ pub right: LengthOrAuto,
+ pub bottom: LengthOrAuto,
+ pub left: LengthOrAuto,
+}
+
+pub use self::GenericClipRect as ClipRect;
+
+/// Either a clip-rect or `auto`.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericClipRectOrAuto<R> {
+ Auto,
+ Rect(R),
+}
+
+pub use self::GenericClipRectOrAuto as ClipRectOrAuto;
+
+impl<L> ClipRectOrAuto<L> {
+ /// Returns the `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ ClipRectOrAuto::Auto
+ }
+
+ /// Returns whether this value is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, ClipRectOrAuto::Auto)
+ }
+}
+
+pub use page::PageSize;
+
+/// An optional value, much like `Option<T>`, but with a defined struct layout
+/// to be able to use it from C++ as well.
+///
+/// Note that this is relatively inefficient, struct-layout-wise, as you have
+/// one byte for the tag, but padding to the alignment of T. If you have
+/// multiple optional values and care about struct compactness, you might be
+/// better off "coalescing" the combinations into a parent enum. But that
+/// shouldn't matter for most use cases.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[repr(C, u8)]
+pub enum Optional<T> {
+ #[css(skip)]
+ None,
+ Some(T),
+}
+
+impl<T> Optional<T> {
+ /// Returns whether this value is present.
+ pub fn is_some(&self) -> bool {
+ matches!(*self, Self::Some(..))
+ }
+
+ /// Returns whether this value is not present.
+ pub fn is_none(&self) -> bool {
+ matches!(*self, Self::None)
+ }
+
+ /// Turns this Optional<> into a regular rust Option<>.
+ pub fn into_rust(self) -> Option<T> {
+ match self {
+ Self::Some(v) => Some(v),
+ Self::None => None,
+ }
+ }
+
+ /// Return a reference to the containing value, if any, as a plain rust
+ /// Option<>.
+ pub fn as_ref(&self) -> Option<&T> {
+ match *self {
+ Self::Some(ref v) => Some(v),
+ Self::None => None,
+ }
+ }
+}
+
+impl<T> From<Option<T>> for Optional<T> {
+ fn from(rust: Option<T>) -> Self {
+ match rust {
+ Some(t) => Self::Some(t),
+ None => Self::None,
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/motion.rs b/servo/components/style/values/generics/motion.rs
new file mode 100644
index 0000000000..ee6f5702da
--- /dev/null
+++ b/servo/components/style/values/generics/motion.rs
@@ -0,0 +1,270 @@
+/* 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/. */
+
+//! Generic types for CSS Motion Path.
+
+use crate::values::animated::ToAnimatedZero;
+use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto};
+use crate::values::specified::motion::CoordBox;
+use serde::Deserializer;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// The <size> in ray() function.
+///
+/// https://drafts.fxtf.org/motion-1/#valdef-offsetpath-size
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum RaySize {
+ ClosestSide,
+ ClosestCorner,
+ FarthestSide,
+ FarthestCorner,
+ Sides,
+}
+
+/// The `ray()` function, `ray( [ <angle> && <size> && contain? && [at <position>]? ] )`
+///
+/// https://drafts.fxtf.org/motion-1/#valdef-offsetpath-ray
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericRayFunction<Angle, Position> {
+ /// The bearing angle with `0deg` pointing up and positive angles
+ /// representing clockwise rotation.
+ pub angle: Angle,
+ /// Decide the path length used when `offset-distance` is expressed
+ /// as a percentage.
+ pub size: RaySize,
+ /// Clamp `offset-distance` so that the box is entirely contained
+ /// within the path.
+ #[animation(constant)]
+ pub contain: bool,
+ /// The "at <position>" part. If omitted, we use auto to represent it.
+ pub position: GenericPositionOrAuto<Position>,
+}
+
+pub use self::GenericRayFunction as RayFunction;
+
+impl<Angle, Position> ToCss for RayFunction<Angle, Position>
+where
+ Angle: ToCss,
+ Position: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.angle.to_css(dest)?;
+
+ if !matches!(self.size, RaySize::ClosestSide) {
+ dest.write_char(' ')?;
+ self.size.to_css(dest)?;
+ }
+
+ if self.contain {
+ dest.write_str(" contain")?;
+ }
+
+ if !matches!(self.position, GenericPositionOrAuto::Auto) {
+ dest.write_str(" at ")?;
+ self.position.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+}
+
+/// Return error if we try to deserialize the url, for Gecko IPC purposes.
+// Note: we cannot use #[serde(skip_deserializing)] variant attribute, which may cause the fatal
+// error when trying to read the parameters because it cannot deserialize the input byte buffer,
+// even if the type of OffsetPathFunction is not an url(), in our tests. This may be an issue of
+// #[serde(skip_deserializing)] on enum, at least in the version (1.0) we are using. So we have to
+// manually implement this deseriailzing function, but return error.
+// FIXME: Bug 1847620, fiure out this is a serde issue or a gecko bug.
+fn deserialize_url<'de, D, T>(_deserializer: D) -> Result<T, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ use crate::serde::de::Error;
+ // Return Err() so the IPC will catch it and assert this as a fetal error.
+ Err(<D as Deserializer>::Error::custom(
+ "we don't support the deserializing for url",
+ ))
+}
+
+/// The <offset-path> value.
+/// <offset-path> = <ray()> | <url> | <basic-shape>
+///
+/// https://drafts.fxtf.org/motion-1/#typedef-offset-path
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(U))]
+#[repr(C, u8)]
+pub enum GenericOffsetPathFunction<Shapes, RayFunction, U> {
+ /// ray() function, which defines a path in the polar coordinate system.
+ /// Use Box<> to make sure the size of offset-path is not too large.
+ #[css(function)]
+ Ray(RayFunction),
+ /// A URL reference to an SVG shape element. If the URL does not reference a shape element,
+ /// this behaves as path("m 0 0") instead.
+ #[animation(error)]
+ #[serde(deserialize_with = "deserialize_url")]
+ #[serde(skip_serializing)]
+ Url(U),
+ /// The <basic-shape> value.
+ Shape(Shapes),
+}
+
+pub use self::GenericOffsetPathFunction as OffsetPathFunction;
+
+/// The offset-path property.
+/// offset-path: none | <offset-path> || <coord-box>
+///
+/// https://drafts.fxtf.org/motion-1/#offset-path-property
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericOffsetPath<Function> {
+ /// <offset-path> || <coord-box>.
+ OffsetPath {
+ /// <offset-path> part.
+ // Note: Use Box<> to make sure the size of this property doesn't go over the threshold.
+ path: Box<Function>,
+ /// <coord-box> part.
+ #[css(skip_if = "CoordBox::is_default")]
+ coord_box: CoordBox,
+ },
+ /// Only <coord-box>. This represents that <offset-path> is omitted, so we use the default
+ /// value, inset(0 round X), where X is the value of border-radius on the element that
+ /// establishes the containing block for this element.
+ CoordBox(CoordBox),
+ /// None value.
+ #[animation(error)]
+ None,
+}
+
+pub use self::GenericOffsetPath as OffsetPath;
+
+impl<Function> OffsetPath<Function> {
+ /// Return None.
+ #[inline]
+ pub fn none() -> Self {
+ OffsetPath::None
+ }
+}
+
+impl<Function> ToAnimatedZero for OffsetPath<Function> {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+/// The offset-position property, which specifies the offset starting position that is used by the
+/// <offset-path> functions if they don’t specify their own starting position.
+///
+/// https://drafts.fxtf.org/motion-1/#offset-position-property
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericOffsetPosition<H, V> {
+ /// The element does not have an offset starting position.
+ Normal,
+ /// The offset starting position is the top-left corner of the box.
+ Auto,
+ /// The offset starting position is the result of using the <position> to position a 0x0 object
+ /// area within the box’s containing block.
+ Position(
+ #[css(field_bound)]
+ #[parse(field_bound)]
+ GenericPosition<H, V>,
+ ),
+}
+
+pub use self::GenericOffsetPosition as OffsetPosition;
+
+impl<H, V> OffsetPosition<H, V> {
+ /// Returns the initial value, normal.
+ #[inline]
+ pub fn normal() -> Self {
+ Self::Normal
+ }
+}
diff --git a/servo/components/style/values/generics/page.rs b/servo/components/style/values/generics/page.rs
new file mode 100644
index 0000000000..91f02bc4b3
--- /dev/null
+++ b/servo/components/style/values/generics/page.rs
@@ -0,0 +1,162 @@
+/* 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/. */
+
+//! @page at-rule properties
+
+use crate::values::generics::NonNegative;
+use crate::values::specified::length::AbsoluteLength;
+
+/// Page size names.
+///
+/// https://drafts.csswg.org/css-page-3/#typedef-page-size-page-size
+#[derive(
+ Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[repr(u8)]
+pub enum PaperSize {
+ /// ISO A5 media
+ A5,
+ /// ISO A4 media
+ A4,
+ /// ISO A3 media
+ A3,
+ /// ISO B5 media
+ B5,
+ /// ISO B4 media
+ B4,
+ /// JIS B5 media
+ JisB5,
+ /// JIS B4 media
+ JisB4,
+ /// North American Letter size
+ Letter,
+ /// North American Legal size
+ Legal,
+ /// North American Ledger size
+ Ledger,
+}
+
+impl PaperSize {
+ /// Gets the long edge length of the paper size
+ pub fn long_edge(&self) -> NonNegative<AbsoluteLength> {
+ NonNegative(match *self {
+ PaperSize::A5 => AbsoluteLength::Mm(210.0),
+ PaperSize::A4 => AbsoluteLength::Mm(297.0),
+ PaperSize::A3 => AbsoluteLength::Mm(420.0),
+ PaperSize::B5 => AbsoluteLength::Mm(250.0),
+ PaperSize::B4 => AbsoluteLength::Mm(353.0),
+ PaperSize::JisB5 => AbsoluteLength::Mm(257.0),
+ PaperSize::JisB4 => AbsoluteLength::Mm(364.0),
+ PaperSize::Letter => AbsoluteLength::In(11.0),
+ PaperSize::Legal => AbsoluteLength::In(14.0),
+ PaperSize::Ledger => AbsoluteLength::In(17.0),
+ })
+ }
+ /// Gets the short edge length of the paper size
+ pub fn short_edge(&self) -> NonNegative<AbsoluteLength> {
+ NonNegative(match *self {
+ PaperSize::A5 => AbsoluteLength::Mm(148.0),
+ PaperSize::A4 => AbsoluteLength::Mm(210.0),
+ PaperSize::A3 => AbsoluteLength::Mm(297.0),
+ PaperSize::B5 => AbsoluteLength::Mm(176.0),
+ PaperSize::B4 => AbsoluteLength::Mm(250.0),
+ PaperSize::JisB5 => AbsoluteLength::Mm(182.0),
+ PaperSize::JisB4 => AbsoluteLength::Mm(257.0),
+ PaperSize::Letter => AbsoluteLength::In(8.5),
+ PaperSize::Legal => AbsoluteLength::In(8.5),
+ PaperSize::Ledger => AbsoluteLength::In(11.0),
+ })
+ }
+}
+
+/// Page orientation names.
+///
+/// https://drafts.csswg.org/css-page-3/#page-orientation-prop
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum PageOrientation {
+ /// upright
+ Upright,
+ /// rotate-left (counter-clockwise)
+ RotateLeft,
+ /// rotate-right (clockwise)
+ RotateRight,
+}
+
+/// Paper orientation
+///
+/// https://drafts.csswg.org/css-page-3/#page-size-prop
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum PageSizeOrientation {
+ /// Portrait orientation
+ Portrait,
+ /// Landscape orientation
+ Landscape,
+}
+
+#[inline]
+fn is_portrait(orientation: &PageSizeOrientation) -> bool {
+ *orientation == PageSizeOrientation::Portrait
+}
+
+/// Page size property
+///
+/// https://drafts.csswg.org/css-page-3/#page-size-prop
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+#[repr(C, u8)]
+pub enum GenericPageSize<S> {
+ /// `auto` value.
+ Auto,
+ /// Page dimensions.
+ Size(S),
+ /// An orientation with no size.
+ Orientation(PageSizeOrientation),
+ /// Paper size by name
+ PaperSize(
+ PaperSize,
+ #[css(skip_if = "is_portrait")] PageSizeOrientation,
+ ),
+}
+
+pub use self::GenericPageSize as PageSize;
+
+impl<S> PageSize<S> {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ PageSize::Auto
+ }
+
+ /// Whether this is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, PageSize::Auto)
+ }
+}
diff --git a/servo/components/style/values/generics/position.rs b/servo/components/style/values/generics/position.rs
new file mode 100644
index 0000000000..8a4a8c9e24
--- /dev/null
+++ b/servo/components/style/values/generics/position.rs
@@ -0,0 +1,238 @@
+/* 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/. */
+
+//! Generic types for CSS handling of specified and computed values of
+//! [`position`](https://drafts.csswg.org/css-backgrounds-3/#position)
+
+use crate::values::animated::ToAnimatedZero;
+use crate::values::generics::ratio::Ratio;
+
+/// A generic type for representing a CSS [position](https://drafts.csswg.org/css-values/#position).
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericPosition<H, V> {
+ /// The horizontal component of position.
+ pub horizontal: H,
+ /// The vertical component of position.
+ pub vertical: V,
+}
+
+impl<H, V> PositionComponent for Position<H, V>
+where
+ H: PositionComponent,
+ V: PositionComponent,
+{
+ #[inline]
+ fn is_center(&self) -> bool {
+ self.horizontal.is_center() && self.vertical.is_center()
+ }
+}
+
+pub use self::GenericPosition as Position;
+
+impl<H, V> Position<H, V> {
+ /// Returns a new position.
+ pub fn new(horizontal: H, vertical: V) -> Self {
+ Self {
+ horizontal,
+ vertical,
+ }
+ }
+}
+
+/// Implements a method that checks if the position is centered.
+pub trait PositionComponent {
+ /// Returns if the position component is 50% or center.
+ /// For pixel lengths, it always returns false.
+ fn is_center(&self) -> bool;
+}
+
+/// A generic type for representing an `Auto | <position>`.
+/// This is used by <offset-anchor> for now.
+/// https://drafts.fxtf.org/motion-1/#offset-anchor-property
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericPositionOrAuto<Pos> {
+ /// The <position> value.
+ Position(Pos),
+ /// The keyword `auto`.
+ Auto,
+}
+
+pub use self::GenericPositionOrAuto as PositionOrAuto;
+
+impl<Pos> PositionOrAuto<Pos> {
+ /// Return `auto`.
+ #[inline]
+ pub fn auto() -> Self {
+ PositionOrAuto::Auto
+ }
+
+ /// Return true if it is 'auto'.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(self, PositionOrAuto::Auto)
+ }
+}
+
+/// A generic value for the `z-index` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericZIndex<I> {
+ /// An integer value.
+ Integer(I),
+ /// The keyword `auto`.
+ Auto,
+}
+
+pub use self::GenericZIndex as ZIndex;
+
+impl<Integer> ZIndex<Integer> {
+ /// Returns `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ ZIndex::Auto
+ }
+
+ /// Returns whether `self` is `auto`.
+ #[inline]
+ pub fn is_auto(self) -> bool {
+ matches!(self, ZIndex::Auto)
+ }
+
+ /// Returns the integer value if it is an integer, or `auto`.
+ #[inline]
+ pub fn integer_or(self, auto: Integer) -> Integer {
+ match self {
+ ZIndex::Integer(n) => n,
+ ZIndex::Auto => auto,
+ }
+ }
+}
+
+/// Ratio or None.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum PreferredRatio<N> {
+ /// Without specified ratio
+ #[css(skip)]
+ None,
+ /// With specified ratio
+ Ratio(
+ #[animation(field_bound)]
+ #[css(field_bound)]
+ #[distance(field_bound)]
+ Ratio<N>,
+ ),
+}
+
+/// A generic value for the `aspect-ratio` property, the value is `auto || <ratio>`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericAspectRatio<N> {
+ /// Specifiy auto or not.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub auto: bool,
+ /// The preferred aspect-ratio value.
+ #[animation(field_bound)]
+ #[css(field_bound)]
+ #[distance(field_bound)]
+ pub ratio: PreferredRatio<N>,
+}
+
+pub use self::GenericAspectRatio as AspectRatio;
+
+impl<N> AspectRatio<N> {
+ /// Returns `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ AspectRatio {
+ auto: true,
+ ratio: PreferredRatio::None,
+ }
+ }
+}
+
+impl<N> ToAnimatedZero for AspectRatio<N> {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
diff --git a/servo/components/style/values/generics/ratio.rs b/servo/components/style/values/generics/ratio.rs
new file mode 100644
index 0000000000..8c66fed602
--- /dev/null
+++ b/servo/components/style/values/generics/ratio.rs
@@ -0,0 +1,50 @@
+/* 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/. */
+
+//! Generic types for CSS values related to <ratio>.
+//! https://drafts.csswg.org/css-values/#ratios
+
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A generic value for the `<ratio>` value.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct Ratio<N>(pub N, pub N);
+
+impl<N> ToCss for Ratio<N>
+where
+ N: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest)?;
+ // Even though 1 could be omitted, we don't per
+ // https://drafts.csswg.org/css-values-4/#ratio-value:
+ //
+ // The second <number> is optional, defaulting to 1. However,
+ // <ratio> is always serialized with both components.
+ //
+ // And for compat reasons, see bug 1669742.
+ //
+ // We serialize with spaces for consistency with all other
+ // slash-delimited things, see
+ // https://github.com/w3c/csswg-drafts/issues/4282
+ dest.write_str(" / ")?;
+ self.1.to_css(dest)?;
+ Ok(())
+ }
+}
diff --git a/servo/components/style/values/generics/rect.rs b/servo/components/style/values/generics/rect.rs
new file mode 100644
index 0000000000..ea9de67732
--- /dev/null
+++ b/servo/components/style/values/generics/rect.rs
@@ -0,0 +1,146 @@
+/* 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/. */
+
+//! Generic types for CSS values that are composed of four sides.
+
+use crate::parser::{Parse, ParserContext};
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A CSS value made of four components, where its `ToCss` impl will try to
+/// serialize as few components as possible, like for example in `border-width`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct Rect<T>(pub T, pub T, pub T, pub T);
+
+impl<T> Rect<T> {
+ /// Returns a new `Rect<T>` value.
+ pub fn new(first: T, second: T, third: T, fourth: T) -> Self {
+ Rect(first, second, third, fourth)
+ }
+}
+
+impl<T> Rect<T>
+where
+ T: Clone,
+{
+ /// Returns a rect with all the values equal to `v`.
+ pub fn all(v: T) -> Self {
+ Rect::new(v.clone(), v.clone(), v.clone(), v)
+ }
+
+ /// Parses a new `Rect<T>` value with the given parse function.
+ pub fn parse_with<'i, 't, Parse>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse: Parse,
+ ) -> Result<Self, ParseError<'i>>
+ where
+ Parse: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>,
+ {
+ let first = parse(context, input)?;
+ let second = if let Ok(second) = input.try_parse(|i| parse(context, i)) {
+ second
+ } else {
+ // <first>
+ return Ok(Self::new(
+ first.clone(),
+ first.clone(),
+ first.clone(),
+ first,
+ ));
+ };
+ let third = if let Ok(third) = input.try_parse(|i| parse(context, i)) {
+ third
+ } else {
+ // <first> <second>
+ return Ok(Self::new(first.clone(), second.clone(), first, second));
+ };
+ let fourth = if let Ok(fourth) = input.try_parse(|i| parse(context, i)) {
+ fourth
+ } else {
+ // <first> <second> <third>
+ return Ok(Self::new(first, second.clone(), third, second));
+ };
+ // <first> <second> <third> <fourth>
+ Ok(Self::new(first, second, third, fourth))
+ }
+
+ /// Parses a new `Rect<T>` value which all components must be specified, with the given parse
+ /// function.
+ pub fn parse_all_components_with<'i, 't, Parse>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse: Parse,
+ ) -> Result<Self, ParseError<'i>>
+ where
+ Parse: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>,
+ {
+ let first = parse(context, input)?;
+ let second = parse(context, input)?;
+ let third = parse(context, input)?;
+ let fourth = parse(context, input)?;
+ // <first> <second> <third> <fourth>
+ Ok(Self::new(first, second, third, fourth))
+ }
+}
+
+impl<T> Parse for Rect<T>
+where
+ T: Clone + Parse,
+{
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, T::parse)
+ }
+}
+
+impl<T> ToCss for Rect<T>
+where
+ T: PartialEq + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest)?;
+ let same_vertical = self.0 == self.2;
+ let same_horizontal = self.1 == self.3;
+ if same_vertical && same_horizontal && self.0 == self.1 {
+ return Ok(());
+ }
+ dest.write_char(' ')?;
+ self.1.to_css(dest)?;
+ if same_vertical && same_horizontal {
+ return Ok(());
+ }
+ dest.write_char(' ')?;
+ self.2.to_css(dest)?;
+ if same_horizontal {
+ return Ok(());
+ }
+ dest.write_char(' ')?;
+ self.3.to_css(dest)
+ }
+}
diff --git a/servo/components/style/values/generics/size.rs b/servo/components/style/values/generics/size.rs
new file mode 100644
index 0000000000..979b8f9322
--- /dev/null
+++ b/servo/components/style/values/generics/size.rs
@@ -0,0 +1,101 @@
+/* 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/. */
+
+//! Generic type for CSS properties that are composed by two dimensions.
+
+use crate::parser::ParserContext;
+use crate::Zero;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A generic size, for `border-*-radius` longhand properties, or
+/// `border-spacing`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedZero,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(C)]
+pub struct Size2D<L> {
+ pub width: L,
+ pub height: L,
+}
+
+impl<L> Size2D<L> {
+ #[inline]
+ /// Create a new `Size2D` for an area of given width and height.
+ pub fn new(width: L, height: L) -> Self {
+ Self { width, height }
+ }
+
+ /// Returns the width component.
+ pub fn width(&self) -> &L {
+ &self.width
+ }
+
+ /// Returns the height component.
+ pub fn height(&self) -> &L {
+ &self.height
+ }
+
+ /// Parse a `Size2D` with a given parsing function.
+ pub fn parse_with<'i, 't, F>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse_one: F,
+ ) -> Result<Self, ParseError<'i>>
+ where
+ L: Clone,
+ F: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<L, ParseError<'i>>,
+ {
+ let first = parse_one(context, input)?;
+ let second = input
+ .try_parse(|i| parse_one(context, i))
+ .unwrap_or_else(|_| first.clone());
+ Ok(Self::new(first, second))
+ }
+}
+
+impl<L> ToCss for Size2D<L>
+where
+ L: ToCss + PartialEq,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.width.to_css(dest)?;
+
+ if self.height != self.width {
+ dest.write_char(' ')?;
+ self.height.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl<L: Zero> Zero for Size2D<L> {
+ fn zero() -> Self {
+ Self::new(L::zero(), L::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ self.width.is_zero() && self.height.is_zero()
+ }
+}
diff --git a/servo/components/style/values/generics/svg.rs b/servo/components/style/values/generics/svg.rs
new file mode 100644
index 0000000000..43ba77f1ff
--- /dev/null
+++ b/servo/components/style/values/generics/svg.rs
@@ -0,0 +1,221 @@
+/* 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/. */
+
+//! Generic types for CSS values in SVG
+
+use crate::parser::{Parse, ParserContext};
+use cssparser::Parser;
+use style_traits::ParseError;
+
+/// The fallback of an SVG paint server value.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSVGPaintFallback<C> {
+ /// The `none` keyword.
+ None,
+ /// A magic value that represents no fallback specified and serializes to
+ /// the empty string.
+ #[css(skip)]
+ Unset,
+ /// A color.
+ Color(C),
+}
+
+pub use self::GenericSVGPaintFallback as SVGPaintFallback;
+
+/// An SVG paint value
+///
+/// <https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint>
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(Url))]
+#[repr(C)]
+pub struct GenericSVGPaint<Color, Url> {
+ /// The paint source.
+ pub kind: GenericSVGPaintKind<Color, Url>,
+ /// The fallback color.
+ pub fallback: GenericSVGPaintFallback<Color>,
+}
+
+pub use self::GenericSVGPaint as SVGPaint;
+
+impl<C, U> Default for SVGPaint<C, U> {
+ fn default() -> Self {
+ Self {
+ kind: SVGPaintKind::None,
+ fallback: SVGPaintFallback::Unset,
+ }
+ }
+}
+
+/// An SVG paint value without the fallback.
+///
+/// Whereas the spec only allows PaintServer to have a fallback, Gecko lets the
+/// context properties have a fallback as well.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(U))]
+#[repr(C, u8)]
+pub enum GenericSVGPaintKind<C, U> {
+ /// `none`
+ #[animation(error)]
+ None,
+ /// `<color>`
+ Color(C),
+ /// `url(...)`
+ #[animation(error)]
+ PaintServer(U),
+ /// `context-fill`
+ ContextFill,
+ /// `context-stroke`
+ ContextStroke,
+}
+
+pub use self::GenericSVGPaintKind as SVGPaintKind;
+
+impl<C: Parse, U: Parse> Parse for SVGPaint<C, U> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let kind = SVGPaintKind::parse(context, input)?;
+ if matches!(kind, SVGPaintKind::None | SVGPaintKind::Color(..)) {
+ return Ok(SVGPaint {
+ kind,
+ fallback: SVGPaintFallback::Unset,
+ });
+ }
+ let fallback = input
+ .try_parse(|i| SVGPaintFallback::parse(context, i))
+ .unwrap_or(SVGPaintFallback::Unset);
+ Ok(SVGPaint { kind, fallback })
+ }
+}
+
+/// An SVG length value supports `context-value` in addition to length.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSVGLength<L> {
+ /// `<length> | <percentage> | <number>`
+ LengthPercentage(L),
+ /// `context-value`
+ #[animation(error)]
+ ContextValue,
+}
+
+pub use self::GenericSVGLength as SVGLength;
+
+/// Generic value for stroke-dasharray.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSVGStrokeDashArray<L> {
+ /// `[ <length> | <percentage> | <number> ]#`
+ #[css(comma)]
+ Values(#[css(if_empty = "none", iterable)] crate::OwnedSlice<L>),
+ /// `context-value`
+ ContextValue,
+}
+
+pub use self::GenericSVGStrokeDashArray as SVGStrokeDashArray;
+
+/// An SVG opacity value accepts `context-{fill,stroke}-opacity` in
+/// addition to opacity value.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSVGOpacity<OpacityType> {
+ /// `<opacity-value>`
+ Opacity(OpacityType),
+ /// `context-fill-opacity`
+ #[animation(error)]
+ ContextFillOpacity,
+ /// `context-stroke-opacity`
+ #[animation(error)]
+ ContextStrokeOpacity,
+}
+
+pub use self::GenericSVGOpacity as SVGOpacity;
diff --git a/servo/components/style/values/generics/text.rs b/servo/components/style/values/generics/text.rs
new file mode 100644
index 0000000000..ef2647b014
--- /dev/null
+++ b/servo/components/style/values/generics/text.rs
@@ -0,0 +1,148 @@
+/* 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/. */
+
+//! Generic types for text properties.
+
+use crate::parser::ParserContext;
+use crate::Zero;
+use cssparser::Parser;
+use style_traits::ParseError;
+
+/// A generic value for the `initial-letter` property.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum InitialLetter<Number, Integer> {
+ /// `normal`
+ Normal,
+ /// `<number> <integer>?`
+ Specified(Number, Option<Integer>),
+}
+
+impl<N, I> InitialLetter<N, I> {
+ /// Returns `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ InitialLetter::Normal
+ }
+}
+
+/// A generic spacing value for the `letter-spacing` and `word-spacing` properties.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum Spacing<Value> {
+ /// `normal`
+ Normal,
+ /// `<value>`
+ Value(Value),
+}
+
+impl<Value> Spacing<Value> {
+ /// Returns `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ Spacing::Normal
+ }
+
+ /// Parses.
+ #[inline]
+ pub fn parse_with<'i, 't, F>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse: F,
+ ) -> Result<Self, ParseError<'i>>
+ where
+ F: FnOnce(&ParserContext, &mut Parser<'i, 't>) -> Result<Value, ParseError<'i>>,
+ {
+ if input
+ .try_parse(|i| i.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(Spacing::Normal);
+ }
+ parse(context, input).map(Spacing::Value)
+ }
+}
+
+/// Implements type for text-decoration-thickness
+/// which takes the grammar of auto | from-font | <length> | <percentage>
+///
+/// https://drafts.csswg.org/css-text-decor-4/
+#[repr(C, u8)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ Copy,
+ ComputeSquaredDistance,
+ ToAnimatedZero,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum GenericTextDecorationLength<L> {
+ LengthPercentage(L),
+ Auto,
+ FromFont,
+}
+
+/// Implements type for text-indent
+/// which takes the grammar of [<length-percentage>] && hanging? && each-line?
+///
+/// https://drafts.csswg.org/css-text/#propdef-text-indent
+#[repr(C)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct GenericTextIndent<LengthPercentage> {
+ /// The amount of indent to be applied to the inline-start of the first line.
+ pub length: LengthPercentage,
+ /// Apply indent to non-first lines instead of first.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub hanging: bool,
+ /// Apply to each line after a hard break, not only first in block.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub each_line: bool,
+}
+
+impl<LengthPercentage: Zero> GenericTextIndent<LengthPercentage> {
+ /// Return the initial zero value.
+ pub fn zero() -> Self {
+ Self {
+ length: LengthPercentage::zero(),
+ hanging: false,
+ each_line: false,
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/transform.rs b/servo/components/style/values/generics/transform.rs
new file mode 100644
index 0000000000..3a65c460a7
--- /dev/null
+++ b/servo/components/style/values/generics/transform.rs
@@ -0,0 +1,879 @@
+/* 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/. */
+
+//! Generic types for CSS values that are related to transformations.
+
+use crate::values::computed::length::Length as ComputedLength;
+use crate::values::computed::length::LengthPercentage as ComputedLengthPercentage;
+use crate::values::specified::angle::Angle as SpecifiedAngle;
+use crate::values::specified::length::Length as SpecifiedLength;
+use crate::values::specified::length::LengthPercentage as SpecifiedLengthPercentage;
+use crate::values::{computed, CSSFloat};
+use crate::{Zero, ZeroNoPercent};
+use euclid;
+use euclid::default::{Rect, Transform3D};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A generic 2D transformation matrix.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma, function = "matrix")]
+#[repr(C)]
+pub struct GenericMatrix<T> {
+ pub a: T,
+ pub b: T,
+ pub c: T,
+ pub d: T,
+ pub e: T,
+ pub f: T,
+}
+
+pub use self::GenericMatrix as Matrix;
+
+#[allow(missing_docs)]
+#[cfg_attr(rustfmt, rustfmt_skip)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma, function = "matrix3d")]
+#[repr(C)]
+pub struct GenericMatrix3D<T> {
+ pub m11: T, pub m12: T, pub m13: T, pub m14: T,
+ pub m21: T, pub m22: T, pub m23: T, pub m24: T,
+ pub m31: T, pub m32: T, pub m33: T, pub m34: T,
+ pub m41: T, pub m42: T, pub m43: T, pub m44: T,
+}
+
+pub use self::GenericMatrix3D as Matrix3D;
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl<T: Into<f64>> From<Matrix<T>> for Transform3D<f64> {
+ #[inline]
+ fn from(m: Matrix<T>) -> Self {
+ Transform3D::new(
+ m.a.into(), m.b.into(), 0.0, 0.0,
+ m.c.into(), m.d.into(), 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0,
+ m.e.into(), m.f.into(), 0.0, 1.0,
+ )
+ }
+}
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl<T: Into<f64>> From<Matrix3D<T>> for Transform3D<f64> {
+ #[inline]
+ fn from(m: Matrix3D<T>) -> Self {
+ Transform3D::new(
+ m.m11.into(), m.m12.into(), m.m13.into(), m.m14.into(),
+ m.m21.into(), m.m22.into(), m.m23.into(), m.m24.into(),
+ m.m31.into(), m.m32.into(), m.m33.into(), m.m34.into(),
+ m.m41.into(), m.m42.into(), m.m43.into(), m.m44.into(),
+ )
+ }
+}
+
+/// A generic transform origin.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericTransformOrigin<H, V, Depth> {
+ /// The horizontal origin.
+ pub horizontal: H,
+ /// The vertical origin.
+ pub vertical: V,
+ /// The depth.
+ pub depth: Depth,
+}
+
+pub use self::GenericTransformOrigin as TransformOrigin;
+
+impl<H, V, D> TransformOrigin<H, V, D> {
+ /// Returns a new transform origin.
+ pub fn new(horizontal: H, vertical: V, depth: D) -> Self {
+ Self {
+ horizontal,
+ vertical,
+ depth,
+ }
+ }
+}
+
+fn is_same<N: PartialEq>(x: &N, y: &N) -> bool {
+ x == y
+}
+
+/// A value for the `perspective()` transform function, which is either a
+/// non-negative `<length>` or `none`.
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericPerspectiveFunction<L> {
+ /// `none`
+ None,
+ /// A `<length>`.
+ Length(L),
+}
+
+impl<L> GenericPerspectiveFunction<L> {
+ /// Returns `f32::INFINITY` or the result of a function on the length value.
+ pub fn infinity_or(&self, f: impl FnOnce(&L) -> f32) -> f32 {
+ match *self {
+ Self::None => f32::INFINITY,
+ Self::Length(ref l) => f(l),
+ }
+ }
+}
+
+pub use self::GenericPerspectiveFunction as PerspectiveFunction;
+
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// A single operation in the list of a `transform` value
+pub enum GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>
+where
+ Angle: Zero,
+ LengthPercentage: Zero + ZeroNoPercent,
+ Number: PartialEq,
+{
+ /// Represents a 2D 2x3 matrix.
+ Matrix(GenericMatrix<Number>),
+ /// Represents a 3D 4x4 matrix.
+ Matrix3D(GenericMatrix3D<Number>),
+ /// A 2D skew.
+ ///
+ /// If the second angle is not provided it is assumed zero.
+ ///
+ /// Syntax can be skew(angle) or skew(angle, angle)
+ #[css(comma, function)]
+ Skew(Angle, #[css(skip_if = "Zero::is_zero")] Angle),
+ /// skewX(angle)
+ #[css(function = "skewX")]
+ SkewX(Angle),
+ /// skewY(angle)
+ #[css(function = "skewY")]
+ SkewY(Angle),
+ /// translate(x, y) or translate(x)
+ #[css(comma, function)]
+ Translate(
+ LengthPercentage,
+ #[css(skip_if = "ZeroNoPercent::is_zero_no_percent")] LengthPercentage,
+ ),
+ /// translateX(x)
+ #[css(function = "translateX")]
+ TranslateX(LengthPercentage),
+ /// translateY(y)
+ #[css(function = "translateY")]
+ TranslateY(LengthPercentage),
+ /// translateZ(z)
+ #[css(function = "translateZ")]
+ TranslateZ(Length),
+ /// translate3d(x, y, z)
+ #[css(comma, function = "translate3d")]
+ Translate3D(LengthPercentage, LengthPercentage, Length),
+ /// A 2D scaling factor.
+ ///
+ /// Syntax can be scale(factor) or scale(factor, factor)
+ #[css(comma, function)]
+ Scale(Number, #[css(contextual_skip_if = "is_same")] Number),
+ /// scaleX(factor)
+ #[css(function = "scaleX")]
+ ScaleX(Number),
+ /// scaleY(factor)
+ #[css(function = "scaleY")]
+ ScaleY(Number),
+ /// scaleZ(factor)
+ #[css(function = "scaleZ")]
+ ScaleZ(Number),
+ /// scale3D(factorX, factorY, factorZ)
+ #[css(comma, function = "scale3d")]
+ Scale3D(Number, Number, Number),
+ /// Describes a 2D Rotation.
+ ///
+ /// In a 3D scene `rotate(angle)` is equivalent to `rotateZ(angle)`.
+ #[css(function)]
+ Rotate(Angle),
+ /// Rotation in 3D space around the x-axis.
+ #[css(function = "rotateX")]
+ RotateX(Angle),
+ /// Rotation in 3D space around the y-axis.
+ #[css(function = "rotateY")]
+ RotateY(Angle),
+ /// Rotation in 3D space around the z-axis.
+ #[css(function = "rotateZ")]
+ RotateZ(Angle),
+ /// Rotation in 3D space.
+ ///
+ /// Generalization of rotateX, rotateY and rotateZ.
+ #[css(comma, function = "rotate3d")]
+ Rotate3D(Number, Number, Number, Angle),
+ /// Specifies a perspective projection matrix.
+ ///
+ /// Part of CSS Transform Module Level 2 and defined at
+ /// [§ 13.1. 3D Transform Function](https://drafts.csswg.org/css-transforms-2/#funcdef-perspective).
+ ///
+ /// The value must be greater than or equal to zero.
+ #[css(function)]
+ Perspective(GenericPerspectiveFunction<Length>),
+ /// A intermediate type for interpolation of mismatched transform lists.
+ #[allow(missing_docs)]
+ #[css(comma, function = "interpolatematrix")]
+ InterpolateMatrix {
+ from_list: GenericTransform<
+ GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>,
+ >,
+ to_list: GenericTransform<
+ GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>,
+ >,
+ progress: computed::Percentage,
+ },
+ /// A intermediate type for accumulation of mismatched transform lists.
+ #[allow(missing_docs)]
+ #[css(comma, function = "accumulatematrix")]
+ AccumulateMatrix {
+ from_list: GenericTransform<
+ GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>,
+ >,
+ to_list: GenericTransform<
+ GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>,
+ >,
+ count: Integer,
+ },
+}
+
+pub use self::GenericTransformOperation as TransformOperation;
+
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// A value of the `transform` property
+pub struct GenericTransform<T>(#[css(if_empty = "none", iterable)] pub crate::OwnedSlice<T>);
+
+pub use self::GenericTransform as Transform;
+
+impl<Angle, Number, Length, Integer, LengthPercentage>
+ TransformOperation<Angle, Number, Length, Integer, LengthPercentage>
+where
+ Angle: Zero,
+ LengthPercentage: Zero + ZeroNoPercent,
+ Number: PartialEq,
+{
+ /// Check if it is any rotate function.
+ pub fn is_rotate(&self) -> bool {
+ use self::TransformOperation::*;
+ matches!(
+ *self,
+ Rotate(..) | Rotate3D(..) | RotateX(..) | RotateY(..) | RotateZ(..)
+ )
+ }
+
+ /// Check if it is any translate function
+ pub fn is_translate(&self) -> bool {
+ use self::TransformOperation::*;
+ match *self {
+ Translate(..) | Translate3D(..) | TranslateX(..) | TranslateY(..) | TranslateZ(..) => {
+ true
+ },
+ _ => false,
+ }
+ }
+
+ /// Check if it is any scale function
+ pub fn is_scale(&self) -> bool {
+ use self::TransformOperation::*;
+ match *self {
+ Scale(..) | Scale3D(..) | ScaleX(..) | ScaleY(..) | ScaleZ(..) => true,
+ _ => false,
+ }
+ }
+}
+
+/// Convert a length type into the absolute lengths.
+pub trait ToAbsoluteLength {
+ /// Returns the absolute length as pixel value.
+ fn to_pixel_length(&self, containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()>;
+}
+
+impl ToAbsoluteLength for SpecifiedLength {
+ // This returns Err(()) if there is any relative length or percentage. We use this when
+ // parsing a transform list of DOMMatrix because we want to return a DOM Exception
+ // if there is relative length.
+ #[inline]
+ fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> {
+ match *self {
+ SpecifiedLength::NoCalc(len) => len.to_computed_pixel_length_without_context(),
+ SpecifiedLength::Calc(ref calc) => calc.to_computed_pixel_length_without_context(),
+ }
+ }
+}
+
+impl ToAbsoluteLength for SpecifiedLengthPercentage {
+ // This returns Err(()) if there is any relative length or percentage. We use this when
+ // parsing a transform list of DOMMatrix because we want to return a DOM Exception
+ // if there is relative length.
+ #[inline]
+ fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> {
+ use self::SpecifiedLengthPercentage::*;
+ match *self {
+ Length(len) => len.to_computed_pixel_length_without_context(),
+ Calc(ref calc) => calc.to_computed_pixel_length_without_context(),
+ Percentage(..) => Err(()),
+ }
+ }
+}
+
+impl ToAbsoluteLength for ComputedLength {
+ #[inline]
+ fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> {
+ Ok(self.px())
+ }
+}
+
+impl ToAbsoluteLength for ComputedLengthPercentage {
+ #[inline]
+ fn to_pixel_length(&self, containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> {
+ Ok(self
+ .maybe_percentage_relative_to(containing_len)
+ .ok_or(())?
+ .px())
+ }
+}
+
+/// Support the conversion to a 3d matrix.
+pub trait ToMatrix {
+ /// Check if it is a 3d transform function.
+ fn is_3d(&self) -> bool;
+
+ /// Return the equivalent 3d matrix.
+ fn to_3d_matrix(
+ &self,
+ reference_box: Option<&Rect<ComputedLength>>,
+ ) -> Result<Transform3D<f64>, ()>;
+}
+
+/// A little helper to deal with both specified and computed angles.
+pub trait ToRadians {
+ /// Return the radians value as a 64-bit floating point value.
+ fn radians64(&self) -> f64;
+}
+
+impl ToRadians for computed::angle::Angle {
+ #[inline]
+ fn radians64(&self) -> f64 {
+ computed::angle::Angle::radians64(self)
+ }
+}
+
+impl ToRadians for SpecifiedAngle {
+ #[inline]
+ fn radians64(&self) -> f64 {
+ computed::angle::Angle::from_degrees(self.degrees()).radians64()
+ }
+}
+
+impl<Angle, Number, Length, Integer, LoP> ToMatrix
+ for TransformOperation<Angle, Number, Length, Integer, LoP>
+where
+ Angle: Zero + ToRadians + Copy,
+ Number: PartialEq + Copy + Into<f32> + Into<f64>,
+ Length: ToAbsoluteLength,
+ LoP: Zero + ToAbsoluteLength + ZeroNoPercent,
+{
+ #[inline]
+ fn is_3d(&self) -> bool {
+ use self::TransformOperation::*;
+ match *self {
+ Translate3D(..) | TranslateZ(..) | Rotate3D(..) | RotateX(..) | RotateY(..) |
+ RotateZ(..) | Scale3D(..) | ScaleZ(..) | Perspective(..) | Matrix3D(..) => true,
+ _ => false,
+ }
+ }
+
+ /// If |reference_box| is None, we will drop the percent part from translate because
+ /// we cannot resolve it without the layout info, for computed TransformOperation.
+ /// However, for specified TransformOperation, we will return Err(()) if there is any relative
+ /// lengths because the only caller, DOMMatrix, doesn't accept relative lengths.
+ #[inline]
+ fn to_3d_matrix(
+ &self,
+ reference_box: Option<&Rect<ComputedLength>>,
+ ) -> Result<Transform3D<f64>, ()> {
+ use self::TransformOperation::*;
+
+ let reference_width = reference_box.map(|v| v.size.width);
+ let reference_height = reference_box.map(|v| v.size.height);
+ let matrix = match *self {
+ Rotate3D(ax, ay, az, theta) => {
+ let theta = theta.radians64();
+ let (ax, ay, az, theta) =
+ get_normalized_vector_and_angle(ax.into(), ay.into(), az.into(), theta);
+ Transform3D::rotation(
+ ax as f64,
+ ay as f64,
+ az as f64,
+ euclid::Angle::radians(theta),
+ )
+ },
+ RotateX(theta) => {
+ let theta = euclid::Angle::radians(theta.radians64());
+ Transform3D::rotation(1., 0., 0., theta)
+ },
+ RotateY(theta) => {
+ let theta = euclid::Angle::radians(theta.radians64());
+ Transform3D::rotation(0., 1., 0., theta)
+ },
+ RotateZ(theta) | Rotate(theta) => {
+ let theta = euclid::Angle::radians(theta.radians64());
+ Transform3D::rotation(0., 0., 1., theta)
+ },
+ Perspective(ref p) => {
+ let px = match p {
+ PerspectiveFunction::None => f32::INFINITY,
+ PerspectiveFunction::Length(ref p) => p.to_pixel_length(None)?,
+ };
+ create_perspective_matrix(px).cast()
+ },
+ Scale3D(sx, sy, sz) => Transform3D::scale(sx.into(), sy.into(), sz.into()),
+ Scale(sx, sy) => Transform3D::scale(sx.into(), sy.into(), 1.),
+ ScaleX(s) => Transform3D::scale(s.into(), 1., 1.),
+ ScaleY(s) => Transform3D::scale(1., s.into(), 1.),
+ ScaleZ(s) => Transform3D::scale(1., 1., s.into()),
+ Translate3D(ref tx, ref ty, ref tz) => {
+ let tx = tx.to_pixel_length(reference_width)? as f64;
+ let ty = ty.to_pixel_length(reference_height)? as f64;
+ Transform3D::translation(tx, ty, tz.to_pixel_length(None)? as f64)
+ },
+ Translate(ref tx, ref ty) => {
+ let tx = tx.to_pixel_length(reference_width)? as f64;
+ let ty = ty.to_pixel_length(reference_height)? as f64;
+ Transform3D::translation(tx, ty, 0.)
+ },
+ TranslateX(ref t) => {
+ let t = t.to_pixel_length(reference_width)? as f64;
+ Transform3D::translation(t, 0., 0.)
+ },
+ TranslateY(ref t) => {
+ let t = t.to_pixel_length(reference_height)? as f64;
+ Transform3D::translation(0., t, 0.)
+ },
+ TranslateZ(ref z) => Transform3D::translation(0., 0., z.to_pixel_length(None)? as f64),
+ Skew(theta_x, theta_y) => Transform3D::skew(
+ euclid::Angle::radians(theta_x.radians64()),
+ euclid::Angle::radians(theta_y.radians64()),
+ ),
+ SkewX(theta) => Transform3D::skew(
+ euclid::Angle::radians(theta.radians64()),
+ euclid::Angle::radians(0.),
+ ),
+ SkewY(theta) => Transform3D::skew(
+ euclid::Angle::radians(0.),
+ euclid::Angle::radians(theta.radians64()),
+ ),
+ Matrix3D(m) => m.into(),
+ Matrix(m) => m.into(),
+ InterpolateMatrix { .. } | AccumulateMatrix { .. } => {
+ // TODO: Convert InterpolateMatrix/AccumulateMatrix into a valid Transform3D by
+ // the reference box and do interpolation on these two Transform3D matrices.
+ // Both Gecko and Servo don't support this for computing distance, and Servo
+ // doesn't support animations on InterpolateMatrix/AccumulateMatrix, so
+ // return an identity matrix.
+ // Note: DOMMatrix doesn't go into this arm.
+ Transform3D::identity()
+ },
+ };
+ Ok(matrix)
+ }
+}
+
+impl<T> Transform<T> {
+ /// `none`
+ pub fn none() -> Self {
+ Transform(Default::default())
+ }
+}
+
+impl<T: ToMatrix> Transform<T> {
+ /// Return the equivalent 3d matrix of this transform list.
+ ///
+ /// We return a pair: the first one is the transform matrix, and the second one
+ /// indicates if there is any 3d transform function in this transform list.
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ pub fn to_transform_3d_matrix(
+ &self,
+ reference_box: Option<&Rect<ComputedLength>>
+ ) -> Result<(Transform3D<CSSFloat>, bool), ()> {
+ Self::components_to_transform_3d_matrix(&self.0, reference_box)
+ }
+
+ /// Converts a series of components to a 3d matrix.
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ pub fn components_to_transform_3d_matrix(
+ ops: &[T],
+ reference_box: Option<&Rect<ComputedLength>>,
+ ) -> Result<(Transform3D<CSSFloat>, bool), ()> {
+ let cast_3d_transform = |m: Transform3D<f64>| -> Transform3D<CSSFloat> {
+ use std::{f32, f64};
+ let cast = |v: f64| v.min(f32::MAX as f64).max(f32::MIN as f64) as f32;
+ Transform3D::new(
+ cast(m.m11), cast(m.m12), cast(m.m13), cast(m.m14),
+ cast(m.m21), cast(m.m22), cast(m.m23), cast(m.m24),
+ cast(m.m31), cast(m.m32), cast(m.m33), cast(m.m34),
+ cast(m.m41), cast(m.m42), cast(m.m43), cast(m.m44),
+ )
+ };
+
+ let (m, is_3d) = Self::components_to_transform_3d_matrix_f64(ops, reference_box)?;
+ Ok((cast_3d_transform(m), is_3d))
+ }
+
+ /// Same as Transform::to_transform_3d_matrix but a f64 version.
+ fn components_to_transform_3d_matrix_f64(
+ ops: &[T],
+ reference_box: Option<&Rect<ComputedLength>>,
+ ) -> Result<(Transform3D<f64>, bool), ()> {
+ // We intentionally use Transform3D<f64> during computation to avoid
+ // error propagation because using f32 to compute triangle functions
+ // (e.g. in rotation()) is not accurate enough. In Gecko, we also use
+ // "double" to compute the triangle functions. Therefore, let's use
+ // Transform3D<f64> during matrix computation and cast it into f32 in
+ // the end.
+ let mut transform = Transform3D::<f64>::identity();
+ let mut contain_3d = false;
+
+ for operation in ops {
+ let matrix = operation.to_3d_matrix(reference_box)?;
+ contain_3d = contain_3d || operation.is_3d();
+ transform = matrix.then(&transform);
+ }
+
+ Ok((transform, contain_3d))
+ }
+}
+
+/// Return the transform matrix from a perspective length.
+#[inline]
+pub fn create_perspective_matrix(d: CSSFloat) -> Transform3D<CSSFloat> {
+ if d.is_finite() {
+ Transform3D::perspective(d.max(1.))
+ } else {
+ Transform3D::identity()
+ }
+}
+
+/// Return the normalized direction vector and its angle for Rotate3D.
+pub fn get_normalized_vector_and_angle<T: Zero>(
+ x: CSSFloat,
+ y: CSSFloat,
+ z: CSSFloat,
+ angle: T,
+) -> (CSSFloat, CSSFloat, CSSFloat, T) {
+ use crate::values::computed::transform::DirectionVector;
+ use euclid::approxeq::ApproxEq;
+ let vector = DirectionVector::new(x, y, z);
+ if vector.square_length().approx_eq(&f32::zero()) {
+ // https://www.w3.org/TR/css-transforms-1/#funcdef-rotate3d
+ // A direction vector that cannot be normalized, such as [0, 0, 0], will cause the
+ // rotation to not be applied, so we use identity matrix (i.e. rotate3d(0, 0, 1, 0)).
+ (0., 0., 1., T::zero())
+ } else {
+ let vector = vector.robust_normalize();
+ (vector.x, vector.y, vector.z, angle)
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// A value of the `Rotate` property
+///
+/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms>
+pub enum GenericRotate<Number, Angle> {
+ /// 'none'
+ None,
+ /// '<angle>'
+ Rotate(Angle),
+ /// '<number>{3} <angle>'
+ Rotate3D(Number, Number, Number, Angle),
+}
+
+pub use self::GenericRotate as Rotate;
+
+/// A trait to check if the current 3D vector is parallel to the DirectionVector.
+/// This is especially for serialization on Rotate.
+pub trait IsParallelTo {
+ /// Returns true if this is parallel to the vector.
+ fn is_parallel_to(&self, vector: &computed::transform::DirectionVector) -> bool;
+}
+
+impl<Number, Angle> ToCss for Rotate<Number, Angle>
+where
+ Number: Copy + ToCss + Zero,
+ Angle: ToCss,
+ (Number, Number, Number): IsParallelTo,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use crate::values::computed::transform::DirectionVector;
+ match *self {
+ Rotate::None => dest.write_str("none"),
+ Rotate::Rotate(ref angle) => angle.to_css(dest),
+ Rotate::Rotate3D(x, y, z, ref angle) => {
+ // If the axis is parallel with the x or y axes, it must serialize as the
+ // appropriate keyword. If a rotation about the z axis (that is, in 2D) is
+ // specified, the property must serialize as just an <angle>
+ //
+ // https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization
+ let v = (x, y, z);
+ let axis = if x.is_zero() && y.is_zero() && z.is_zero() {
+ // The zero length vector is parallel to every other vector, so
+ // is_parallel_to() returns true for it. However, it is definitely different
+ // from x axis, y axis, or z axis, and it's meaningless to perform a rotation
+ // using that direction vector. So we *have* to serialize it using that same
+ // vector - we can't simplify to some theoretically parallel axis-aligned
+ // vector.
+ None
+ } else if v.is_parallel_to(&DirectionVector::new(1., 0., 0.)) {
+ Some("x ")
+ } else if v.is_parallel_to(&DirectionVector::new(0., 1., 0.)) {
+ Some("y ")
+ } else if v.is_parallel_to(&DirectionVector::new(0., 0., 1.)) {
+ // When we're parallel to the z-axis, we can just serialize the angle.
+ return angle.to_css(dest);
+ } else {
+ None
+ };
+ match axis {
+ Some(a) => dest.write_str(a)?,
+ None => {
+ x.to_css(dest)?;
+ dest.write_char(' ')?;
+ y.to_css(dest)?;
+ dest.write_char(' ')?;
+ z.to_css(dest)?;
+ dest.write_char(' ')?;
+ },
+ }
+ angle.to_css(dest)
+ },
+ }
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// A value of the `Scale` property
+///
+/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms>
+pub enum GenericScale<Number> {
+ /// 'none'
+ None,
+ /// '<number>{1,3}'
+ Scale(Number, Number, Number),
+}
+
+pub use self::GenericScale as Scale;
+
+impl<Number> ToCss for Scale<Number>
+where
+ Number: ToCss + PartialEq + Copy,
+ f32: From<Number>,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ f32: From<Number>,
+ {
+ match *self {
+ Scale::None => dest.write_str("none"),
+ Scale::Scale(ref x, ref y, ref z) => {
+ x.to_css(dest)?;
+
+ let is_3d = f32::from(*z) != 1.0;
+ if is_3d || x != y {
+ dest.write_char(' ')?;
+ y.to_css(dest)?;
+ }
+
+ if is_3d {
+ dest.write_char(' ')?;
+ z.to_css(dest)?;
+ }
+ Ok(())
+ },
+ }
+ }
+}
+
+#[inline]
+fn y_axis_and_z_axis_are_zero<LengthPercentage: Zero + ZeroNoPercent, Length: Zero>(
+ _: &LengthPercentage,
+ y: &LengthPercentage,
+ z: &Length,
+) -> bool {
+ y.is_zero_no_percent() && z.is_zero()
+}
+
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// A value of the `translate` property
+///
+/// https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization:
+///
+/// If a 2d translation is specified, the property must serialize with only one
+/// or two values (per usual, if the second value is 0px, the default, it must
+/// be omitted when serializing; however if 0% is the second value, it is included).
+///
+/// If a 3d translation is specified and the value can be expressed as 2d, we treat as 2d and
+/// serialize accoringly. Otherwise, we serialize all three values.
+/// https://github.com/w3c/csswg-drafts/issues/3305
+///
+/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms>
+pub enum GenericTranslate<LengthPercentage, Length>
+where
+ LengthPercentage: Zero + ZeroNoPercent,
+ Length: Zero,
+{
+ /// 'none'
+ None,
+ /// <length-percentage> [ <length-percentage> <length>? ]?
+ Translate(
+ LengthPercentage,
+ #[css(contextual_skip_if = "y_axis_and_z_axis_are_zero")] LengthPercentage,
+ #[css(skip_if = "Zero::is_zero")] Length,
+ ),
+}
+
+pub use self::GenericTranslate as Translate;
+
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum TransformStyle {
+ Flat,
+ #[css(keyword = "preserve-3d")]
+ Preserve3d,
+}
diff --git a/servo/components/style/values/generics/ui.rs b/servo/components/style/values/generics/ui.rs
new file mode 100644
index 0000000000..87c8674182
--- /dev/null
+++ b/servo/components/style/values/generics/ui.rs
@@ -0,0 +1,129 @@
+/* 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/. */
+
+//! Generic values for UI properties.
+
+use crate::values::specified::ui::CursorKind;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A generic value for the `cursor` property.
+///
+/// https://drafts.csswg.org/css-ui/#cursor
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericCursor<Image> {
+ /// The parsed images for the cursor.
+ pub images: crate::OwnedSlice<Image>,
+ /// The kind of the cursor [default | help | ...].
+ pub keyword: CursorKind,
+}
+
+pub use self::GenericCursor as Cursor;
+
+impl<Image> Cursor<Image> {
+ /// Set `cursor` to `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ Self {
+ images: Default::default(),
+ keyword: CursorKind::Auto,
+ }
+ }
+}
+
+impl<Image: ToCss> ToCss for Cursor<Image> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ for image in &*self.images {
+ image.to_css(dest)?;
+ dest.write_str(", ")?;
+ }
+ self.keyword.to_css(dest)
+ }
+}
+
+/// A generic value for item of `image cursors`.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub struct GenericCursorImage<Image, Number> {
+ /// The url to parse images from.
+ pub image: Image,
+ /// Whether the image has a hotspot or not.
+ pub has_hotspot: bool,
+ /// The x coordinate.
+ pub hotspot_x: Number,
+ /// The y coordinate.
+ pub hotspot_y: Number,
+}
+
+pub use self::GenericCursorImage as CursorImage;
+
+impl<Image: ToCss, Number: ToCss> ToCss for CursorImage<Image, Number> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.image.to_css(dest)?;
+ if self.has_hotspot {
+ dest.write_char(' ')?;
+ self.hotspot_x.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.hotspot_y.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// A generic value for `scrollbar-color` property.
+///
+/// https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericScrollbarColor<Color> {
+ /// `auto`
+ Auto,
+ /// `<color>{2}`
+ Colors {
+ /// First `<color>`, for color of the scrollbar thumb.
+ thumb: Color,
+ /// Second `<color>`, for color of the scrollbar track.
+ track: Color,
+ },
+}
+
+pub use self::GenericScrollbarColor as ScrollbarColor;
+
+impl<Color> Default for ScrollbarColor<Color> {
+ #[inline]
+ fn default() -> Self {
+ ScrollbarColor::Auto
+ }
+}
diff --git a/servo/components/style/values/generics/url.rs b/servo/components/style/values/generics/url.rs
new file mode 100644
index 0000000000..46ed453e82
--- /dev/null
+++ b/servo/components/style/values/generics/url.rs
@@ -0,0 +1,47 @@
+/* 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/. */
+
+//! Generic types for url properties.
+
+/// An image url or none, used for example in list-style-image
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericUrlOrNone<U> {
+ /// `none`
+ None,
+ /// A URL.
+ Url(U),
+}
+
+pub use self::GenericUrlOrNone as UrlOrNone;
+
+impl<Url> UrlOrNone<Url> {
+ /// Initial "none" value for properties such as `list-style-image`
+ pub fn none() -> Self {
+ UrlOrNone::None
+ }
+
+ /// Returns whether the value is `none`.
+ pub fn is_none(&self) -> bool {
+ match *self {
+ UrlOrNone::None => true,
+ UrlOrNone::Url(..) => false,
+ }
+ }
+}
diff --git a/servo/components/style/values/mod.rs b/servo/components/style/values/mod.rs
new file mode 100644
index 0000000000..6138d5a2ab
--- /dev/null
+++ b/servo/components/style/values/mod.rs
@@ -0,0 +1,796 @@
+/* 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/. */
+
+//! Common [values][values] used in CSS.
+//!
+//! [values]: https://drafts.csswg.org/css-values/
+
+#![deny(missing_docs)]
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::Atom;
+pub use cssparser::{serialize_identifier, serialize_name, CowRcStr, Parser};
+pub use cssparser::{SourceLocation, Token};
+use precomputed_hash::PrecomputedHash;
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Debug, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+use to_shmem::impl_trivial_to_shmem;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::url::CssUrl;
+#[cfg(feature = "servo")]
+pub use crate::servo::url::CssUrl;
+
+pub mod animated;
+pub mod computed;
+pub mod distance;
+pub mod generics;
+pub mod resolved;
+pub mod specified;
+
+/// A CSS float value.
+pub type CSSFloat = f32;
+
+/// Normalizes a float value to zero after a set of operations that might turn
+/// it into NaN.
+#[inline]
+pub fn normalize(v: CSSFloat) -> CSSFloat {
+ if v.is_nan() {
+ 0.0
+ } else {
+ v
+ }
+}
+
+/// A CSS integer value.
+pub type CSSInteger = i32;
+
+/// Serialize an identifier which is represented as an atom.
+#[cfg(feature = "gecko")]
+pub fn serialize_atom_identifier<W>(ident: &Atom, dest: &mut W) -> fmt::Result
+where
+ W: Write,
+{
+ ident.with_str(|s| serialize_identifier(s, dest))
+}
+
+/// Serialize an identifier which is represented as an atom.
+#[cfg(feature = "servo")]
+pub fn serialize_atom_identifier<Static, W>(
+ ident: &::string_cache::Atom<Static>,
+ dest: &mut W,
+) -> fmt::Result
+where
+ Static: string_cache::StaticAtomSet,
+ W: Write,
+{
+ serialize_identifier(&ident, dest)
+}
+
+/// Serialize a name which is represented as an Atom.
+#[cfg(feature = "gecko")]
+pub fn serialize_atom_name<W>(ident: &Atom, dest: &mut W) -> fmt::Result
+where
+ W: Write,
+{
+ ident.with_str(|s| serialize_name(s, dest))
+}
+
+/// Serialize a name which is represented as an Atom.
+#[cfg(feature = "servo")]
+pub fn serialize_atom_name<Static, W>(
+ ident: &::string_cache::Atom<Static>,
+ dest: &mut W,
+) -> fmt::Result
+where
+ Static: string_cache::StaticAtomSet,
+ W: Write,
+{
+ serialize_name(&ident, dest)
+}
+
+/// Serialize a number with calc, and NaN/infinity handling (if enabled)
+pub fn serialize_number<W>(v: f32, was_calc: bool, dest: &mut CssWriter<W>) -> fmt::Result
+where
+ W: Write,
+{
+ serialize_specified_dimension(v, "", was_calc, dest)
+}
+
+/// Serialize a specified dimension with unit, calc, and NaN/infinity handling (if enabled)
+pub fn serialize_specified_dimension<W>(
+ v: f32,
+ unit: &str,
+ was_calc: bool,
+ dest: &mut CssWriter<W>,
+) -> fmt::Result
+where
+ W: Write,
+{
+ if was_calc {
+ dest.write_str("calc(")?;
+ }
+
+ if !v.is_finite() {
+ // https://drafts.csswg.org/css-values/#calc-error-constants:
+ // "While not technically numbers, these keywords act as numeric values,
+ // similar to e and pi. Thus to get an infinite length, for example,
+ // requires an expression like calc(infinity * 1px)."
+
+ if v.is_nan() {
+ dest.write_str("NaN")?;
+ } else if v == f32::INFINITY {
+ dest.write_str("infinity")?;
+ } else if v == f32::NEG_INFINITY {
+ dest.write_str("-infinity")?;
+ }
+
+ if !unit.is_empty() {
+ dest.write_str(" * 1")?;
+ }
+ } else {
+ v.to_css(dest)?;
+ }
+
+ dest.write_str(unit)?;
+
+ if was_calc {
+ dest.write_char(')')?;
+ }
+ Ok(())
+}
+
+/// A CSS string stored as an `Atom`.
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Deref,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct AtomString(pub Atom);
+
+#[cfg(feature = "servo")]
+impl AsRef<str> for AtomString {
+ fn as_ref(&self) -> &str {
+ &*self.0
+ }
+}
+
+impl cssparser::ToCss for AtomString {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: Write,
+ {
+ // Wrap in quotes to form a string literal
+ dest.write_char('"')?;
+ #[cfg(feature = "servo")]
+ {
+ cssparser::CssStringWriter::new(dest).write_str(self.as_ref())?;
+ }
+ #[cfg(feature = "gecko")]
+ {
+ self.0
+ .with_str(|s| cssparser::CssStringWriter::new(dest).write_str(s))?;
+ }
+ dest.write_char('"')
+ }
+}
+
+impl style_traits::ToCss for AtomString {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ cssparser::ToCss::to_css(self, dest)
+ }
+}
+
+impl PrecomputedHash for AtomString {
+ #[inline]
+ fn precomputed_hash(&self) -> u32 {
+ self.0.precomputed_hash()
+ }
+}
+
+impl<'a> From<&'a str> for AtomString {
+ #[inline]
+ fn from(string: &str) -> Self {
+ Self(Atom::from(string))
+ }
+}
+
+/// A generic CSS `<ident>` stored as an `Atom`.
+#[cfg(feature = "servo")]
+#[repr(transparent)]
+#[derive(Deref)]
+pub struct GenericAtomIdent<Set>(pub string_cache::Atom<Set>)
+where
+ Set: string_cache::StaticAtomSet;
+
+/// A generic CSS `<ident>` stored as an `Atom`, for the default atom set.
+#[cfg(feature = "servo")]
+pub type AtomIdent = GenericAtomIdent<servo_atoms::AtomStaticSet>;
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> style_traits::SpecifiedValueInfo for GenericAtomIdent<Set> {}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> Default for GenericAtomIdent<Set> {
+ fn default() -> Self {
+ Self(string_cache::Atom::default())
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> std::fmt::Debug for GenericAtomIdent<Set> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> std::hash::Hash for GenericAtomIdent<Set> {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.0.hash(state)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> Eq for GenericAtomIdent<Set> {}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> PartialEq for GenericAtomIdent<Set> {
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> Clone for GenericAtomIdent<Set> {
+ fn clone(&self) -> Self {
+ Self(self.0.clone())
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> to_shmem::ToShmem for GenericAtomIdent<Set> {
+ fn to_shmem(&self, builder: &mut to_shmem::SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ use std::mem::ManuallyDrop;
+
+ let atom = self.0.to_shmem(builder)?;
+ Ok(ManuallyDrop::new(Self(ManuallyDrop::into_inner(atom))))
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> malloc_size_of::MallocSizeOf for GenericAtomIdent<Set> {
+ fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
+ self.0.size_of(ops)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> cssparser::ToCss for GenericAtomIdent<Set> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> PrecomputedHash for GenericAtomIdent<Set> {
+ #[inline]
+ fn precomputed_hash(&self) -> u32 {
+ self.0.precomputed_hash()
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<'a, Set: string_cache::StaticAtomSet> From<&'a str> for GenericAtomIdent<Set> {
+ #[inline]
+ fn from(string: &str) -> Self {
+ Self(string_cache::Atom::from(string))
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> std::borrow::Borrow<string_cache::Atom<Set>>
+ for GenericAtomIdent<Set>
+{
+ #[inline]
+ fn borrow(&self) -> &string_cache::Atom<Set> {
+ &self.0
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> GenericAtomIdent<Set> {
+ /// Constructs a new GenericAtomIdent.
+ #[inline]
+ pub fn new(atom: string_cache::Atom<Set>) -> Self {
+ Self(atom)
+ }
+
+ /// Cast an atom ref to an AtomIdent ref.
+ #[inline]
+ pub fn cast<'a>(atom: &'a string_cache::Atom<Set>) -> &'a Self {
+ let ptr = atom as *const _ as *const Self;
+ // safety: repr(transparent)
+ unsafe { &*ptr }
+ }
+}
+
+/// A CSS `<ident>` stored as an `Atom`.
+#[cfg(feature = "gecko")]
+#[repr(transparent)]
+#[derive(
+ Clone, Debug, Default, Deref, Eq, Hash, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem,
+)]
+pub struct AtomIdent(pub Atom);
+
+#[cfg(feature = "gecko")]
+impl cssparser::ToCss for AtomIdent {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl style_traits::ToCss for AtomIdent {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ cssparser::ToCss::to_css(self, dest)
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl PrecomputedHash for AtomIdent {
+ #[inline]
+ fn precomputed_hash(&self) -> u32 {
+ self.0.precomputed_hash()
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl<'a> From<&'a str> for AtomIdent {
+ #[inline]
+ fn from(string: &str) -> Self {
+ Self(Atom::from(string))
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl AtomIdent {
+ /// Constructs a new AtomIdent.
+ #[inline]
+ pub fn new(atom: Atom) -> Self {
+ Self(atom)
+ }
+
+ /// Like `Atom::with` but for `AtomIdent`.
+ pub unsafe fn with<F, R>(ptr: *const crate::gecko_bindings::structs::nsAtom, callback: F) -> R
+ where
+ F: FnOnce(&Self) -> R,
+ {
+ Atom::with(ptr, |atom: &Atom| {
+ // safety: repr(transparent)
+ let atom = atom as *const Atom as *const AtomIdent;
+ callback(&*atom)
+ })
+ }
+
+ /// Cast an atom ref to an AtomIdent ref.
+ #[inline]
+ pub fn cast<'a>(atom: &'a Atom) -> &'a Self {
+ let ptr = atom as *const _ as *const Self;
+ // safety: repr(transparent)
+ unsafe { &*ptr }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl std::borrow::Borrow<crate::gecko_string_cache::WeakAtom> for AtomIdent {
+ #[inline]
+ fn borrow(&self) -> &crate::gecko_string_cache::WeakAtom {
+ self.0.borrow()
+ }
+}
+
+/// Serialize a value into percentage.
+pub fn serialize_percentage<W>(value: CSSFloat, dest: &mut CssWriter<W>) -> fmt::Result
+where
+ W: Write,
+{
+ serialize_specified_dimension(value * 100., "%", /* was_calc = */ false, dest)
+}
+
+/// Serialize a value into normalized (no NaN/inf serialization) percentage.
+pub fn serialize_normalized_percentage<W>(value: CSSFloat, dest: &mut CssWriter<W>) -> fmt::Result
+where
+ W: Write,
+{
+ (value * 100.).to_css(dest)?;
+ dest.write_char('%')
+}
+
+/// Convenience void type to disable some properties and values through types.
+#[cfg_attr(feature = "servo", derive(Deserialize, MallocSizeOf, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+)]
+pub enum Impossible {}
+
+// FIXME(nox): This should be derived but the derive code cannot cope
+// with uninhabited enums.
+impl ComputeSquaredDistance for Impossible {
+ #[inline]
+ fn compute_squared_distance(&self, _other: &Self) -> Result<SquaredDistance, ()> {
+ match *self {}
+ }
+}
+
+impl_trivial_to_shmem!(Impossible);
+
+impl Parse for Impossible {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+/// A struct representing one of two kinds of values.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum Either<A, B> {
+ /// The first value.
+ First(A),
+ /// The second kind of value.
+ Second(B),
+}
+
+impl<A: Debug, B: Debug> Debug for Either<A, B> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ Either::First(ref v) => v.fmt(f),
+ Either::Second(ref v) => v.fmt(f),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-values-4/#custom-idents>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct CustomIdent(pub Atom);
+
+impl CustomIdent {
+ /// Parse a <custom-ident>
+ ///
+ /// TODO(zrhoffman, bug 1844501): Use CustomIdent::parse in more places instead of
+ /// CustomIdent::from_ident.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ invalid: &[&str],
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ CustomIdent::from_ident(location, ident, invalid)
+ }
+
+ /// Parse an already-tokenizer identifier
+ pub fn from_ident<'i>(
+ location: SourceLocation,
+ ident: &CowRcStr<'i>,
+ excluding: &[&str],
+ ) -> Result<Self, ParseError<'i>> {
+ if !Self::is_valid(ident, excluding) {
+ return Err(
+ location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))
+ );
+ }
+ if excluding.iter().any(|s| ident.eq_ignore_ascii_case(s)) {
+ Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(CustomIdent(Atom::from(ident.as_ref())))
+ }
+ }
+
+ fn is_valid(ident: &str, excluding: &[&str]) -> bool {
+ use crate::properties::CSSWideKeyword;
+ // https://drafts.csswg.org/css-values-4/#custom-idents:
+ //
+ // The CSS-wide keywords are not valid <custom-ident>s. The default
+ // keyword is reserved and is also not a valid <custom-ident>.
+ if CSSWideKeyword::from_ident(ident).is_ok() || ident.eq_ignore_ascii_case("default") {
+ return false;
+ }
+
+ // https://drafts.csswg.org/css-values-4/#custom-idents:
+ //
+ // Excluded keywords are excluded in all ASCII case permutations.
+ !excluding.iter().any(|s| ident.eq_ignore_ascii_case(s))
+ }
+}
+
+impl ToCss for CustomIdent {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+/// <https://www.w3.org/TR/css-values-4/#dashed-idents>
+/// This is simply an Atom, but will only parse if the identifier starts with "--".
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct DashedIdent(pub Atom);
+
+impl Parse for DashedIdent {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ if ident.starts_with("--") {
+ Ok(Self(Atom::from(ident.as_ref())))
+ } else {
+ Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())))
+ }
+ }
+}
+
+impl ToCss for DashedIdent {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+/// The <timeline-name> or <keyframes-name>.
+/// The definition of these two names are the same, so we use the same type for them.
+///
+/// <https://drafts.csswg.org/css-animations-2/#typedef-timeline-name>
+/// <https://drafts.csswg.org/css-animations/#typedef-keyframes-name>
+///
+/// We use a single atom for these. Empty atom represents `none` animation.
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Hash,
+ PartialEq,
+ MallocSizeOf,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct TimelineOrKeyframesName(Atom);
+
+impl TimelineOrKeyframesName {
+ /// <https://drafts.csswg.org/css-animations/#dom-csskeyframesrule-name>
+ pub fn from_ident(value: &str) -> Self {
+ Self(Atom::from(value))
+ }
+
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ Self(atom!(""))
+ }
+
+ /// Returns whether this is the special `none` value.
+ pub fn is_none(&self) -> bool {
+ self.0 == atom!("")
+ }
+
+ /// Create a new TimelineOrKeyframesName from Atom.
+ #[cfg(feature = "gecko")]
+ pub fn from_atom(atom: Atom) -> Self {
+ Self(atom)
+ }
+
+ /// The name as an Atom
+ pub fn as_atom(&self) -> &Atom {
+ &self.0
+ }
+
+ fn parse<'i, 't>(input: &mut Parser<'i, 't>, invalid: &[&str]) -> Result<Self, ParseError<'i>> {
+ debug_assert!(invalid.contains(&"none"));
+ let location = input.current_source_location();
+ Ok(match *input.next()? {
+ Token::Ident(ref s) => Self(CustomIdent::from_ident(location, s, invalid)?.0),
+ Token::QuotedString(ref s) => Self(Atom::from(s.as_ref())),
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ })
+ }
+
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, invalid: &[&str]) -> fmt::Result
+ where
+ W: Write,
+ {
+ debug_assert!(invalid.contains(&"none"));
+
+ if self.0 == atom!("") {
+ return dest.write_str("none");
+ }
+
+ self.0.with_str(|s| {
+ if CustomIdent::is_valid(s, invalid) {
+ serialize_identifier(s, dest)
+ } else {
+ s.to_css(dest)
+ }
+ })
+ }
+}
+
+impl Eq for TimelineOrKeyframesName {}
+
+/// The typedef of <timeline-name>.
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Deref,
+ Hash,
+ Eq,
+ PartialEq,
+ MallocSizeOf,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct TimelineName(TimelineOrKeyframesName);
+
+impl TimelineName {
+ /// Create a new TimelineName from Atom.
+ #[cfg(feature = "gecko")]
+ pub fn from_atom(atom: Atom) -> Self {
+ Self(TimelineOrKeyframesName::from_atom(atom))
+ }
+
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ Self(TimelineOrKeyframesName::none())
+ }
+}
+
+impl Parse for TimelineName {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self(TimelineOrKeyframesName::parse(
+ input,
+ &["none", "auto"],
+ )?))
+ }
+}
+
+impl ToCss for TimelineName {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest, &["none", "auto"])
+ }
+}
+
+/// The typedef of <keyframes-name>.
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Deref,
+ Hash,
+ Eq,
+ PartialEq,
+ MallocSizeOf,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct KeyframesName(TimelineOrKeyframesName);
+
+impl KeyframesName {
+ /// Create a new KeyframesName from Atom.
+ #[cfg(feature = "gecko")]
+ pub fn from_atom(atom: Atom) -> Self {
+ Self(TimelineOrKeyframesName::from_atom(atom))
+ }
+
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ Self(TimelineOrKeyframesName::none())
+ }
+}
+
+impl Parse for KeyframesName {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self(TimelineOrKeyframesName::parse(input, &["none"])?))
+ }
+}
+
+impl ToCss for KeyframesName {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest, &["none"])
+ }
+}
diff --git a/servo/components/style/values/resolved/color.rs b/servo/components/style/values/resolved/color.rs
new file mode 100644
index 0000000000..79dfd8685f
--- /dev/null
+++ b/servo/components/style/values/resolved/color.rs
@@ -0,0 +1,48 @@
+/* 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/. */
+
+//! Resolved color values.
+
+use super::{Context, ToResolvedValue};
+
+use crate::color::AbsoluteColor;
+use crate::values::computed::color as computed;
+use crate::values::generics::color as generics;
+
+impl ToResolvedValue for computed::Color {
+ // A resolved color value is an rgba color, with currentcolor resolved.
+ type ResolvedValue = AbsoluteColor;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ context.style.resolve_color(self)
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ generics::Color::Absolute(resolved)
+ }
+}
+
+impl ToResolvedValue for computed::CaretColor {
+ // A resolved caret-color value is an rgba color, with auto resolving to
+ // currentcolor.
+ type ResolvedValue = AbsoluteColor;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ let color = match self.0 {
+ generics::ColorOrAuto::Color(color) => color,
+ generics::ColorOrAuto::Auto => generics::Color::currentcolor(),
+ };
+ color.to_resolved_value(context)
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ generics::CaretColor(generics::ColorOrAuto::Color(
+ computed::Color::from_resolved_value(resolved),
+ ))
+ }
+}
diff --git a/servo/components/style/values/resolved/counters.rs b/servo/components/style/values/resolved/counters.rs
new file mode 100644
index 0000000000..c1332449ad
--- /dev/null
+++ b/servo/components/style/values/resolved/counters.rs
@@ -0,0 +1,51 @@
+/* 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/. */
+
+//! Resolved values for counter properties
+
+use super::{Context, ToResolvedValue};
+use crate::values::computed;
+
+/// https://drafts.csswg.org/css-content/#content-property
+///
+/// We implement this at resolved value time because otherwise it causes us to
+/// allocate a bunch of useless initial structs for ::before / ::after, which is
+/// a bit unfortunate.
+///
+/// Though these should be temporary, mostly, so if this causes complexity in
+/// other places, it should be fine to move to `StyleAdjuster`.
+///
+/// See https://github.com/w3c/csswg-drafts/issues/4632 for where some related
+/// issues are being discussed.
+impl ToResolvedValue for computed::Content {
+ type ResolvedValue = Self;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self {
+ let (is_pseudo, is_before_or_after, is_marker) = match context.style.pseudo() {
+ Some(ref pseudo) => (true, pseudo.is_before_or_after(), pseudo.is_marker()),
+ None => (false, false, false),
+ };
+ match self {
+ Self::Normal if is_before_or_after => Self::None,
+ // For now, make `content: none` compute to `normal` for pseudos
+ // other than ::before, ::after and ::marker, as we don't respect it.
+ // https://github.com/w3c/csswg-drafts/issues/6124
+ // Ditto for non-pseudo elements if the pref is disabled.
+ Self::None
+ if (is_pseudo && !is_before_or_after && !is_marker) ||
+ (!is_pseudo &&
+ !static_prefs::pref!("layout.css.element-content-none.enabled")) =>
+ {
+ Self::Normal
+ },
+ other => other,
+ }
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self) -> Self {
+ resolved
+ }
+}
diff --git a/servo/components/style/values/resolved/mod.rs b/servo/components/style/values/resolved/mod.rs
new file mode 100644
index 0000000000..675f3cca68
--- /dev/null
+++ b/servo/components/style/values/resolved/mod.rs
@@ -0,0 +1,275 @@
+/* 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/. */
+
+//! Resolved values. These are almost always computed values, but in some cases
+//! there are used values.
+
+use crate::media_queries::Device;
+use crate::properties::ComputedValues;
+use crate::ArcSlice;
+use servo_arc::Arc;
+use smallvec::SmallVec;
+
+mod color;
+mod counters;
+
+use crate::values::computed;
+
+/// Element-specific information needed to resolve property values.
+pub struct ResolvedElementInfo<'a> {
+ /// Element we're resolving line-height against.
+ #[cfg(feature = "gecko")]
+ pub element: crate::gecko::wrapper::GeckoElement<'a>,
+}
+
+/// Information needed to resolve a given value.
+pub struct Context<'a> {
+ /// The style we're resolving for. This is useful to resolve currentColor.
+ pub style: &'a ComputedValues,
+ /// The device / document we're resolving style for. Useful to do font metrics stuff needed for
+ /// line-height.
+ pub device: &'a Device,
+ /// The element-specific information to resolve the value.
+ pub element_info: ResolvedElementInfo<'a>,
+}
+
+/// A trait to represent the conversion between resolved and resolved values.
+///
+/// This trait is derivable with `#[derive(ToResolvedValue)]`.
+///
+/// The deriving code assumes that if the type isn't generic, then the trait can
+/// be implemented as simple move. This means that a manual implementation with
+/// `ResolvedValue = Self` is bogus if it returns anything else than a clone.
+pub trait ToResolvedValue {
+ /// The resolved value type we're going to be converted to.
+ type ResolvedValue;
+
+ /// Convert a resolved value to a resolved value.
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue;
+
+ /// Convert a resolved value to resolved value form.
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self;
+}
+
+macro_rules! trivial_to_resolved_value {
+ ($ty:ty) => {
+ impl $crate::values::resolved::ToResolvedValue for $ty {
+ type ResolvedValue = Self;
+
+ #[inline]
+ fn to_resolved_value(self, _: &Context) -> Self {
+ self
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ resolved
+ }
+ }
+ };
+}
+
+trivial_to_resolved_value!(());
+trivial_to_resolved_value!(bool);
+trivial_to_resolved_value!(f32);
+trivial_to_resolved_value!(u8);
+trivial_to_resolved_value!(i8);
+trivial_to_resolved_value!(u16);
+trivial_to_resolved_value!(i16);
+trivial_to_resolved_value!(u32);
+trivial_to_resolved_value!(i32);
+trivial_to_resolved_value!(usize);
+trivial_to_resolved_value!(String);
+trivial_to_resolved_value!(Box<str>);
+trivial_to_resolved_value!(crate::OwnedStr);
+trivial_to_resolved_value!(crate::color::AbsoluteColor);
+trivial_to_resolved_value!(crate::values::generics::color::ColorMixFlags);
+trivial_to_resolved_value!(crate::Atom);
+trivial_to_resolved_value!(crate::values::AtomIdent);
+trivial_to_resolved_value!(app_units::Au);
+trivial_to_resolved_value!(computed::url::ComputedUrl);
+#[cfg(feature = "gecko")]
+trivial_to_resolved_value!(computed::url::ComputedImageUrl);
+#[cfg(feature = "servo")]
+trivial_to_resolved_value!(crate::Namespace);
+#[cfg(feature = "servo")]
+trivial_to_resolved_value!(crate::Prefix);
+trivial_to_resolved_value!(computed::LengthPercentage);
+trivial_to_resolved_value!(style_traits::values::specified::AllowedNumericType);
+trivial_to_resolved_value!(computed::TimingFunction);
+
+impl<A, B> ToResolvedValue for (A, B)
+where
+ A: ToResolvedValue,
+ B: ToResolvedValue,
+{
+ type ResolvedValue = (
+ <A as ToResolvedValue>::ResolvedValue,
+ <B as ToResolvedValue>::ResolvedValue,
+ );
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ (
+ self.0.to_resolved_value(context),
+ self.1.to_resolved_value(context),
+ )
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ (
+ A::from_resolved_value(resolved.0),
+ B::from_resolved_value(resolved.1),
+ )
+ }
+}
+
+impl<T> ToResolvedValue for Option<T>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = Option<<T as ToResolvedValue>::ResolvedValue>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ self.map(|item| item.to_resolved_value(context))
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ resolved.map(T::from_resolved_value)
+ }
+}
+
+impl<T> ToResolvedValue for SmallVec<[T; 1]>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = SmallVec<[<T as ToResolvedValue>::ResolvedValue; 1]>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ self.into_iter()
+ .map(|item| item.to_resolved_value(context))
+ .collect()
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ resolved.into_iter().map(T::from_resolved_value).collect()
+ }
+}
+
+impl<T> ToResolvedValue for Vec<T>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = Vec<<T as ToResolvedValue>::ResolvedValue>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ self.into_iter()
+ .map(|item| item.to_resolved_value(context))
+ .collect()
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ resolved.into_iter().map(T::from_resolved_value).collect()
+ }
+}
+
+impl<T> ToResolvedValue for Box<T>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = Box<<T as ToResolvedValue>::ResolvedValue>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ Box::new(T::to_resolved_value(*self, context))
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ Box::new(T::from_resolved_value(*resolved))
+ }
+}
+
+impl<T> ToResolvedValue for Box<[T]>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = Box<[<T as ToResolvedValue>::ResolvedValue]>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ Vec::from(self)
+ .to_resolved_value(context)
+ .into_boxed_slice()
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ Vec::from_resolved_value(Vec::from(resolved)).into_boxed_slice()
+ }
+}
+
+impl<T> ToResolvedValue for crate::OwnedSlice<T>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = crate::OwnedSlice<<T as ToResolvedValue>::ResolvedValue>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ self.into_box().to_resolved_value(context).into()
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ Self::from(Box::from_resolved_value(resolved.into_box()))
+ }
+}
+
+// NOTE(emilio): This is implementable more generically, but it's unlikely what
+// you want there, as it forces you to have an extra allocation.
+//
+// We could do that if needed, ideally with specialization for the case where
+// ResolvedValue = T. But we don't need it for now.
+impl<T> ToResolvedValue for Arc<T>
+where
+ T: ToResolvedValue<ResolvedValue = T>,
+{
+ type ResolvedValue = Self;
+
+ #[inline]
+ fn to_resolved_value(self, _: &Context) -> Self {
+ self
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self) -> Self {
+ resolved
+ }
+}
+
+// Same caveat as above applies.
+impl<T> ToResolvedValue for ArcSlice<T>
+where
+ T: ToResolvedValue<ResolvedValue = T>,
+{
+ type ResolvedValue = Self;
+
+ #[inline]
+ fn to_resolved_value(self, _: &Context) -> Self {
+ self
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self) -> Self {
+ resolved
+ }
+}
diff --git a/servo/components/style/values/specified/align.rs b/servo/components/style/values/specified/align.rs
new file mode 100644
index 0000000000..60eca4556b
--- /dev/null
+++ b/servo/components/style/values/specified/align.rs
@@ -0,0 +1,820 @@
+/* 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/. */
+
+//! Values for CSS Box Alignment properties
+//!
+//! https://drafts.csswg.org/css-align/
+
+use crate::parser::{Parse, ParserContext};
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, ToCss};
+
+/// Constants shared by multiple CSS Box Alignment properties
+#[derive(
+ Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(C)]
+pub struct AlignFlags(u8);
+bitflags! {
+ impl AlignFlags: u8 {
+ // Enumeration stored in the lower 5 bits:
+ /// {align,justify}-{content,items,self}: 'auto'
+ const AUTO = 0;
+ /// 'normal'
+ const NORMAL = 1;
+ /// 'start'
+ const START = 2;
+ /// 'end'
+ const END = 3;
+ /// 'flex-start'
+ const FLEX_START = 4;
+ /// 'flex-end'
+ const FLEX_END = 5;
+ /// 'center'
+ const CENTER = 6;
+ /// 'left'
+ const LEFT = 7;
+ /// 'right'
+ const RIGHT = 8;
+ /// 'baseline'
+ const BASELINE = 9;
+ /// 'last-baseline'
+ const LAST_BASELINE = 10;
+ /// 'stretch'
+ const STRETCH = 11;
+ /// 'self-start'
+ const SELF_START = 12;
+ /// 'self-end'
+ const SELF_END = 13;
+ /// 'space-between'
+ const SPACE_BETWEEN = 14;
+ /// 'space-around'
+ const SPACE_AROUND = 15;
+ /// 'space-evenly'
+ const SPACE_EVENLY = 16;
+
+ // Additional flags stored in the upper bits:
+ /// 'legacy' (mutually exclusive w. SAFE & UNSAFE)
+ const LEGACY = 1 << 5;
+ /// 'safe'
+ const SAFE = 1 << 6;
+ /// 'unsafe' (mutually exclusive w. SAFE)
+ const UNSAFE = 1 << 7;
+
+ /// Mask for the additional flags above.
+ const FLAG_BITS = 0b11100000;
+ }
+}
+
+impl AlignFlags {
+ /// Returns the enumeration value stored in the lower 5 bits.
+ #[inline]
+ fn value(&self) -> Self {
+ *self & !AlignFlags::FLAG_BITS
+ }
+}
+
+impl ToCss for AlignFlags {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let extra_flags = *self & AlignFlags::FLAG_BITS;
+ let value = self.value();
+
+ match extra_flags {
+ AlignFlags::LEGACY => {
+ dest.write_str("legacy")?;
+ if value.is_empty() {
+ return Ok(());
+ }
+ dest.write_char(' ')?;
+ },
+ AlignFlags::SAFE => dest.write_str("safe ")?,
+ AlignFlags::UNSAFE => dest.write_str("unsafe ")?,
+ _ => {
+ debug_assert_eq!(extra_flags, AlignFlags::empty());
+ },
+ }
+
+ dest.write_str(match value {
+ AlignFlags::AUTO => "auto",
+ AlignFlags::NORMAL => "normal",
+ AlignFlags::START => "start",
+ AlignFlags::END => "end",
+ AlignFlags::FLEX_START => "flex-start",
+ AlignFlags::FLEX_END => "flex-end",
+ AlignFlags::CENTER => "center",
+ AlignFlags::LEFT => "left",
+ AlignFlags::RIGHT => "right",
+ AlignFlags::BASELINE => "baseline",
+ AlignFlags::LAST_BASELINE => "last baseline",
+ AlignFlags::STRETCH => "stretch",
+ AlignFlags::SELF_START => "self-start",
+ AlignFlags::SELF_END => "self-end",
+ AlignFlags::SPACE_BETWEEN => "space-between",
+ AlignFlags::SPACE_AROUND => "space-around",
+ AlignFlags::SPACE_EVENLY => "space-evenly",
+ _ => unreachable!(),
+ })
+ }
+}
+
+/// An axis direction, either inline (for the `justify` properties) or block,
+/// (for the `align` properties).
+#[derive(Clone, Copy, PartialEq)]
+pub enum AxisDirection {
+ /// Block direction.
+ Block,
+ /// Inline direction.
+ Inline,
+}
+
+/// Shared value for the `align-content` and `justify-content` properties.
+///
+/// <https://drafts.csswg.org/css-align/#content-distribution>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+pub struct ContentDistribution {
+ primary: AlignFlags,
+ // FIXME(https://github.com/w3c/csswg-drafts/issues/1002): This will need to
+ // accept fallback alignment, eventually.
+}
+
+impl ContentDistribution {
+ /// The initial value 'normal'
+ #[inline]
+ pub fn normal() -> Self {
+ Self::new(AlignFlags::NORMAL)
+ }
+
+ /// `start`
+ #[inline]
+ pub fn start() -> Self {
+ Self::new(AlignFlags::START)
+ }
+
+ /// The initial value 'normal'
+ #[inline]
+ pub fn new(primary: AlignFlags) -> Self {
+ Self { primary }
+ }
+
+ /// Returns whether this value is a <baseline-position>.
+ pub fn is_baseline_position(&self) -> bool {
+ matches!(
+ self.primary.value(),
+ AlignFlags::BASELINE | AlignFlags::LAST_BASELINE
+ )
+ }
+
+ /// The primary alignment
+ #[inline]
+ pub fn primary(self) -> AlignFlags {
+ self.primary
+ }
+
+ /// Parse a value for align-content / justify-content.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ axis: AxisDirection,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update the `list_keywords` function below
+ // when this function is updated.
+
+ // Try to parse normal first
+ if input
+ .try_parse(|i| i.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(ContentDistribution::normal());
+ }
+
+ // Parse <baseline-position>, but only on the block axis.
+ if axis == AxisDirection::Block {
+ if let Ok(value) = input.try_parse(parse_baseline) {
+ return Ok(ContentDistribution::new(value));
+ }
+ }
+
+ // <content-distribution>
+ if let Ok(value) = input.try_parse(parse_content_distribution) {
+ return Ok(ContentDistribution::new(value));
+ }
+
+ // <overflow-position>? <content-position>
+ let overflow_position = input
+ .try_parse(parse_overflow_position)
+ .unwrap_or(AlignFlags::empty());
+
+ let content_position = try_match_ident_ignore_ascii_case! { input,
+ "start" => AlignFlags::START,
+ "end" => AlignFlags::END,
+ "flex-start" => AlignFlags::FLEX_START,
+ "flex-end" => AlignFlags::FLEX_END,
+ "center" => AlignFlags::CENTER,
+ "left" if axis == AxisDirection::Inline => AlignFlags::LEFT,
+ "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT,
+ };
+
+ Ok(ContentDistribution::new(
+ content_position | overflow_position,
+ ))
+ }
+
+ fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) {
+ f(&["normal"]);
+ if axis == AxisDirection::Block {
+ list_baseline_keywords(f);
+ }
+ list_content_distribution_keywords(f);
+ list_overflow_position_keywords(f);
+ f(&["start", "end", "flex-start", "flex-end", "center"]);
+ if axis == AxisDirection::Inline {
+ f(&["left", "right"]);
+ }
+ }
+}
+
+/// Value for the `align-content` property.
+///
+/// <https://drafts.csswg.org/css-align/#propdef-align-content>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct AlignContent(pub ContentDistribution);
+
+impl Parse for AlignContent {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+ Ok(AlignContent(ContentDistribution::parse(
+ input,
+ AxisDirection::Block,
+ )?))
+ }
+}
+
+impl SpecifiedValueInfo for AlignContent {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ ContentDistribution::list_keywords(f, AxisDirection::Block);
+ }
+}
+
+/// Value for the `align-tracks` property.
+///
+/// <https://github.com/w3c/csswg-drafts/issues/4650>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+#[css(comma)]
+pub struct AlignTracks(#[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<AlignContent>);
+
+impl Parse for AlignTracks {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let values = input.parse_comma_separated(|input| AlignContent::parse(context, input))?;
+ Ok(AlignTracks(values.into()))
+ }
+}
+
+/// Value for the `justify-content` property.
+///
+/// <https://drafts.csswg.org/css-align/#propdef-justify-content>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct JustifyContent(pub ContentDistribution);
+
+impl Parse for JustifyContent {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+ Ok(JustifyContent(ContentDistribution::parse(
+ input,
+ AxisDirection::Inline,
+ )?))
+ }
+}
+
+impl SpecifiedValueInfo for JustifyContent {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ ContentDistribution::list_keywords(f, AxisDirection::Inline);
+ }
+}
+/// Value for the `justify-tracks` property.
+///
+/// <https://github.com/w3c/csswg-drafts/issues/4650>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+#[css(comma)]
+pub struct JustifyTracks(
+ #[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<JustifyContent>,
+);
+
+impl Parse for JustifyTracks {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let values = input.parse_comma_separated(|input| JustifyContent::parse(context, input))?;
+ Ok(JustifyTracks(values.into()))
+ }
+}
+
+/// <https://drafts.csswg.org/css-align/#self-alignment>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct SelfAlignment(pub AlignFlags);
+
+impl SelfAlignment {
+ /// The initial value 'auto'
+ #[inline]
+ pub fn auto() -> Self {
+ SelfAlignment(AlignFlags::AUTO)
+ }
+
+ /// Returns whether this value is valid for both axis directions.
+ pub fn is_valid_on_both_axes(&self) -> bool {
+ match self.0.value() {
+ // left | right are only allowed on the inline axis.
+ AlignFlags::LEFT | AlignFlags::RIGHT => false,
+
+ _ => true,
+ }
+ }
+
+ /// Parse a self-alignment value on one of the axis.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ axis: AxisDirection,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update the `list_keywords` function below
+ // when this function is updated.
+
+ // <baseline-position>
+ //
+ // It's weird that this accepts <baseline-position>, but not
+ // justify-content...
+ if let Ok(value) = input.try_parse(parse_baseline) {
+ return Ok(SelfAlignment(value));
+ }
+
+ // auto | normal | stretch
+ if let Ok(value) = input.try_parse(parse_auto_normal_stretch) {
+ return Ok(SelfAlignment(value));
+ }
+
+ // <overflow-position>? <self-position>
+ let overflow_position = input
+ .try_parse(parse_overflow_position)
+ .unwrap_or(AlignFlags::empty());
+ let self_position = parse_self_position(input, axis)?;
+ Ok(SelfAlignment(overflow_position | self_position))
+ }
+
+ fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) {
+ list_baseline_keywords(f);
+ list_auto_normal_stretch(f);
+ list_overflow_position_keywords(f);
+ list_self_position_keywords(f, axis);
+ }
+}
+
+/// The specified value of the align-self property.
+///
+/// <https://drafts.csswg.org/css-align/#propdef-align-self>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct AlignSelf(pub SelfAlignment);
+
+impl Parse for AlignSelf {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+ Ok(AlignSelf(SelfAlignment::parse(
+ input,
+ AxisDirection::Block,
+ )?))
+ }
+}
+
+impl SpecifiedValueInfo for AlignSelf {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ SelfAlignment::list_keywords(f, AxisDirection::Block);
+ }
+}
+
+/// The specified value of the justify-self property.
+///
+/// <https://drafts.csswg.org/css-align/#propdef-justify-self>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct JustifySelf(pub SelfAlignment);
+
+impl Parse for JustifySelf {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+ Ok(JustifySelf(SelfAlignment::parse(
+ input,
+ AxisDirection::Inline,
+ )?))
+ }
+}
+
+impl SpecifiedValueInfo for JustifySelf {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ SelfAlignment::list_keywords(f, AxisDirection::Inline);
+ }
+}
+
+/// Value of the `align-items` property
+///
+/// <https://drafts.csswg.org/css-align/#propdef-align-items>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct AlignItems(pub AlignFlags);
+
+impl AlignItems {
+ /// The initial value 'normal'
+ #[inline]
+ pub fn normal() -> Self {
+ AlignItems(AlignFlags::NORMAL)
+ }
+}
+
+impl Parse for AlignItems {
+ // normal | stretch | <baseline-position> |
+ // <overflow-position>? <self-position>
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+
+ // <baseline-position>
+ if let Ok(baseline) = input.try_parse(parse_baseline) {
+ return Ok(AlignItems(baseline));
+ }
+
+ // normal | stretch
+ if let Ok(value) = input.try_parse(parse_normal_stretch) {
+ return Ok(AlignItems(value));
+ }
+ // <overflow-position>? <self-position>
+ let overflow = input
+ .try_parse(parse_overflow_position)
+ .unwrap_or(AlignFlags::empty());
+ let self_position = parse_self_position(input, AxisDirection::Block)?;
+ Ok(AlignItems(self_position | overflow))
+ }
+}
+
+impl SpecifiedValueInfo for AlignItems {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ list_baseline_keywords(f);
+ list_normal_stretch(f);
+ list_overflow_position_keywords(f);
+ list_self_position_keywords(f, AxisDirection::Block);
+ }
+}
+
+/// Value of the `justify-items` property
+///
+/// <https://drafts.csswg.org/css-align/#justify-items-property>
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub struct JustifyItems(pub AlignFlags);
+
+impl JustifyItems {
+ /// The initial value 'legacy'
+ #[inline]
+ pub fn legacy() -> Self {
+ JustifyItems(AlignFlags::LEGACY)
+ }
+
+ /// The value 'normal'
+ #[inline]
+ pub fn normal() -> Self {
+ JustifyItems(AlignFlags::NORMAL)
+ }
+}
+
+impl Parse for JustifyItems {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+
+ // <baseline-position>
+ //
+ // It's weird that this accepts <baseline-position>, but not
+ // justify-content...
+ if let Ok(baseline) = input.try_parse(parse_baseline) {
+ return Ok(JustifyItems(baseline));
+ }
+
+ // normal | stretch
+ if let Ok(value) = input.try_parse(parse_normal_stretch) {
+ return Ok(JustifyItems(value));
+ }
+
+ // legacy | [ legacy && [ left | right | center ] ]
+ if let Ok(value) = input.try_parse(parse_legacy) {
+ return Ok(JustifyItems(value));
+ }
+
+ // <overflow-position>? <self-position>
+ let overflow = input
+ .try_parse(parse_overflow_position)
+ .unwrap_or(AlignFlags::empty());
+ let self_position = parse_self_position(input, AxisDirection::Inline)?;
+ Ok(JustifyItems(overflow | self_position))
+ }
+}
+
+impl SpecifiedValueInfo for JustifyItems {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ list_baseline_keywords(f);
+ list_normal_stretch(f);
+ list_legacy_keywords(f);
+ list_overflow_position_keywords(f);
+ list_self_position_keywords(f, AxisDirection::Inline);
+ }
+}
+
+// auto | normal | stretch
+fn parse_auto_normal_stretch<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_auto_normal_stretch` function
+ // below when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "auto" => Ok(AlignFlags::AUTO),
+ "normal" => Ok(AlignFlags::NORMAL),
+ "stretch" => Ok(AlignFlags::STRETCH),
+ }
+}
+
+fn list_auto_normal_stretch(f: KeywordsCollectFn) {
+ f(&["auto", "normal", "stretch"]);
+}
+
+// normal | stretch
+fn parse_normal_stretch<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_normal_stretch` function below
+ // when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "normal" => Ok(AlignFlags::NORMAL),
+ "stretch" => Ok(AlignFlags::STRETCH),
+ }
+}
+
+fn list_normal_stretch(f: KeywordsCollectFn) {
+ f(&["normal", "stretch"]);
+}
+
+// <baseline-position>
+fn parse_baseline<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_baseline_keywords` function
+ // below when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "baseline" => Ok(AlignFlags::BASELINE),
+ "first" => {
+ input.expect_ident_matching("baseline")?;
+ Ok(AlignFlags::BASELINE)
+ },
+ "last" => {
+ input.expect_ident_matching("baseline")?;
+ Ok(AlignFlags::LAST_BASELINE)
+ },
+ }
+}
+
+fn list_baseline_keywords(f: KeywordsCollectFn) {
+ f(&["baseline", "first baseline", "last baseline"]);
+}
+
+// <content-distribution>
+fn parse_content_distribution<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_content_distribution_keywords`
+ // function below when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "stretch" => Ok(AlignFlags::STRETCH),
+ "space-between" => Ok(AlignFlags::SPACE_BETWEEN),
+ "space-around" => Ok(AlignFlags::SPACE_AROUND),
+ "space-evenly" => Ok(AlignFlags::SPACE_EVENLY),
+ }
+}
+
+fn list_content_distribution_keywords(f: KeywordsCollectFn) {
+ f(&["stretch", "space-between", "space-around", "space-evenly"]);
+}
+
+// <overflow-position>
+fn parse_overflow_position<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_overflow_position_keywords`
+ // function below when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "safe" => Ok(AlignFlags::SAFE),
+ "unsafe" => Ok(AlignFlags::UNSAFE),
+ }
+}
+
+fn list_overflow_position_keywords(f: KeywordsCollectFn) {
+ f(&["safe", "unsafe"]);
+}
+
+// <self-position> | left | right in the inline axis.
+fn parse_self_position<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ axis: AxisDirection,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_self_position_keywords`
+ // function below when this function is updated.
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "start" => AlignFlags::START,
+ "end" => AlignFlags::END,
+ "flex-start" => AlignFlags::FLEX_START,
+ "flex-end" => AlignFlags::FLEX_END,
+ "center" => AlignFlags::CENTER,
+ "self-start" => AlignFlags::SELF_START,
+ "self-end" => AlignFlags::SELF_END,
+ "left" if axis == AxisDirection::Inline => AlignFlags::LEFT,
+ "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT,
+ })
+}
+
+fn list_self_position_keywords(f: KeywordsCollectFn, axis: AxisDirection) {
+ f(&[
+ "start",
+ "end",
+ "flex-start",
+ "flex-end",
+ "center",
+ "self-start",
+ "self-end",
+ ]);
+ if axis == AxisDirection::Inline {
+ f(&["left", "right"]);
+ }
+}
+
+fn parse_left_right_center<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_legacy_keywords` function below
+ // when this function is updated.
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "left" => AlignFlags::LEFT,
+ "right" => AlignFlags::RIGHT,
+ "center" => AlignFlags::CENTER,
+ })
+}
+
+// legacy | [ legacy && [ left | right | center ] ]
+fn parse_legacy<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_legacy_keywords` function below
+ // when this function is updated.
+ let flags = try_match_ident_ignore_ascii_case! { input,
+ "legacy" => {
+ let flags = input.try_parse(parse_left_right_center)
+ .unwrap_or(AlignFlags::empty());
+
+ return Ok(AlignFlags::LEGACY | flags)
+ },
+ "left" => AlignFlags::LEFT,
+ "right" => AlignFlags::RIGHT,
+ "center" => AlignFlags::CENTER,
+ };
+
+ input.expect_ident_matching("legacy")?;
+ Ok(AlignFlags::LEGACY | flags)
+}
+
+fn list_legacy_keywords(f: KeywordsCollectFn) {
+ f(&["legacy", "left", "right", "center"]);
+}
diff --git a/servo/components/style/values/specified/angle.rs b/servo/components/style/values/specified/angle.rs
new file mode 100644
index 0000000000..fb4554eb85
--- /dev/null
+++ b/servo/components/style/values/specified/angle.rs
@@ -0,0 +1,276 @@
+/* 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 angles.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::angle::Angle as ComputedAngle;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::specified::calc::CalcNode;
+use crate::values::CSSFloat;
+use crate::Zero;
+use cssparser::{Parser, Token};
+use std::f32::consts::PI;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss};
+
+/// A specified angle dimension.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToCss, ToShmem)]
+pub enum AngleDimension {
+ /// An angle with degree unit.
+ #[css(dimension)]
+ Deg(CSSFloat),
+ /// An angle with gradian unit.
+ #[css(dimension)]
+ Grad(CSSFloat),
+ /// An angle with radian unit.
+ #[css(dimension)]
+ Rad(CSSFloat),
+ /// An angle with turn unit.
+ #[css(dimension)]
+ Turn(CSSFloat),
+}
+
+impl Zero for AngleDimension {
+ fn zero() -> Self {
+ AngleDimension::Deg(0.)
+ }
+
+ fn is_zero(&self) -> bool {
+ self.unitless_value() == 0.0
+ }
+}
+
+impl AngleDimension {
+ /// Returns the amount of degrees this angle represents.
+ #[inline]
+ fn degrees(&self) -> CSSFloat {
+ const DEG_PER_RAD: f32 = 180.0 / PI;
+ const DEG_PER_TURN: f32 = 360.0;
+ const DEG_PER_GRAD: f32 = 180.0 / 200.0;
+
+ match *self {
+ AngleDimension::Deg(d) => d,
+ AngleDimension::Rad(rad) => rad * DEG_PER_RAD,
+ AngleDimension::Turn(turns) => turns * DEG_PER_TURN,
+ AngleDimension::Grad(gradians) => gradians * DEG_PER_GRAD,
+ }
+ }
+
+ fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ AngleDimension::Deg(v) |
+ AngleDimension::Rad(v) |
+ AngleDimension::Turn(v) |
+ AngleDimension::Grad(v) => v,
+ }
+ }
+
+ fn unit(&self) -> &'static str {
+ match *self {
+ AngleDimension::Deg(_) => "deg",
+ AngleDimension::Rad(_) => "rad",
+ AngleDimension::Turn(_) => "turn",
+ AngleDimension::Grad(_) => "grad",
+ }
+ }
+}
+
+/// A specified Angle value, which is just the angle dimension, plus whether it
+/// was specified as `calc()` or not.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Angle {
+ value: AngleDimension,
+ was_calc: bool,
+}
+
+impl Zero for Angle {
+ fn zero() -> Self {
+ Self {
+ value: Zero::zero(),
+ was_calc: false,
+ }
+ }
+
+ fn is_zero(&self) -> bool {
+ self.value.is_zero()
+ }
+}
+
+impl ToCss for Angle {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ crate::values::serialize_specified_dimension(
+ self.value.unitless_value(),
+ self.value.unit(),
+ self.was_calc,
+ dest,
+ )
+ }
+}
+
+impl ToComputedValue for Angle {
+ type ComputedValue = ComputedAngle;
+
+ #[inline]
+ fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
+ let degrees = self.degrees();
+
+ // NaN and +-infinity should degenerate to 0: https://github.com/w3c/csswg-drafts/issues/6105
+ ComputedAngle::from_degrees(if degrees.is_finite() { degrees } else { 0.0 })
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Angle {
+ value: AngleDimension::Deg(computed.degrees()),
+ was_calc: false,
+ }
+ }
+}
+
+impl Angle {
+ /// Creates an angle with the given value in degrees.
+ #[inline]
+ pub fn from_degrees(value: CSSFloat, was_calc: bool) -> Self {
+ Angle {
+ value: AngleDimension::Deg(value),
+ was_calc,
+ }
+ }
+
+ /// Creates an angle with the given value in radians.
+ #[inline]
+ pub fn from_radians(value: CSSFloat) -> Self {
+ Angle {
+ value: AngleDimension::Rad(value),
+ was_calc: false,
+ }
+ }
+
+ /// Return `0deg`.
+ pub fn zero() -> Self {
+ Self::from_degrees(0.0, false)
+ }
+
+ /// Returns the value of the angle in degrees, mostly for `calc()`.
+ #[inline]
+ pub fn degrees(&self) -> CSSFloat {
+ self.value.degrees()
+ }
+
+ /// Returns the value of the angle in radians.
+ #[inline]
+ pub fn radians(&self) -> CSSFloat {
+ const RAD_PER_DEG: f32 = PI / 180.0;
+ self.value.degrees() * RAD_PER_DEG
+ }
+
+ /// Whether this specified angle came from a `calc()` expression.
+ #[inline]
+ pub fn was_calc(&self) -> bool {
+ self.was_calc
+ }
+
+ /// Returns an `Angle` parsed from a `calc()` expression.
+ pub fn from_calc(degrees: CSSFloat) -> Self {
+ Angle {
+ value: AngleDimension::Deg(degrees),
+ was_calc: true,
+ }
+ }
+
+ /// Returns the unit of the angle.
+ #[inline]
+ pub fn unit(&self) -> &'static str {
+ self.value.unit()
+ }
+}
+
+/// Whether to allow parsing an unitless zero as a valid angle.
+///
+/// This should always be `No`, except for exceptions like:
+///
+/// https://github.com/w3c/fxtf-drafts/issues/228
+///
+/// See also: https://github.com/w3c/csswg-drafts/issues/1162.
+#[allow(missing_docs)]
+pub enum AllowUnitlessZeroAngle {
+ Yes,
+ No,
+}
+
+impl Parse for Angle {
+ /// Parses an angle according to CSS-VALUES § 6.1.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowUnitlessZeroAngle::No)
+ }
+}
+
+impl Angle {
+ /// Parse an `<angle>` value given a value and an unit.
+ pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> Result<Angle, ()> {
+ let value = match_ignore_ascii_case! { unit,
+ "deg" => AngleDimension::Deg(value),
+ "grad" => AngleDimension::Grad(value),
+ "turn" => AngleDimension::Turn(value),
+ "rad" => AngleDimension::Rad(value),
+ _ => return Err(())
+ };
+
+ Ok(Self { value, was_calc })
+ }
+
+ /// Parse an `<angle>` allowing unitless zero to represent a zero angle.
+ ///
+ /// See the comment in `AllowUnitlessZeroAngle` for why.
+ #[inline]
+ pub fn parse_with_unitless<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowUnitlessZeroAngle::Yes)
+ }
+
+ pub(super) fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_unitless_zero: AllowUnitlessZeroAngle,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let t = input.next()?;
+ let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes);
+ match *t {
+ Token::Dimension {
+ value, ref unit, ..
+ } => {
+ match Angle::parse_dimension(value, unit, /* from_calc = */ false) {
+ Ok(angle) => Ok(angle),
+ Err(()) => {
+ let t = t.clone();
+ Err(input.new_unexpected_token_error(t))
+ },
+ }
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse_angle(context, input, function)
+ },
+ Token::Number { value, .. } if value == 0. && allow_unitless_zero => Ok(Angle::zero()),
+ ref t => {
+ let t = t.clone();
+ Err(input.new_unexpected_token_error(t))
+ },
+ }
+ }
+}
+
+impl SpecifiedValueInfo for Angle {}
diff --git a/servo/components/style/values/specified/animation.rs b/servo/components/style/values/specified/animation.rs
new file mode 100644
index 0000000000..e7bbf26fb3
--- /dev/null
+++ b/servo/components/style/values/specified/animation.rs
@@ -0,0 +1,463 @@
+/* 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 types for properties related to animations and transitions.
+
+use crate::parser::{Parse, ParserContext};
+use crate::properties::{NonCustomPropertyId, PropertyId, ShorthandId};
+use crate::values::generics::animation as generics;
+use crate::values::specified::{LengthPercentage, NonNegativeNumber};
+use crate::values::{CustomIdent, KeyframesName, TimelineName};
+use crate::Atom;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{
+ CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss,
+};
+
+/// A given transition property, that is either `All`, a longhand or shorthand
+/// property, or an unsupported or custom property.
+#[derive(
+ Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(u8)]
+pub enum TransitionProperty {
+ /// A non-custom property.
+ NonCustom(NonCustomPropertyId),
+ /// A custom property.
+ Custom(Atom),
+ /// Unrecognized property which could be any non-transitionable, custom property, or
+ /// unknown property.
+ Unsupported(CustomIdent),
+}
+
+impl ToCss for TransitionProperty {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ TransitionProperty::NonCustom(ref id) => id.to_css(dest),
+ TransitionProperty::Custom(ref name) => {
+ dest.write_str("--")?;
+ crate::values::serialize_atom_name(name, dest)
+ },
+ TransitionProperty::Unsupported(ref i) => i.to_css(dest),
+ }
+ }
+}
+
+impl Parse for TransitionProperty {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+
+ let id = match PropertyId::parse_ignoring_rule_type(&ident, context) {
+ Ok(id) => id,
+ Err(..) => {
+ // None is not acceptable as a single transition-property.
+ return Ok(TransitionProperty::Unsupported(CustomIdent::from_ident(
+ location,
+ ident,
+ &["none"],
+ )?));
+ },
+ };
+
+ Ok(match id {
+ PropertyId::NonCustom(id) => TransitionProperty::NonCustom(id.unaliased()),
+ PropertyId::Custom(name) => TransitionProperty::Custom(name),
+ })
+ }
+}
+
+impl SpecifiedValueInfo for TransitionProperty {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ // `transition-property` can actually accept all properties and
+ // arbitrary identifiers, but `all` is a special one we'd like
+ // to list.
+ f(&["all"]);
+ }
+}
+
+impl TransitionProperty {
+ /// Returns the `none` value.
+ #[inline]
+ pub fn none() -> Self {
+ TransitionProperty::Unsupported(CustomIdent(atom!("none")))
+ }
+
+ /// Returns whether we're the `none` value.
+ #[inline]
+ pub fn is_none(&self) -> bool {
+ matches!(*self, TransitionProperty::Unsupported(ref ident) if ident.0 == atom!("none"))
+ }
+
+ /// Returns `all`.
+ #[inline]
+ pub fn all() -> Self {
+ TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(ShorthandId::All))
+ }
+
+ /// Returns true if it is `all`.
+ #[inline]
+ pub fn is_all(&self) -> bool {
+ self == &TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(
+ ShorthandId::All,
+ ))
+ }
+}
+
+/// https://drafts.csswg.org/css-animations/#animation-iteration-count
+#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum AnimationIterationCount {
+ /// A `<number>` value.
+ Number(NonNegativeNumber),
+ /// The `infinite` keyword.
+ Infinite,
+}
+
+impl AnimationIterationCount {
+ /// Returns the value `1.0`.
+ #[inline]
+ pub fn one() -> Self {
+ Self::Number(NonNegativeNumber::new(1.0))
+ }
+}
+
+/// A value for the `animation-name` property.
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "none")]
+#[repr(C)]
+pub struct AnimationName(pub KeyframesName);
+
+impl AnimationName {
+ /// Get the name of the animation as an `Atom`.
+ pub fn as_atom(&self) -> Option<&Atom> {
+ if self.is_none() {
+ return None;
+ }
+ Some(self.0.as_atom())
+ }
+
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ AnimationName(KeyframesName::none())
+ }
+
+ /// Returns whether this is the none value.
+ pub fn is_none(&self) -> bool {
+ self.0.is_none()
+ }
+}
+
+impl Parse for AnimationName {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(name) = input.try_parse(|input| KeyframesName::parse(context, input)) {
+ return Ok(AnimationName(name));
+ }
+
+ input.expect_ident_matching("none")?;
+ Ok(AnimationName(KeyframesName::none()))
+ }
+}
+
+/// https://drafts.csswg.org/css-animations/#propdef-animation-direction
+#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum AnimationDirection {
+ Normal,
+ Reverse,
+ Alternate,
+ AlternateReverse,
+}
+
+/// https://drafts.csswg.org/css-animations/#animation-play-state
+#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum AnimationPlayState {
+ Running,
+ Paused,
+}
+
+/// https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode
+#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum AnimationFillMode {
+ None,
+ Forwards,
+ Backwards,
+ Both,
+}
+
+/// https://drafts.csswg.org/css-animations-2/#animation-composition
+#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum AnimationComposition {
+ Replace,
+ Add,
+ Accumulate,
+}
+
+/// A value for the <Scroller> used in scroll().
+///
+/// https://drafts.csswg.org/scroll-animations-1/rewrite#typedef-scroller
+#[derive(
+ Copy,
+ Clone,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum Scroller {
+ /// The nearest ancestor scroll container. (Default.)
+ Nearest,
+ /// The document viewport as the scroll container.
+ Root,
+ /// Specifies to use the element’s own principal box as the scroll container.
+ #[css(keyword = "self")]
+ SelfElement,
+}
+
+impl Scroller {
+ /// Returns true if it is default.
+ #[inline]
+ fn is_default(&self) -> bool {
+ matches!(*self, Self::Nearest)
+ }
+}
+
+impl Default for Scroller {
+ fn default() -> Self {
+ Self::Nearest
+ }
+}
+
+/// A value for the <Axis> used in scroll(), or a value for {scroll|view}-timeline-axis.
+///
+/// https://drafts.csswg.org/scroll-animations-1/#typedef-axis
+/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis
+/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis
+#[derive(
+ Copy,
+ Clone,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollAxis {
+ /// The block axis of the scroll container. (Default.)
+ Block = 0,
+ /// The inline axis of the scroll container.
+ Inline = 1,
+ /// The vertical block axis of the scroll container.
+ Vertical = 2,
+ /// The horizontal axis of the scroll container.
+ Horizontal = 3,
+}
+
+impl ScrollAxis {
+ /// Returns true if it is default.
+ #[inline]
+ pub fn is_default(&self) -> bool {
+ matches!(*self, Self::Block)
+ }
+}
+
+impl Default for ScrollAxis {
+ fn default() -> Self {
+ Self::Block
+ }
+}
+
+/// The scroll() notation.
+/// https://drafts.csswg.org/scroll-animations-1/#scroll-notation
+#[derive(
+ Copy,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "scroll")]
+#[repr(C)]
+pub struct ScrollFunction {
+ /// The scroll container element whose scroll position drives the progress of the timeline.
+ #[css(skip_if = "Scroller::is_default")]
+ pub scroller: Scroller,
+ /// The axis of scrolling that drives the progress of the timeline.
+ #[css(skip_if = "ScrollAxis::is_default")]
+ pub axis: ScrollAxis,
+}
+
+impl ScrollFunction {
+ /// Parse the inner function arguments of `scroll()`.
+ fn parse_arguments<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ // <scroll()> = scroll( [ <scroller> || <axis> ]? )
+ // https://drafts.csswg.org/scroll-animations-1/#funcdef-scroll
+ let mut scroller = None;
+ let mut axis = None;
+ loop {
+ if scroller.is_none() {
+ scroller = input.try_parse(Scroller::parse).ok();
+ }
+
+ if axis.is_none() {
+ axis = input.try_parse(ScrollAxis::parse).ok();
+ if axis.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ Ok(Self {
+ scroller: scroller.unwrap_or_default(),
+ axis: axis.unwrap_or_default(),
+ })
+ }
+}
+
+impl generics::ViewFunction<LengthPercentage> {
+ /// Parse the inner function arguments of `view()`.
+ fn parse_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // <view()> = view( [ <axis> || <'view-timeline-inset'> ]? )
+ // https://drafts.csswg.org/scroll-animations-1/#funcdef-view
+ let mut axis = None;
+ let mut inset = None;
+ loop {
+ if axis.is_none() {
+ axis = input.try_parse(ScrollAxis::parse).ok();
+ }
+
+ if inset.is_none() {
+ inset = input
+ .try_parse(|i| ViewTimelineInset::parse(context, i))
+ .ok();
+ if inset.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ Ok(Self {
+ inset: inset.unwrap_or_default(),
+ axis: axis.unwrap_or_default(),
+ })
+ }
+}
+
+/// A specified value for the `animation-timeline` property.
+pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>;
+
+impl Parse for AnimationTimeline {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::generics::animation::ViewFunction;
+
+ // <single-animation-timeline> = auto | none | <custom-ident> | <scroll()> | <view()>
+ // https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline
+
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ return Ok(Self::Auto);
+ }
+
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(AnimationTimeline::Timeline(TimelineName::none()));
+ }
+
+ if let Ok(name) = input.try_parse(|i| TimelineName::parse(context, i)) {
+ return Ok(AnimationTimeline::Timeline(name));
+ }
+
+ // Parse possible functions
+ let location = input.current_source_location();
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(move |i| {
+ match_ignore_ascii_case! { &function,
+ "scroll" => ScrollFunction::parse_arguments(i).map(Self::Scroll),
+ "view" => ViewFunction::parse_arguments(context, i).map(Self::View),
+ _ => {
+ Err(location.new_custom_error(
+ StyleParseErrorKind::UnexpectedFunction(function.clone())
+ ))
+ },
+ }
+ })
+ }
+}
+
+/// A value for the scroll-timeline-name or view-timeline-name.
+pub type ScrollTimelineName = AnimationName;
+
+/// A specified value for the `view-timeline-inset` property.
+pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>;
+
+impl Parse for ViewTimelineInset {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::LengthPercentageOrAuto;
+
+ let start = LengthPercentageOrAuto::parse(context, input)?;
+ let end = match input.try_parse(|input| LengthPercentageOrAuto::parse(context, input)) {
+ Ok(end) => end,
+ Err(_) => start.clone(),
+ };
+
+ Ok(Self { start, end })
+ }
+}
diff --git a/servo/components/style/values/specified/background.rs b/servo/components/style/values/specified/background.rs
new file mode 100644
index 0000000000..39a5a85193
--- /dev/null
+++ b/servo/components/style/values/specified/background.rs
@@ -0,0 +1,143 @@
+/* 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 types for CSS values related to backgrounds.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::background::BackgroundSize as GenericBackgroundSize;
+use crate::values::specified::length::{
+ NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto,
+};
+use cssparser::Parser;
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A specified value for the `background-size` property.
+pub type BackgroundSize = GenericBackgroundSize<NonNegativeLengthPercentage>;
+
+impl Parse for BackgroundSize {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(width) = input.try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i))
+ {
+ let height = input
+ .try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i))
+ .unwrap_or(NonNegativeLengthPercentageOrAuto::auto());
+ return Ok(GenericBackgroundSize::ExplicitSize { width, height });
+ }
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "cover" => GenericBackgroundSize::Cover,
+ "contain" => GenericBackgroundSize::Contain,
+ })
+ }
+}
+
+/// One of the keywords for `background-repeat`.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[value_info(other_values = "repeat-x,repeat-y")]
+pub enum BackgroundRepeatKeyword {
+ Repeat,
+ Space,
+ Round,
+ NoRepeat,
+}
+
+/// The value of the `background-repeat` property, with `repeat-x` / `repeat-y`
+/// represented as the combination of `no-repeat` and `repeat` in the opposite
+/// axes.
+///
+/// https://drafts.csswg.org/css-backgrounds/#the-background-repeat
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct BackgroundRepeat(pub BackgroundRepeatKeyword, pub BackgroundRepeatKeyword);
+
+impl BackgroundRepeat {
+ /// Returns the `repeat repeat` value.
+ pub fn repeat() -> Self {
+ BackgroundRepeat(
+ BackgroundRepeatKeyword::Repeat,
+ BackgroundRepeatKeyword::Repeat,
+ )
+ }
+}
+
+impl ToCss for BackgroundRepeat {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match (self.0, self.1) {
+ (BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat) => {
+ dest.write_str("repeat-x")
+ },
+ (BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat) => {
+ dest.write_str("repeat-y")
+ },
+ (horizontal, vertical) => {
+ horizontal.to_css(dest)?;
+ if horizontal != vertical {
+ dest.write_char(' ')?;
+ vertical.to_css(dest)?;
+ }
+ Ok(())
+ },
+ }
+ }
+}
+
+impl Parse for BackgroundRepeat {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let ident = input.expect_ident_cloned()?;
+
+ match_ignore_ascii_case! { &ident,
+ "repeat-x" => {
+ return Ok(BackgroundRepeat(BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat));
+ },
+ "repeat-y" => {
+ return Ok(BackgroundRepeat(BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat));
+ },
+ _ => {},
+ }
+
+ let horizontal = match BackgroundRepeatKeyword::from_ident(&ident) {
+ Ok(h) => h,
+ Err(()) => {
+ return Err(
+ input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))
+ );
+ },
+ };
+
+ let vertical = input.try_parse(BackgroundRepeatKeyword::parse).ok();
+ Ok(BackgroundRepeat(horizontal, vertical.unwrap_or(horizontal)))
+ }
+}
diff --git a/servo/components/style/values/specified/basic_shape.rs b/servo/components/style/values/specified/basic_shape.rs
new file mode 100644
index 0000000000..526296b735
--- /dev/null
+++ b/servo/components/style/values/specified/basic_shape.rs
@@ -0,0 +1,719 @@
+/* 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/. */
+
+//! CSS handling for the specified value of
+//! [`basic-shape`][basic-shape]s
+//!
+//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::basic_shape as generic;
+use crate::values::generics::basic_shape::{Path, PolygonCoord};
+use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto};
+use crate::values::generics::rect::Rect;
+use crate::values::specified::border::BorderRadius;
+use crate::values::specified::image::Image;
+use crate::values::specified::length::LengthPercentageOrAuto;
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData};
+use crate::Zero;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// A specified alias for FillRule.
+pub use crate::values::generics::basic_shape::FillRule;
+
+/// A specified `clip-path` value.
+pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>;
+
+/// A specified `shape-outside` value.
+pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;
+
+/// A specified value for `at <position>` in circle() and ellipse().
+// Note: its computed value is the same as computed::position::Position. We just want to always use
+// LengthPercentage as the type of its components, for basic shapes.
+pub type ShapePosition = GenericPosition<LengthPercentage, LengthPercentage>;
+
+/// A specified basic shape.
+pub type BasicShape = generic::GenericBasicShape<
+ ShapePosition,
+ LengthPercentage,
+ NonNegativeLengthPercentage,
+ BasicShapeRect,
+>;
+
+/// The specified value of `inset()`.
+pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>;
+
+/// A specified circle.
+pub type Circle = generic::Circle<ShapePosition, NonNegativeLengthPercentage>;
+
+/// A specified ellipse.
+pub type Ellipse = generic::Ellipse<ShapePosition, NonNegativeLengthPercentage>;
+
+/// The specified value of `ShapeRadius`.
+pub type ShapeRadius = generic::ShapeRadius<NonNegativeLengthPercentage>;
+
+/// The specified value of `Polygon`.
+pub type Polygon = generic::GenericPolygon<LengthPercentage>;
+
+/// The specified value of `xywh()`.
+/// Defines a rectangle via offsets from the top and left edge of the reference box, and a
+/// specified width and height.
+///
+/// The four <length-percentage>s define, respectively, the inset from the left edge of the
+/// reference box, the inset from the top edge of the reference box, the width of the rectangle,
+/// and the height of the rectangle.
+///
+/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
+pub struct Xywh {
+ /// The left edge of the reference box.
+ pub x: LengthPercentage,
+ /// The top edge of the reference box.
+ pub y: LengthPercentage,
+ /// The specified width.
+ pub width: NonNegativeLengthPercentage,
+ /// The specified height.
+ pub height: NonNegativeLengthPercentage,
+ /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
+ /// using the border-radius shorthand syntax.
+ pub round: BorderRadius,
+}
+
+/// Defines a rectangle via insets from the top and left edges of the reference box.
+///
+/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
+#[repr(C)]
+pub struct ShapeRectFunction {
+ /// The four <length-percentage>s define the position of the top, right, bottom, and left edges
+ /// of a rectangle, respectively, as insets from the top edge of the reference box (for the
+ /// first and third values) or the left edge of the reference box (for the second and fourth
+ /// values).
+ ///
+ /// An auto value makes the edge of the box coincide with the corresponding edge of the
+ /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and
+ /// equivalent to 100% as the second (right) or third (bottom) value.
+ pub rect: Rect<LengthPercentageOrAuto>,
+ /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
+ /// using the border-radius shorthand syntax.
+ pub round: BorderRadius,
+}
+
+/// The specified value of <basic-shape-rect>.
+/// <basic-shape-rect> = <inset()> | <rect()> | <xywh()>
+///
+/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum BasicShapeRect {
+ /// Defines an inset rectangle via insets from each edge of the reference box.
+ Inset(InsetRect),
+ /// Defines a xywh function.
+ #[css(function)]
+ Xywh(Xywh),
+ /// Defines a rect function.
+ #[css(function)]
+ Rect(ShapeRectFunction),
+}
+
+/// For filled shapes, we use fill-rule, and store it for path() and polygon().
+/// For outline shapes, we should ignore fill-rule.
+///
+/// https://github.com/w3c/fxtf-drafts/issues/512
+/// https://github.com/w3c/csswg-drafts/issues/7390
+/// https://github.com/w3c/csswg-drafts/issues/3468
+pub enum ShapeType {
+ /// The CSS property uses filled shapes. The default behavior.
+ Filled,
+ /// The CSS property uses outline shapes. This is especially useful for offset-path.
+ Outline,
+}
+
+bitflags! {
+ /// The flags to represent which basic shapes we would like to support.
+ ///
+ /// Different properties may use different subsets of <basic-shape>:
+ /// e.g.
+ /// clip-path: all basic shapes.
+ /// motion-path: all basic shapes (but ignore fill-rule).
+ /// shape-outside: inset(), circle(), ellipse(), polygon().
+ ///
+ /// Also there are some properties we don't support for now:
+ /// shape-inside: inset(), circle(), ellipse(), polygon().
+ /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon().
+ ///
+ /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now
+ /// we use the bitflags to choose the supported basic shapes for each property at the parse
+ /// time.
+ /// https://github.com/w3c/csswg-drafts/issues/7390
+ #[derive(Clone, Copy)]
+ #[repr(C)]
+ pub struct AllowedBasicShapes: u8 {
+ /// inset().
+ const INSET = 1 << 0;
+ /// xywh().
+ const XYWH = 1 << 1;
+ /// rect().
+ const RECT = 1 << 2;
+ /// circle().
+ const CIRCLE = 1 << 3;
+ /// ellipse().
+ const ELLIPSE = 1 << 4;
+ /// polygon().
+ const POLYGON = 1 << 5;
+ /// path().
+ const PATH = 1 << 6;
+ // TODO: Bug 1823463. Add shape().
+ // const SHAPE = 1 << 7;
+
+ /// All flags.
+ const ALL =
+ Self::INSET.bits() |
+ Self::XYWH.bits() |
+ Self::RECT.bits() |
+ Self::CIRCLE.bits() |
+ Self::ELLIPSE.bits() |
+ Self::POLYGON.bits() |
+ Self::PATH.bits();
+
+ /// For shape-outside.
+ const SHAPE_OUTSIDE =
+ Self::INSET.bits() |
+ Self::CIRCLE.bits() |
+ Self::ELLIPSE.bits() |
+ Self::POLYGON.bits();
+ }
+}
+
+/// A helper for both clip-path and shape-outside parsing of shapes.
+fn parse_shape_or_box<'i, 't, R, ReferenceBox>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R,
+ to_reference_box: impl FnOnce(ReferenceBox) -> R,
+ flags: AllowedBasicShapes,
+) -> Result<R, ParseError<'i>>
+where
+ ReferenceBox: Default + Parse,
+{
+ let mut shape = None;
+ let mut ref_box = None;
+ loop {
+ if shape.is_none() {
+ shape = input
+ .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled))
+ .ok();
+ }
+
+ if ref_box.is_none() {
+ ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok();
+ if ref_box.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ if let Some(shp) = shape {
+ return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default()));
+ }
+
+ match ref_box {
+ Some(r) => Ok(to_reference_box(r)),
+ None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+}
+
+impl Parse for ClipPath {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(ClipPath::None);
+ }
+
+ if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
+ return Ok(ClipPath::Url(url));
+ }
+
+ parse_shape_or_box(
+ context,
+ input,
+ ClipPath::Shape,
+ ClipPath::Box,
+ AllowedBasicShapes::ALL,
+ )
+ }
+}
+
+impl Parse for ShapeOutside {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Need to parse this here so that `Image::parse_with_cors_anonymous`
+ // doesn't parse it.
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(ShapeOutside::None);
+ }
+
+ if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) {
+ debug_assert_ne!(image, Image::None);
+ return Ok(ShapeOutside::Image(image));
+ }
+
+ parse_shape_or_box(
+ context,
+ input,
+ ShapeOutside::Shape,
+ ShapeOutside::Box,
+ AllowedBasicShapes::SHAPE_OUTSIDE,
+ )
+ }
+}
+
+impl BasicShape {
+ /// Parse with some parameters.
+ /// 1. The supported <basic-shape>.
+ /// 2. The type of shapes. Should we ignore fill-rule?
+ /// 3. The default value of `at <position>`.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ flags: AllowedBasicShapes,
+ shape_type: ShapeType,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(move |i| {
+ match_ignore_ascii_case! { &function,
+ "inset" if flags.contains(AllowedBasicShapes::INSET) => {
+ InsetRect::parse_function_arguments(context, i)
+ .map(BasicShapeRect::Inset)
+ .map(BasicShape::Rect)
+ },
+ "xywh"
+ if flags.contains(AllowedBasicShapes::XYWH)
+ && static_prefs::pref!("layout.css.basic-shape-xywh.enabled") =>
+ {
+ Xywh::parse_function_arguments(context, i)
+ .map(BasicShapeRect::Xywh)
+ .map(BasicShape::Rect)
+ },
+ "rect"
+ if flags.contains(AllowedBasicShapes::RECT)
+ && static_prefs::pref!("layout.css.basic-shape-rect.enabled") =>
+ {
+ ShapeRectFunction::parse_function_arguments(context, i)
+ .map(BasicShapeRect::Rect)
+ .map(BasicShape::Rect)
+ },
+ "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => {
+ Circle::parse_function_arguments(context, i)
+ .map(BasicShape::Circle)
+ },
+ "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => {
+ Ellipse::parse_function_arguments(context, i)
+ .map(BasicShape::Ellipse)
+ },
+ "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => {
+ Polygon::parse_function_arguments(context, i, shape_type)
+ .map(BasicShape::Polygon)
+ },
+ "path" if flags.contains(AllowedBasicShapes::PATH) => {
+ Path::parse_function_arguments(i, shape_type).map(BasicShape::Path)
+ },
+ _ => Err(location
+ .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
+ }
+ })
+ }
+}
+
+impl Parse for InsetRect {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("inset")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
+ }
+}
+
+fn parse_round<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<BorderRadius, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("round"))
+ .is_ok()
+ {
+ return BorderRadius::parse(context, input);
+ }
+
+ Ok(BorderRadius::zero())
+}
+
+impl InsetRect {
+ /// Parse the inner function arguments of `inset()`
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let rect = Rect::parse_with(context, input, LengthPercentage::parse)?;
+ let round = parse_round(context, input)?;
+ Ok(generic::InsetRect { rect, round })
+ }
+}
+
+impl ToCss for ShapePosition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.horizontal.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.vertical.to_css(dest)
+ }
+}
+
+fn parse_at_position<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<GenericPositionOrAuto<ShapePosition>, ParseError<'i>> {
+ use crate::values::specified::position::{Position, Side};
+ use crate::values::specified::{AllowedNumericType, Percentage, PositionComponent};
+
+ fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage {
+ // Convert the value when parsing, to make sure we serialize it properly for both
+ // specified and computed values.
+ // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization
+ match c {
+ // Since <position> keywords stand in for percentages, keywords without an offset
+ // turn into percentages.
+ PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)),
+ PositionComponent::Side(keyword, None) => {
+ Percentage::new(if keyword.is_start() { 0. } else { 1. }).into()
+ },
+ // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of
+ // "avoiding calc() expressions where possible" and "avoiding calc()
+ // transformations" will be removed from the spec, and we should follow the
+ // css-values-4 for position, i.e. we make it as length-percentage always.
+ // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization.
+ // https://drafts.csswg.org/css-values-4/#typedef-position
+ PositionComponent::Side(keyword, Some(length)) => {
+ if keyword.is_start() {
+ length
+ } else {
+ length.hundred_percent_minus(AllowedNumericType::All)
+ }
+ },
+ PositionComponent::Length(length) => length,
+ }
+ }
+
+ if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
+ Position::parse(context, input).map(|pos| {
+ GenericPositionOrAuto::Position(ShapePosition::new(
+ convert_to_length_percentage(pos.horizontal),
+ convert_to_length_percentage(pos.vertical),
+ ))
+ })
+ } else {
+ // `at <position>` is omitted.
+ Ok(GenericPositionOrAuto::Auto)
+ }
+}
+
+impl Parse for Circle {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("circle")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
+ }
+}
+
+impl Circle {
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let radius = input
+ .try_parse(|i| ShapeRadius::parse(context, i))
+ .unwrap_or_default();
+ let position = parse_at_position(context, input)?;
+
+ Ok(generic::Circle { radius, position })
+ }
+}
+
+impl Parse for Ellipse {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("ellipse")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
+ }
+}
+
+impl Ellipse {
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let (semiaxis_x, semiaxis_y) = input
+ .try_parse(|i| -> Result<_, ParseError> {
+ Ok((
+ ShapeRadius::parse(context, i)?,
+ ShapeRadius::parse(context, i)?,
+ ))
+ })
+ .unwrap_or_default();
+ let position = parse_at_position(context, input)?;
+
+ Ok(generic::Ellipse {
+ semiaxis_x,
+ semiaxis_y,
+ position,
+ })
+ }
+}
+
+fn parse_fill_rule<'i, 't>(input: &mut Parser<'i, 't>, shape_type: ShapeType) -> FillRule {
+ match shape_type {
+ // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default
+ // value.
+ // [1] https://github.com/w3c/csswg-drafts/issues/3468
+ // [2] https://github.com/w3c/csswg-drafts/issues/7390
+ //
+ // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g.
+ // offset-path, which means we don't parse it when setting `ShapeType::Outline`.
+ // This should be web compatible because the shipped "offset-path:path()" doesn't have
+ // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the
+ // preference.
+ // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321
+ // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929
+ ShapeType::Outline => Default::default(),
+ ShapeType::Filled => input
+ .try_parse(|i| -> Result<_, ParseError> {
+ let fill = FillRule::parse(i)?;
+ i.expect_comma()?;
+ Ok(fill)
+ })
+ .unwrap_or_default(),
+ }
+}
+
+impl Parse for Polygon {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("polygon")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled))
+ }
+}
+
+impl Polygon {
+ /// Parse the inner arguments of a `polygon` function.
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ shape_type: ShapeType,
+ ) -> Result<Self, ParseError<'i>> {
+ let fill = parse_fill_rule(input, shape_type);
+ let coordinates = input
+ .parse_comma_separated(|i| {
+ Ok(PolygonCoord(
+ LengthPercentage::parse(context, i)?,
+ LengthPercentage::parse(context, i)?,
+ ))
+ })?
+ .into();
+
+ Ok(Polygon { fill, coordinates })
+ }
+}
+
+impl Path {
+ /// Parse the inner arguments of a `path` function.
+ fn parse_function_arguments<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ shape_type: ShapeType,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::svg_path::AllowEmpty;
+
+ let fill = parse_fill_rule(input, shape_type);
+ let path = SVGPathData::parse(input, AllowEmpty::No)?;
+ Ok(Path { fill, path })
+ }
+}
+
+fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result
+where
+ W: Write,
+{
+ if !round.is_zero() {
+ dest.write_str(" round ")?;
+ round.to_css(dest)?;
+ }
+ Ok(())
+}
+
+impl ToCss for Xywh {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.x.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.y.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.width.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.height.to_css(dest)?;
+ round_to_css(&self.round, dest)
+ }
+}
+
+impl Xywh {
+ /// Parse the inner function arguments of `xywh()`.
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let x = LengthPercentage::parse(context, input)?;
+ let y = LengthPercentage::parse(context, input)?;
+ let width = NonNegativeLengthPercentage::parse(context, input)?;
+ let height = NonNegativeLengthPercentage::parse(context, input)?;
+ let round = parse_round(context, input)?;
+ Ok(Xywh {
+ x,
+ y,
+ width,
+ height,
+ round,
+ })
+ }
+}
+
+impl ToCss for ShapeRectFunction {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.rect.0.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.rect.1.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.rect.2.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.rect.3.to_css(dest)?;
+ round_to_css(&self.round, dest)
+ }
+}
+
+impl ShapeRectFunction {
+ /// Parse the inner function arguments of `rect()`.
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?;
+ let round = parse_round(context, input)?;
+ Ok(ShapeRectFunction { rect, round })
+ }
+}
+
+impl ToComputedValue for BasicShapeRect {
+ type ComputedValue = ComputedInsetRect;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ use crate::values::computed::LengthPercentage;
+ use crate::values::computed::LengthPercentageOrAuto;
+ use style_traits::values::specified::AllowedNumericType;
+
+ match self {
+ Self::Inset(ref inset) => inset.to_computed_value(context),
+ Self::Xywh(ref xywh) => {
+ // Given `xywh(x y w h)`, construct the equivalent inset() function,
+ // `inset(y calc(100% - x - w) calc(100% - y - h) x)`.
+ //
+ // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
+ // https://github.com/w3c/csswg-drafts/issues/9053
+ let x = xywh.x.to_computed_value(context);
+ let y = xywh.y.to_computed_value(context);
+ let w = xywh.width.to_computed_value(context);
+ let h = xywh.height.to_computed_value(context);
+ // calc(100% - x - w).
+ let right = LengthPercentage::hundred_percent_minus_list(
+ &[&x, &w.0],
+ AllowedNumericType::All,
+ );
+ // calc(100% - y - h).
+ let bottom = LengthPercentage::hundred_percent_minus_list(
+ &[&y, &h.0],
+ AllowedNumericType::All,
+ );
+
+ ComputedInsetRect {
+ rect: Rect::new(y, right, bottom, x),
+ round: xywh.round.to_computed_value(context),
+ }
+ },
+ Self::Rect(ref rect) => {
+ // Given `rect(t r b l)`, the equivalent function is
+ // `inset(t calc(100% - r) calc(100% - b) l)`.
+ //
+ // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
+ fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage {
+ match v {
+ // it’s equivalent to 0% as the first (top) or fourth (left) value.
+ // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
+ LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
+ LengthPercentageOrAuto::LengthPercentage(lp) => lp,
+ }
+ }
+ fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage {
+ match v {
+ // It's equivalent to 100% as the second (right) or third (bottom) value.
+ // So calc(100% - 100%) = 0%.
+ // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
+ LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
+ LengthPercentageOrAuto::LengthPercentage(lp) => {
+ LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All)
+ },
+ }
+ }
+
+ let round = rect.round.to_computed_value(context);
+ let rect = rect.rect.to_computed_value(context);
+ let rect = Rect::new(
+ compute_top_or_left(rect.0),
+ compute_bottom_or_right(rect.1),
+ compute_bottom_or_right(rect.2),
+ compute_top_or_left(rect.3),
+ );
+
+ ComputedInsetRect { rect, round }
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::Inset(ToComputedValue::from_computed_value(computed))
+ }
+}
diff --git a/servo/components/style/values/specified/border.rs b/servo/components/style/values/specified/border.rs
new file mode 100644
index 0000000000..a4660c7f60
--- /dev/null
+++ b/servo/components/style/values/specified/border.rs
@@ -0,0 +1,398 @@
+/* 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 types for CSS values related to borders.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius;
+use crate::values::generics::border::BorderImageSideWidth as GenericBorderImageSideWidth;
+use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice;
+use crate::values::generics::border::BorderRadius as GenericBorderRadius;
+use crate::values::generics::border::BorderSpacing as GenericBorderSpacing;
+use crate::values::generics::rect::Rect;
+use crate::values::generics::size::Size2D;
+use crate::values::specified::length::{Length, NonNegativeLength, NonNegativeLengthPercentage};
+use crate::values::specified::Color;
+use crate::values::specified::{AllowQuirks, NonNegativeNumber, NonNegativeNumberOrPercentage};
+use crate::Zero;
+use app_units::Au;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{values::SequenceWriter, CssWriter, ParseError, ToCss};
+
+/// A specified value for a single side of a `border-style` property.
+///
+/// The order here corresponds to the integer values from the border conflict
+/// resolution rules in CSS 2.1 § 17.6.2.1. Higher values override lower values.
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ MallocSizeOf,
+ Ord,
+ Parse,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum BorderStyle {
+ Hidden,
+ None,
+ Inset,
+ Groove,
+ Outset,
+ Ridge,
+ Dotted,
+ Dashed,
+ Solid,
+ Double,
+}
+
+impl BorderStyle {
+ /// Whether this border style is either none or hidden.
+ #[inline]
+ pub fn none_or_hidden(&self) -> bool {
+ matches!(*self, BorderStyle::None | BorderStyle::Hidden)
+ }
+}
+
+/// A specified value for the `border-image-width` property.
+pub type BorderImageWidth = Rect<BorderImageSideWidth>;
+
+/// A specified value for a single side of a `border-image-width` property.
+pub type BorderImageSideWidth =
+ GenericBorderImageSideWidth<NonNegativeLengthPercentage, NonNegativeNumber>;
+
+/// A specified value for the `border-image-slice` property.
+pub type BorderImageSlice = GenericBorderImageSlice<NonNegativeNumberOrPercentage>;
+
+/// A specified value for the `border-radius` property.
+pub type BorderRadius = GenericBorderRadius<NonNegativeLengthPercentage>;
+
+/// A specified value for the `border-*-radius` longhand properties.
+pub type BorderCornerRadius = GenericBorderCornerRadius<NonNegativeLengthPercentage>;
+
+/// A specified value for the `border-spacing` longhand properties.
+pub type BorderSpacing = GenericBorderSpacing<NonNegativeLength>;
+
+impl BorderImageSlice {
+ /// Returns the `100%` value.
+ #[inline]
+ pub fn hundred_percent() -> Self {
+ GenericBorderImageSlice {
+ offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()),
+ fill: false,
+ }
+ }
+}
+
+/// https://drafts.csswg.org/css-backgrounds-3/#typedef-line-width
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum LineWidth {
+ /// `thin`
+ Thin,
+ /// `medium`
+ Medium,
+ /// `thick`
+ Thick,
+ /// `<length>`
+ Length(NonNegativeLength),
+}
+
+impl LineWidth {
+ /// Returns the `0px` value.
+ #[inline]
+ pub fn zero() -> Self {
+ Self::Length(NonNegativeLength::zero())
+ }
+
+ fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(length) =
+ input.try_parse(|i| NonNegativeLength::parse_quirky(context, i, allow_quirks))
+ {
+ return Ok(Self::Length(length));
+ }
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "thin" => Self::Thin,
+ "medium" => Self::Medium,
+ "thick" => Self::Thick,
+ })
+ }
+}
+
+impl Parse for LineWidth {
+ fn parse<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl ToComputedValue for LineWidth {
+ type ComputedValue = app_units::Au;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ // https://drafts.csswg.org/css-backgrounds-3/#line-width
+ Self::Thin => Au::from_px(1),
+ Self::Medium => Au::from_px(3),
+ Self::Thick => Au::from_px(5),
+ Self::Length(ref length) => Au::from_f32_px(length.to_computed_value(context).px()),
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::Length(NonNegativeLength::from_px(computed.to_f32_px()))
+ }
+}
+
+/// A specified value for a single side of the `border-width` property. The difference between this
+/// and LineWidth is whether we snap to device pixels or not.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct BorderSideWidth(LineWidth);
+
+impl BorderSideWidth {
+ /// Returns the `medium` value.
+ pub fn medium() -> Self {
+ Self(LineWidth::Medium)
+ }
+
+ /// Returns a bare px value from the argument.
+ pub fn from_px(px: f32) -> Self {
+ Self(LineWidth::Length(Length::from_px(px).into()))
+ }
+
+ /// Parses, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self(LineWidth::parse_quirky(context, input, allow_quirks)?))
+ }
+}
+
+impl Parse for BorderSideWidth {
+ fn parse<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl ToComputedValue for BorderSideWidth {
+ type ComputedValue = app_units::Au;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let width = self.0.to_computed_value(context);
+ // Round `width` down to the nearest device pixel, but any non-zero value that would round
+ // down to zero is clamped to 1 device pixel.
+ if width == Au(0) {
+ return width;
+ }
+
+ let au_per_dev_px = context.device().app_units_per_device_pixel();
+ std::cmp::max(
+ Au(au_per_dev_px),
+ Au(width.0 / au_per_dev_px * au_per_dev_px),
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self(LineWidth::from_computed_value(computed))
+ }
+}
+
+impl BorderImageSideWidth {
+ /// Returns `1`.
+ #[inline]
+ pub fn one() -> Self {
+ GenericBorderImageSideWidth::Number(NonNegativeNumber::new(1.))
+ }
+}
+
+impl Parse for BorderImageSlice {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok();
+ let offsets = Rect::parse_with(context, input, NonNegativeNumberOrPercentage::parse)?;
+ if !fill {
+ fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok();
+ }
+ Ok(GenericBorderImageSlice { offsets, fill })
+ }
+}
+
+impl Parse for BorderRadius {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let widths = Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)?;
+ let heights = if input.try_parse(|i| i.expect_delim('/')).is_ok() {
+ Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)?
+ } else {
+ widths.clone()
+ };
+
+ Ok(GenericBorderRadius {
+ top_left: BorderCornerRadius::new(widths.0, heights.0),
+ top_right: BorderCornerRadius::new(widths.1, heights.1),
+ bottom_right: BorderCornerRadius::new(widths.2, heights.2),
+ bottom_left: BorderCornerRadius::new(widths.3, heights.3),
+ })
+ }
+}
+
+impl Parse for BorderCornerRadius {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Size2D::parse_with(context, input, NonNegativeLengthPercentage::parse)
+ .map(GenericBorderCornerRadius)
+ }
+}
+
+impl Parse for BorderSpacing {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Size2D::parse_with(context, input, |context, input| {
+ NonNegativeLength::parse_quirky(context, input, AllowQuirks::Yes)
+ })
+ .map(GenericBorderSpacing)
+ }
+}
+
+/// A single border-image-repeat keyword.
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum BorderImageRepeatKeyword {
+ Stretch,
+ Repeat,
+ Round,
+ Space,
+}
+
+/// The specified value for the `border-image-repeat` property.
+///
+/// https://drafts.csswg.org/css-backgrounds/#the-border-image-repeat
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct BorderImageRepeat(pub BorderImageRepeatKeyword, pub BorderImageRepeatKeyword);
+
+impl ToCss for BorderImageRepeat {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest)?;
+ if self.0 != self.1 {
+ dest.write_char(' ')?;
+ self.1.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl BorderImageRepeat {
+ /// Returns the `stretch` value.
+ #[inline]
+ pub fn stretch() -> Self {
+ BorderImageRepeat(
+ BorderImageRepeatKeyword::Stretch,
+ BorderImageRepeatKeyword::Stretch,
+ )
+ }
+}
+
+impl Parse for BorderImageRepeat {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let horizontal = BorderImageRepeatKeyword::parse(input)?;
+ let vertical = input.try_parse(BorderImageRepeatKeyword::parse).ok();
+ Ok(BorderImageRepeat(
+ horizontal,
+ vertical.unwrap_or(horizontal),
+ ))
+ }
+}
+
+/// Serializes a border shorthand value composed of width/style/color.
+pub fn serialize_directional_border<W>(
+ dest: &mut CssWriter<W>,
+ width: &BorderSideWidth,
+ style: &BorderStyle,
+ color: &Color,
+) -> fmt::Result
+where
+ W: Write,
+{
+ let has_style = *style != BorderStyle::None;
+ let has_color = *color != Color::CurrentColor;
+ let has_width = *width != BorderSideWidth::medium();
+ if !has_style && !has_color && !has_width {
+ return width.to_css(dest);
+ }
+ let mut writer = SequenceWriter::new(dest, " ");
+ if has_width {
+ writer.item(width)?;
+ }
+ if has_style {
+ writer.item(style)?;
+ }
+ if has_color {
+ writer.item(color)?;
+ }
+ Ok(())
+}
diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs
new file mode 100644
index 0000000000..8414591c2b
--- /dev/null
+++ b/servo/components/style/values/specified/box.rs
@@ -0,0 +1,1945 @@
+/* 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 types for box properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::properties::{LonghandId, PropertyDeclarationId, PropertyId};
+use crate::values::generics::box_::{
+ GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign,
+ VerticalAlignKeyword,
+};
+use crate::values::specified::length::{LengthPercentage, NonNegativeLength};
+use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumberOrPercentage};
+use crate::values::CustomIdent;
+use cssparser::Parser;
+use num_traits::FromPrimitive;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, KeywordsCollectFn, ParseError};
+use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+#[cfg(not(feature = "servo-layout-2020"))]
+fn flexbox_enabled() -> bool {
+ true
+}
+
+#[cfg(feature = "servo-layout-2020")]
+fn flexbox_enabled() -> bool {
+ servo_config::prefs::pref_map()
+ .get("layout.flexbox.enabled")
+ .as_bool()
+ .unwrap_or(false)
+}
+
+/// Defines an element’s display type, which consists of
+/// the two basic qualities of how an element generates boxes
+/// <https://drafts.csswg.org/css-display/#propdef-display>
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+#[repr(u8)]
+pub enum DisplayOutside {
+ None = 0,
+ Inline,
+ Block,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableCaption,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ InternalTable,
+ #[cfg(feature = "gecko")]
+ InternalRuby,
+}
+
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+#[repr(u8)]
+pub enum DisplayInside {
+ None = 0,
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ Contents,
+ Flow,
+ FlowRoot,
+ Flex,
+ #[cfg(feature = "gecko")]
+ Grid,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ Table,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableRowGroup,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableColumn,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableColumnGroup,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableHeaderGroup,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableFooterGroup,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableRow,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableCell,
+ #[cfg(feature = "gecko")]
+ Ruby,
+ #[cfg(feature = "gecko")]
+ RubyBase,
+ #[cfg(feature = "gecko")]
+ RubyBaseContainer,
+ #[cfg(feature = "gecko")]
+ RubyText,
+ #[cfg(feature = "gecko")]
+ RubyTextContainer,
+ #[cfg(feature = "gecko")]
+ WebkitBox,
+}
+
+impl DisplayInside {
+ fn is_valid_for_list_item(self) -> bool {
+ match self {
+ DisplayInside::Flow => true,
+ #[cfg(feature = "gecko")]
+ DisplayInside::FlowRoot => true,
+ _ => false,
+ }
+ }
+
+ /// https://drafts.csswg.org/css-display/#inside-model:
+ /// If <display-outside> is omitted, the element’s outside display type defaults to block
+ /// — except for ruby, which defaults to inline.
+ fn default_display_outside(self) -> DisplayOutside {
+ match self {
+ #[cfg(feature = "gecko")]
+ DisplayInside::Ruby => DisplayOutside::Inline,
+ _ => DisplayOutside::Block,
+ }
+ }
+}
+
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct Display(u16);
+
+/// Gecko-only impl block for Display (shared stuff later in this file):
+#[allow(missing_docs)]
+#[allow(non_upper_case_globals)]
+impl Display {
+ // Our u16 bits are used as follows: LOOOOOOOIIIIIIII
+ pub const LIST_ITEM_MASK: u16 = 0b1000000000000000;
+ pub const OUTSIDE_MASK: u16 = 0b0111111100000000;
+ pub const INSIDE_MASK: u16 = 0b0000000011111111;
+ pub const OUTSIDE_SHIFT: u16 = 8;
+
+ /// https://drafts.csswg.org/css-display/#the-display-properties
+ /// ::new() inlined so cbindgen can use it
+ pub const None: Self =
+ Self(((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::None as u16);
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ pub const Contents: Self = Self(
+ ((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Contents as u16,
+ );
+ pub const Inline: Self =
+ Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16);
+ pub const InlineBlock: Self = Self(
+ ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16,
+ );
+ pub const Block: Self =
+ Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16);
+ #[cfg(feature = "gecko")]
+ pub const FlowRoot: Self = Self(
+ ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16,
+ );
+ pub const Flex: Self =
+ Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16);
+ pub const InlineFlex: Self =
+ Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16);
+ #[cfg(feature = "gecko")]
+ pub const Grid: Self =
+ Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16);
+ #[cfg(feature = "gecko")]
+ pub const InlineGrid: Self =
+ Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16);
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const Table: Self =
+ Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16);
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const InlineTable: Self = Self(
+ ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableCaption: Self = Self(
+ ((DisplayOutside::TableCaption as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const Ruby: Self =
+ Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Ruby as u16);
+ #[cfg(feature = "gecko")]
+ pub const WebkitBox: Self = Self(
+ ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const WebkitInlineBox: Self = Self(
+ ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16,
+ );
+
+ // Internal table boxes.
+
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableRowGroup: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableRowGroup as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableHeaderGroup: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableHeaderGroup as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableFooterGroup: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableFooterGroup as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableColumn: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableColumn as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableColumnGroup: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableColumnGroup as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableRow: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableRow as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableCell: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableCell as u16,
+ );
+
+ /// Internal ruby boxes.
+ #[cfg(feature = "gecko")]
+ pub const RubyBase: Self = Self(
+ ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::RubyBase as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const RubyBaseContainer: Self = Self(
+ ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::RubyBaseContainer as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const RubyText: Self = Self(
+ ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::RubyText as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const RubyTextContainer: Self = Self(
+ ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::RubyTextContainer as u16,
+ );
+
+ /// Make a raw display value from <display-outside> and <display-inside> values.
+ #[inline]
+ const fn new(outside: DisplayOutside, inside: DisplayInside) -> Self {
+ Self((outside as u16) << Self::OUTSIDE_SHIFT | inside as u16)
+ }
+
+ /// Make a display enum value from <display-outside> and <display-inside> values.
+ #[inline]
+ fn from3(outside: DisplayOutside, inside: DisplayInside, list_item: bool) -> Self {
+ let v = Self::new(outside, inside);
+ if !list_item {
+ return v;
+ }
+ Self(v.0 | Self::LIST_ITEM_MASK)
+ }
+
+ /// Accessor for the <display-inside> value.
+ #[inline]
+ pub fn inside(&self) -> DisplayInside {
+ DisplayInside::from_u16(self.0 & Self::INSIDE_MASK).unwrap()
+ }
+
+ /// Accessor for the <display-outside> value.
+ #[inline]
+ pub fn outside(&self) -> DisplayOutside {
+ DisplayOutside::from_u16((self.0 & Self::OUTSIDE_MASK) >> Self::OUTSIDE_SHIFT).unwrap()
+ }
+
+ /// Returns the raw underlying u16 value.
+ #[inline]
+ pub const fn to_u16(&self) -> u16 {
+ self.0
+ }
+
+ /// Whether this is `display: inline` (or `inline list-item`).
+ #[inline]
+ pub fn is_inline_flow(&self) -> bool {
+ self.outside() == DisplayOutside::Inline && self.inside() == DisplayInside::Flow
+ }
+
+ /// Returns whether this `display` value is some kind of list-item.
+ #[inline]
+ pub const fn is_list_item(&self) -> bool {
+ (self.0 & Self::LIST_ITEM_MASK) != 0
+ }
+
+ /// Returns whether this `display` value is a ruby level container.
+ pub fn is_ruby_level_container(&self) -> bool {
+ match *self {
+ #[cfg(feature = "gecko")]
+ Display::RubyBaseContainer | Display::RubyTextContainer => true,
+ _ => false,
+ }
+ }
+
+ /// Returns whether this `display` value is one of the types for ruby.
+ pub fn is_ruby_type(&self) -> bool {
+ match self.inside() {
+ #[cfg(feature = "gecko")]
+ DisplayInside::Ruby |
+ DisplayInside::RubyBase |
+ DisplayInside::RubyText |
+ DisplayInside::RubyBaseContainer |
+ DisplayInside::RubyTextContainer => true,
+ _ => false,
+ }
+ }
+}
+
+/// Shared Display impl for both Gecko and Servo.
+impl Display {
+ /// The initial display value.
+ #[inline]
+ pub fn inline() -> Self {
+ Display::Inline
+ }
+
+ /// <https://drafts.csswg.org/css2/visuren.html#x13>
+ #[cfg(feature = "servo")]
+ #[inline]
+ pub fn is_atomic_inline_level(&self) -> bool {
+ match *self {
+ Display::InlineBlock | Display::InlineFlex => true,
+ #[cfg(any(feature = "servo-layout-2013"))]
+ Display::InlineTable => true,
+ _ => false,
+ }
+ }
+
+ /// Returns whether this `display` value is the display of a flex or
+ /// grid container.
+ ///
+ /// This is used to implement various style fixups.
+ pub fn is_item_container(&self) -> bool {
+ match self.inside() {
+ DisplayInside::Flex => true,
+ #[cfg(feature = "gecko")]
+ DisplayInside::Grid => true,
+ _ => false,
+ }
+ }
+
+ /// Returns whether an element with this display type is a line
+ /// participant, which means it may lay its children on the same
+ /// line as itself.
+ pub fn is_line_participant(&self) -> bool {
+ if self.is_inline_flow() {
+ return true;
+ }
+ match *self {
+ #[cfg(feature = "gecko")]
+ Display::Contents | Display::Ruby | Display::RubyBaseContainer => true,
+ _ => false,
+ }
+ }
+
+ /// Convert this display into an equivalent block display.
+ ///
+ /// Also used for :root style adjustments.
+ pub fn equivalent_block_display(&self, _is_root_element: bool) -> Self {
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ {
+ // Special handling for `contents` and `list-item`s on the root element.
+ if _is_root_element && (self.is_contents() || self.is_list_item()) {
+ return Display::Block;
+ }
+ }
+
+ match self.outside() {
+ DisplayOutside::Inline => {
+ let inside = match self.inside() {
+ // `inline-block` blockifies to `block` rather than
+ // `flow-root`, for legacy reasons.
+ DisplayInside::FlowRoot => DisplayInside::Flow,
+ inside => inside,
+ };
+ Display::from3(DisplayOutside::Block, inside, self.is_list_item())
+ },
+ DisplayOutside::Block | DisplayOutside::None => *self,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ _ => Display::Block,
+ }
+ }
+
+ /// Convert this display into an equivalent inline-outside display.
+ /// https://drafts.csswg.org/css-display/#inlinify
+ #[cfg(feature = "gecko")]
+ pub fn inlinify(&self) -> Self {
+ match self.outside() {
+ DisplayOutside::Block => {
+ let inside = match self.inside() {
+ // `display: block` inlinifies to `display: inline-block`,
+ // rather than `inline`, for legacy reasons.
+ DisplayInside::Flow => DisplayInside::FlowRoot,
+ inside => inside,
+ };
+ Display::from3(DisplayOutside::Inline, inside, self.is_list_item())
+ },
+ _ => *self,
+ }
+ }
+
+ /// Returns true if the value is `Contents`
+ #[inline]
+ pub fn is_contents(&self) -> bool {
+ match *self {
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ Display::Contents => true,
+ _ => false,
+ }
+ }
+
+ /// Returns true if the value is `None`
+ #[inline]
+ pub fn is_none(&self) -> bool {
+ *self == Display::None
+ }
+}
+
+enum DisplayKeyword {
+ Full(Display),
+ Inside(DisplayInside),
+ Outside(DisplayOutside),
+ ListItem,
+}
+
+impl DisplayKeyword {
+ fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ use self::DisplayKeyword::*;
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "none" => Full(Display::None),
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ "contents" => Full(Display::Contents),
+ "inline-block" => Full(Display::InlineBlock),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "inline-table" => Full(Display::InlineTable),
+ "-webkit-flex" if flexbox_enabled() => Full(Display::Flex),
+ "inline-flex" | "-webkit-inline-flex" if flexbox_enabled() => Full(Display::InlineFlex),
+ #[cfg(feature = "gecko")]
+ "inline-grid" => Full(Display::InlineGrid),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-caption" => Full(Display::TableCaption),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-row-group" => Full(Display::TableRowGroup),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-header-group" => Full(Display::TableHeaderGroup),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-footer-group" => Full(Display::TableFooterGroup),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-column" => Full(Display::TableColumn),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-column-group" => Full(Display::TableColumnGroup),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-row" => Full(Display::TableRow),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-cell" => Full(Display::TableCell),
+ #[cfg(feature = "gecko")]
+ "ruby-base" => Full(Display::RubyBase),
+ #[cfg(feature = "gecko")]
+ "ruby-base-container" => Full(Display::RubyBaseContainer),
+ #[cfg(feature = "gecko")]
+ "ruby-text" => Full(Display::RubyText),
+ #[cfg(feature = "gecko")]
+ "ruby-text-container" => Full(Display::RubyTextContainer),
+ #[cfg(feature = "gecko")]
+ "-webkit-box" => Full(Display::WebkitBox),
+ #[cfg(feature = "gecko")]
+ "-webkit-inline-box" => Full(Display::WebkitInlineBox),
+
+ /// <display-outside> = block | inline | run-in
+ /// https://drafts.csswg.org/css-display/#typedef-display-outside
+ "block" => Outside(DisplayOutside::Block),
+ "inline" => Outside(DisplayOutside::Inline),
+
+ "list-item" => ListItem,
+
+ /// <display-inside> = flow | flow-root | table | flex | grid | ruby
+ /// https://drafts.csswg.org/css-display/#typedef-display-inside
+ "flow" => Inside(DisplayInside::Flow),
+ "flex" if flexbox_enabled() => Inside(DisplayInside::Flex),
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ "flow-root" => Inside(DisplayInside::FlowRoot),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table" => Inside(DisplayInside::Table),
+ #[cfg(feature = "gecko")]
+ "grid" => Inside(DisplayInside::Grid),
+ #[cfg(feature = "gecko")]
+ "ruby" => Inside(DisplayInside::Ruby),
+ })
+ }
+}
+
+impl ToCss for Display {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let outside = self.outside();
+ let inside = self.inside();
+ match *self {
+ Display::Block | Display::Inline => outside.to_css(dest),
+ Display::InlineBlock => dest.write_str("inline-block"),
+ #[cfg(feature = "gecko")]
+ Display::WebkitInlineBox => dest.write_str("-webkit-inline-box"),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ Display::TableCaption => dest.write_str("table-caption"),
+ _ => match (outside, inside) {
+ #[cfg(feature = "gecko")]
+ (DisplayOutside::Inline, DisplayInside::Grid) => dest.write_str("inline-grid"),
+ (DisplayOutside::Inline, DisplayInside::Flex) => dest.write_str("inline-flex"),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ (DisplayOutside::Inline, DisplayInside::Table) => dest.write_str("inline-table"),
+ #[cfg(feature = "gecko")]
+ (DisplayOutside::Block, DisplayInside::Ruby) => dest.write_str("block ruby"),
+ (_, inside) => {
+ if self.is_list_item() {
+ if outside != DisplayOutside::Block {
+ outside.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+ if inside != DisplayInside::Flow {
+ inside.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+ dest.write_str("list-item")
+ } else {
+ inside.to_css(dest)
+ }
+ },
+ },
+ }
+ }
+}
+
+impl Parse for Display {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Display, ParseError<'i>> {
+ let mut got_list_item = false;
+ let mut inside = None;
+ let mut outside = None;
+ match DisplayKeyword::parse(input)? {
+ DisplayKeyword::Full(d) => return Ok(d),
+ DisplayKeyword::Outside(o) => {
+ outside = Some(o);
+ },
+ DisplayKeyword::Inside(i) => {
+ inside = Some(i);
+ },
+ DisplayKeyword::ListItem => {
+ got_list_item = true;
+ },
+ };
+
+ while let Ok(kw) = input.try_parse(DisplayKeyword::parse) {
+ match kw {
+ DisplayKeyword::ListItem if !got_list_item => {
+ got_list_item = true;
+ },
+ DisplayKeyword::Outside(o) if outside.is_none() => {
+ outside = Some(o);
+ },
+ DisplayKeyword::Inside(i) if inside.is_none() => {
+ inside = Some(i);
+ },
+ _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+
+ let inside = inside.unwrap_or(DisplayInside::Flow);
+ let outside = outside.unwrap_or_else(|| inside.default_display_outside());
+ if got_list_item && !inside.is_valid_for_list_item() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ return Ok(Display::from3(outside, inside, got_list_item));
+ }
+}
+
+impl SpecifiedValueInfo for Display {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&[
+ "block",
+ "contents",
+ "flex",
+ "flow-root",
+ "flow-root list-item",
+ "grid",
+ "inline",
+ "inline-block",
+ "inline-flex",
+ "inline-grid",
+ "inline-table",
+ "inline list-item",
+ "inline flow-root list-item",
+ "list-item",
+ "none",
+ "block ruby",
+ "ruby",
+ "ruby-base",
+ "ruby-base-container",
+ "ruby-text",
+ "ruby-text-container",
+ "table",
+ "table-caption",
+ "table-cell",
+ "table-column",
+ "table-column-group",
+ "table-footer-group",
+ "table-header-group",
+ "table-row",
+ "table-row-group",
+ "-webkit-box",
+ "-webkit-inline-box",
+ ]);
+ }
+}
+
+/// A specified value for the `contain-intrinsic-size` property.
+pub type ContainIntrinsicSize = GenericContainIntrinsicSize<NonNegativeLength>;
+
+/// A specified value for the `line-clamp` property.
+pub type LineClamp = GenericLineClamp<Integer>;
+
+/// A specified value for the `vertical-align` property.
+pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>;
+
+impl Parse for VerticalAlign {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(lp) =
+ input.try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes))
+ {
+ return Ok(GenericVerticalAlign::Length(lp));
+ }
+
+ Ok(GenericVerticalAlign::Keyword(VerticalAlignKeyword::parse(
+ input,
+ )?))
+ }
+}
+
+/// A specified value for the `baseline-source` property.
+/// https://drafts.csswg.org/css-inline-3/#baseline-source
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToShmem,
+ ToComputedValue,
+ ToResolvedValue,
+)]
+#[repr(u8)]
+pub enum BaselineSource {
+ /// `Last` for `inline-block`, `First` otherwise.
+ Auto,
+ /// Use first baseline for alignment.
+ First,
+ /// Use last baseline for alignment.
+ Last,
+}
+
+/// https://drafts.csswg.org/css-scroll-snap-1/#snap-axis
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollSnapAxis {
+ X,
+ Y,
+ Block,
+ Inline,
+ Both,
+}
+
+/// https://drafts.csswg.org/css-scroll-snap-1/#snap-strictness
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollSnapStrictness {
+ #[css(skip)]
+ None, // Used to represent scroll-snap-type: none. It's not parsed.
+ Mandatory,
+ Proximity,
+}
+
+/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct ScrollSnapType {
+ axis: ScrollSnapAxis,
+ strictness: ScrollSnapStrictness,
+}
+
+impl ScrollSnapType {
+ /// Returns `none`.
+ #[inline]
+ pub fn none() -> Self {
+ Self {
+ axis: ScrollSnapAxis::Both,
+ strictness: ScrollSnapStrictness::None,
+ }
+ }
+}
+
+impl Parse for ScrollSnapType {
+ /// none | [ x | y | block | inline | both ] [ mandatory | proximity ]?
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(ScrollSnapType::none());
+ }
+
+ let axis = ScrollSnapAxis::parse(input)?;
+ let strictness = input
+ .try_parse(ScrollSnapStrictness::parse)
+ .unwrap_or(ScrollSnapStrictness::Proximity);
+ Ok(Self { axis, strictness })
+ }
+}
+
+impl ToCss for ScrollSnapType {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.strictness == ScrollSnapStrictness::None {
+ return dest.write_str("none");
+ }
+ self.axis.to_css(dest)?;
+ if self.strictness != ScrollSnapStrictness::Proximity {
+ dest.write_char(' ')?;
+ self.strictness.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// Specified value of scroll-snap-align keyword value.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollSnapAlignKeyword {
+ None,
+ Start,
+ End,
+ Center,
+}
+
+/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct ScrollSnapAlign {
+ block: ScrollSnapAlignKeyword,
+ inline: ScrollSnapAlignKeyword,
+}
+
+impl ScrollSnapAlign {
+ /// Returns `none`.
+ #[inline]
+ pub fn none() -> Self {
+ ScrollSnapAlign {
+ block: ScrollSnapAlignKeyword::None,
+ inline: ScrollSnapAlignKeyword::None,
+ }
+ }
+}
+
+impl Parse for ScrollSnapAlign {
+ /// [ none | start | end | center ]{1,2}
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<ScrollSnapAlign, ParseError<'i>> {
+ let block = ScrollSnapAlignKeyword::parse(input)?;
+ let inline = input
+ .try_parse(ScrollSnapAlignKeyword::parse)
+ .unwrap_or(block);
+ Ok(ScrollSnapAlign { block, inline })
+ }
+}
+
+impl ToCss for ScrollSnapAlign {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.block.to_css(dest)?;
+ if self.block != self.inline {
+ dest.write_char(' ')?;
+ self.inline.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollSnapStop {
+ Normal,
+ Always,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum OverscrollBehavior {
+ Auto,
+ Contain,
+ None,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum OverflowAnchor {
+ Auto,
+ None,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum OverflowClipBox {
+ PaddingBox,
+ ContentBox,
+}
+
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma)]
+#[repr(C)]
+/// Provides a rendering hint to the user agent, stating what kinds of changes
+/// the author expects to perform on the element.
+///
+/// `auto` is represented by an empty `features` list.
+///
+/// <https://drafts.csswg.org/css-will-change/#will-change>
+pub struct WillChange {
+ /// The features that are supposed to change.
+ ///
+ /// TODO(emilio): Consider using ArcSlice since we just clone them from the
+ /// specified value? That'd save an allocation, which could be worth it.
+ #[css(iterable, if_empty = "auto")]
+ features: crate::OwnedSlice<CustomIdent>,
+ /// A bitfield with the kind of change that the value will create, based
+ /// on the above field.
+ #[css(skip)]
+ bits: WillChangeBits,
+}
+
+impl WillChange {
+ #[inline]
+ /// Get default value of `will-change` as `auto`
+ pub fn auto() -> Self {
+ Self::default()
+ }
+}
+
+/// The change bits that we care about.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Default,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct WillChangeBits(u16);
+bitflags! {
+ impl WillChangeBits: u16 {
+ /// Whether a property which can create a stacking context **on any
+ /// box** will change.
+ const STACKING_CONTEXT_UNCONDITIONAL = 1 << 0;
+ /// Whether `transform` or related properties will change.
+ const TRANSFORM = 1 << 1;
+ /// Whether `scroll-position` will change.
+ const SCROLL = 1 << 2;
+ /// Whether `contain` will change.
+ const CONTAIN = 1 << 3;
+ /// Whether `opacity` will change.
+ const OPACITY = 1 << 4;
+ /// Whether `perspective` will change.
+ const PERSPECTIVE = 1 << 5;
+ /// Whether `z-index` will change.
+ const Z_INDEX = 1 << 6;
+ /// Whether any property which creates a containing block for non-svg
+ /// text frames will change.
+ const FIXPOS_CB_NON_SVG = 1 << 7;
+ /// Whether the position property will change.
+ const POSITION = 1 << 8;
+ }
+}
+
+fn change_bits_for_longhand(longhand: LonghandId) -> WillChangeBits {
+ match longhand {
+ LonghandId::Opacity => WillChangeBits::OPACITY,
+ LonghandId::Contain => WillChangeBits::CONTAIN,
+ LonghandId::Perspective => WillChangeBits::PERSPECTIVE,
+ LonghandId::Position => {
+ WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::POSITION
+ },
+ LonghandId::ZIndex => WillChangeBits::Z_INDEX,
+ LonghandId::Transform |
+ LonghandId::TransformStyle |
+ LonghandId::Translate |
+ LonghandId::Rotate |
+ LonghandId::Scale |
+ LonghandId::OffsetPath => WillChangeBits::TRANSFORM,
+ LonghandId::BackdropFilter | LonghandId::Filter => {
+ WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::FIXPOS_CB_NON_SVG
+ },
+ LonghandId::MixBlendMode |
+ LonghandId::Isolation |
+ LonghandId::MaskImage |
+ LonghandId::ClipPath => WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL,
+ _ => WillChangeBits::empty(),
+ }
+}
+
+fn change_bits_for_maybe_property(ident: &str, context: &ParserContext) -> WillChangeBits {
+ let id = match PropertyId::parse_ignoring_rule_type(ident, context) {
+ Ok(id) => id,
+ Err(..) => return WillChangeBits::empty(),
+ };
+
+ match id.as_shorthand() {
+ Ok(shorthand) => shorthand
+ .longhands()
+ .fold(WillChangeBits::empty(), |flags, p| {
+ flags | change_bits_for_longhand(p)
+ }),
+ Err(PropertyDeclarationId::Longhand(longhand)) => change_bits_for_longhand(longhand),
+ Err(PropertyDeclarationId::Custom(..)) => WillChangeBits::empty(),
+ }
+}
+
+impl Parse for WillChange {
+ /// auto | <animateable-feature>#
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("auto"))
+ .is_ok()
+ {
+ return Ok(Self::default());
+ }
+
+ let mut bits = WillChangeBits::empty();
+ let custom_idents = input.parse_comma_separated(|i| {
+ let location = i.current_source_location();
+ let parser_ident = i.expect_ident()?;
+ let ident = CustomIdent::from_ident(
+ location,
+ parser_ident,
+ &["will-change", "none", "all", "auto"],
+ )?;
+
+ if context.in_ua_sheet() && ident.0 == atom!("-moz-fixed-pos-containing-block") {
+ bits |= WillChangeBits::FIXPOS_CB_NON_SVG;
+ } else if ident.0 == atom!("scroll-position") {
+ bits |= WillChangeBits::SCROLL;
+ } else {
+ bits |= change_bits_for_maybe_property(&parser_ident, context);
+ }
+ Ok(ident)
+ })?;
+
+ Ok(Self {
+ features: custom_idents.into(),
+ bits,
+ })
+ }
+}
+
+/// Values for the `touch-action` property.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(bitflags(single = "none,auto,manipulation", mixed = "pan-x,pan-y,pinch-zoom"))]
+#[repr(C)]
+pub struct TouchAction(u8);
+bitflags! {
+ impl TouchAction: u8 {
+ /// `none` variant
+ const NONE = 1 << 0;
+ /// `auto` variant
+ const AUTO = 1 << 1;
+ /// `pan-x` variant
+ const PAN_X = 1 << 2;
+ /// `pan-y` variant
+ const PAN_Y = 1 << 3;
+ /// `manipulation` variant
+ const MANIPULATION = 1 << 4;
+ /// `pinch-zoom` variant
+ const PINCH_ZOOM = 1 << 5;
+ }
+}
+
+impl TouchAction {
+ #[inline]
+ /// Get default `touch-action` as `auto`
+ pub fn auto() -> TouchAction {
+ TouchAction::AUTO
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(bitflags(
+ single = "none,strict,content",
+ mixed = "size,layout,style,paint,inline-size",
+ overlapping_bits
+))]
+#[repr(C)]
+/// Constants for contain: https://drafts.csswg.org/css-contain/#contain-property
+pub struct Contain(u8);
+bitflags! {
+ impl Contain: u8 {
+ /// `none` variant, just for convenience.
+ const NONE = 0;
+ /// `inline-size` variant, turns on single-axis inline size containment
+ const INLINE_SIZE = 1 << 0;
+ /// `block-size` variant, turns on single-axis block size containment, internal only
+ const BLOCK_SIZE = 1 << 1;
+ /// `layout` variant, turns on layout containment
+ const LAYOUT = 1 << 2;
+ /// `style` variant, turns on style containment
+ const STYLE = 1 << 3;
+ /// `paint` variant, turns on paint containment
+ const PAINT = 1 << 4;
+ /// 'size' variant, turns on size containment
+ const SIZE = 1 << 5 | Contain::INLINE_SIZE.bits() | Contain::BLOCK_SIZE.bits();
+ /// `content` variant, turns on layout and paint containment
+ const CONTENT = 1 << 6 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits();
+ /// `strict` variant, turns on all types of containment
+ const STRICT = 1 << 7 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits() | Contain::SIZE.bits();
+ }
+}
+
+impl Parse for ContainIntrinsicSize {
+ /// none | <length> | auto <length>
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(l) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
+ return Ok(Self::Length(l));
+ }
+
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(Self::AutoNone);
+ }
+
+ let l = NonNegativeLength::parse(context, input)?;
+ return Ok(Self::AutoLength(l));
+ }
+
+ input.expect_ident_matching("none")?;
+ Ok(Self::None)
+ }
+}
+
+impl Parse for LineClamp {
+ /// none | <positive-integer>
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(i) =
+ input.try_parse(|i| crate::values::specified::PositiveInteger::parse(context, i))
+ {
+ return Ok(Self(i.0));
+ }
+ input.expect_ident_matching("none")?;
+ Ok(Self::none())
+ }
+}
+
+/// https://drafts.csswg.org/css-contain-2/#content-visibility
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ContentVisibility {
+ /// `auto` variant, the element turns on layout containment, style containment, and paint
+ /// containment. In addition, if the element is not relevant to the user (such as by being
+ /// offscreen) it also skips its content
+ Auto,
+ /// `hidden` variant, the element skips its content
+ Hidden,
+ /// 'visible' variant, no effect
+ Visible,
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ PartialEq,
+ Eq,
+ MallocSizeOf,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ Parse,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+#[allow(missing_docs)]
+/// https://drafts.csswg.org/css-contain-3/#container-type
+pub enum ContainerType {
+ /// The `normal` variant.
+ Normal,
+ /// The `inline-size` variant.
+ InlineSize,
+ /// The `size` variant.
+ Size,
+}
+
+impl ContainerType {
+ /// Is this container-type: normal?
+ pub fn is_normal(self) -> bool {
+ self == Self::Normal
+ }
+
+ /// Is this type containing size in any way?
+ pub fn is_size_container_type(self) -> bool {
+ !self.is_normal()
+ }
+}
+
+/// https://drafts.csswg.org/css-contain-3/#container-name
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct ContainerName(#[css(iterable, if_empty = "none")] pub crate::OwnedSlice<CustomIdent>);
+
+impl ContainerName {
+ /// Return the `none` value.
+ pub fn none() -> Self {
+ Self(Default::default())
+ }
+
+ /// Returns whether this is the `none` value.
+ pub fn is_none(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ fn parse_internal<'i>(
+ input: &mut Parser<'i, '_>,
+ for_query: bool,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut idents = vec![];
+ let location = input.current_source_location();
+ let first = input.expect_ident()?;
+ if !for_query && first.eq_ignore_ascii_case("none") {
+ return Ok(Self::none());
+ }
+ const DISALLOWED_CONTAINER_NAMES: &'static [&'static str] = &["none", "not", "or", "and"];
+ idents.push(CustomIdent::from_ident(
+ location,
+ first,
+ DISALLOWED_CONTAINER_NAMES,
+ )?);
+ if !for_query {
+ while let Ok(name) =
+ input.try_parse(|input| CustomIdent::parse(input, DISALLOWED_CONTAINER_NAMES))
+ {
+ idents.push(name);
+ }
+ }
+ Ok(ContainerName(idents.into()))
+ }
+
+ /// https://github.com/w3c/csswg-drafts/issues/7203
+ /// Only a single name allowed in @container rule.
+ /// Disallow none for container-name in @container rule.
+ pub fn parse_for_query<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(input, /* for_query = */ true)
+ }
+}
+
+impl Parse for ContainerName {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(input, /* for_query = */ false)
+ }
+}
+
+/// A specified value for the `perspective` property.
+pub type Perspective = GenericPerspective<NonNegativeLength>;
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+/// https://drafts.csswg.org/css-box/#propdef-float
+pub enum Float {
+ Left,
+ Right,
+ None,
+ // https://drafts.csswg.org/css-logical-props/#float-clear
+ InlineStart,
+ InlineEnd,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+/// https://drafts.csswg.org/css2/#propdef-clear
+pub enum Clear {
+ None,
+ Left,
+ Right,
+ Both,
+ // https://drafts.csswg.org/css-logical-props/#float-clear
+ InlineStart,
+ InlineEnd,
+}
+
+/// https://drafts.csswg.org/css-ui/#propdef-resize
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub enum Resize {
+ None,
+ Both,
+ Horizontal,
+ Vertical,
+ // https://drafts.csswg.org/css-logical-1/#resize
+ Inline,
+ Block,
+}
+
+/// The value for the `appearance` property.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum Appearance {
+ /// No appearance at all.
+ None,
+ /// Default appearance for the element.
+ ///
+ /// This value doesn't make sense for -moz-default-appearance, but we don't bother to guard
+ /// against parsing it.
+ Auto,
+ /// A searchfield.
+ Searchfield,
+ /// A multi-line text field, e.g. HTML <textarea>.
+ Textarea,
+ /// A checkbox element.
+ Checkbox,
+ /// A radio element within a radio group.
+ Radio,
+ /// A dropdown list.
+ Menulist,
+ /// List boxes.
+ Listbox,
+ /// A horizontal meter bar.
+ Meter,
+ /// A horizontal progress bar.
+ ProgressBar,
+ /// A typical dialog button.
+ Button,
+ /// A single-line text field, e.g. HTML <input type=text>.
+ Textfield,
+ /// The dropdown button(s) that open up a dropdown list.
+ MenulistButton,
+ /// Various arrows that go in buttons
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ButtonArrowDown,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ButtonArrowNext,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ButtonArrowPrevious,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ButtonArrowUp,
+ /// A dual toolbar button (e.g., a Back button with a dropdown)
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Dualbutton,
+ /// Menu Popup background.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Menupopup,
+ /// The meter bar's meter indicator.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Meterchunk,
+ /// The "arrowed" part of the dropdown button that open up a dropdown list.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMenulistArrowButton,
+ /// For HTML's <input type=number>
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ NumberInput,
+ /// The progress bar's progress indicator
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Progresschunk,
+ /// nsRangeFrame and its subparts
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Range,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ RangeThumb,
+ /// The scrollbar slider
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarHorizontal,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarVertical,
+ /// A scrollbar button (up/down/left/right).
+ /// Keep these in order (some code casts these values to `int` in order to
+ /// compare them against each other).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarbuttonUp,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarbuttonDown,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarbuttonLeft,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarbuttonRight,
+ /// The scrollbar thumb.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarthumbHorizontal,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarthumbVertical,
+ /// The scrollbar track.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbartrackHorizontal,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbartrackVertical,
+ /// The scroll corner
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Scrollcorner,
+ /// A separator. Can be horizontal or vertical.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Separator,
+ /// A spin control (up/down control for time/date pickers).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Spinner,
+ /// The up button of a spin control.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ SpinnerUpbutton,
+ /// The down button of a spin control.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ SpinnerDownbutton,
+ /// The textfield of a spin control
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ SpinnerTextfield,
+ /// A splitter. Can be horizontal or vertical.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Splitter,
+ /// A status bar in a main application window.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Statusbar,
+ /// A single tab in a tab widget.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Tab,
+ /// A single pane (inside the tabpanels container).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Tabpanel,
+ /// The tab panels container.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Tabpanels,
+ /// The tabs scroll arrows (left/right).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ TabScrollArrowBack,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ TabScrollArrowForward,
+ /// A toolbar in an application window.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Toolbar,
+ /// A single toolbar button (with no associated dropdown).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Toolbarbutton,
+ /// The dropdown portion of a toolbar button
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ToolbarbuttonDropdown,
+ /// The toolbox that contains the toolbars.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Toolbox,
+ /// A tooltip.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Tooltip,
+ /// A listbox or tree widget header
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeheader,
+ /// An individual header cell
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeheadercell,
+ /// A tree item.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeitem,
+ /// A tree widget branch line
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeline,
+ /// A tree widget twisty.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treetwisty,
+ /// Open tree widget twisty.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treetwistyopen,
+ /// A tree widget.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeview,
+
+ /// Mac help button.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacHelpButton,
+
+ /// An appearance value for the root, so that we can get unified toolbar looks (which require a
+ /// transparent gecko background) without really using the whole transparency set-up which
+ /// otherwise loses window borders, see bug 1870481.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacUnifiedToolbarWindow,
+
+ /// Windows themed window frame elements.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonBox,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonClose,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonMaximize,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonMinimize,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonRestore,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowTitlebar,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowTitlebarMaximized,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowDecorations,
+
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacDisclosureButtonClosed,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacDisclosureButtonOpen,
+
+ /// A themed focus outline (for outline:auto).
+ ///
+ /// This isn't exposed to CSS at all, just here for convenience.
+ #[css(skip)]
+ FocusOutline,
+
+ /// A dummy variant that should be last to let the GTK widget do hackery.
+ #[css(skip)]
+ Count,
+}
+
+/// A kind of break between two boxes.
+///
+/// https://drafts.csswg.org/css-break/#break-between
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum BreakBetween {
+ Always,
+ Auto,
+ Page,
+ Avoid,
+ Left,
+ Right,
+}
+
+impl BreakBetween {
+ /// Parse a legacy break-between value for `page-break-{before,after}`.
+ ///
+ /// See https://drafts.csswg.org/css-break/#page-break-properties.
+ #[inline]
+ pub(crate) fn parse_legacy<'i>(
+ _: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Self, ParseError<'i>> {
+ let break_value = BreakBetween::parse(input)?;
+ match break_value {
+ BreakBetween::Always => Ok(BreakBetween::Page),
+ BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => {
+ Ok(break_value)
+ },
+ BreakBetween::Page => {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ }
+ }
+
+ /// Serialize a legacy break-between value for `page-break-*`.
+ ///
+ /// See https://drafts.csswg.org/css-break/#page-break-properties.
+ pub(crate) fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => {
+ self.to_css(dest)
+ },
+ BreakBetween::Page => dest.write_str("always"),
+ BreakBetween::Always => Ok(()),
+ }
+ }
+}
+
+/// A kind of break within a box.
+///
+/// https://drafts.csswg.org/css-break/#break-within
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum BreakWithin {
+ Auto,
+ Avoid,
+ AvoidPage,
+ AvoidColumn,
+}
+
+impl BreakWithin {
+ /// Parse a legacy break-between value for `page-break-inside`.
+ ///
+ /// See https://drafts.csswg.org/css-break/#page-break-properties.
+ #[inline]
+ pub(crate) fn parse_legacy<'i>(
+ _: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Self, ParseError<'i>> {
+ let break_value = BreakWithin::parse(input)?;
+ match break_value {
+ BreakWithin::Auto | BreakWithin::Avoid => Ok(break_value),
+ BreakWithin::AvoidPage | BreakWithin::AvoidColumn => {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ }
+ }
+
+ /// Serialize a legacy break-between value for `page-break-inside`.
+ ///
+ /// See https://drafts.csswg.org/css-break/#page-break-properties.
+ pub(crate) fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ BreakWithin::Auto | BreakWithin::Avoid => self.to_css(dest),
+ BreakWithin::AvoidPage | BreakWithin::AvoidColumn => Ok(()),
+ }
+ }
+}
+
+/// The value for the `overflow-x` / `overflow-y` properties.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum Overflow {
+ Visible,
+ Hidden,
+ Scroll,
+ Auto,
+ #[cfg(feature = "gecko")]
+ Clip,
+}
+
+// This can be derived once we remove or keep `-moz-hidden-unscrollable`
+// indefinitely.
+impl Parse for Overflow {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "visible" => Self::Visible,
+ "hidden" => Self::Hidden,
+ "scroll" => Self::Scroll,
+ "auto" | "overlay" => Self::Auto,
+ #[cfg(feature = "gecko")]
+ "clip" => Self::Clip,
+ #[cfg(feature = "gecko")]
+ "-moz-hidden-unscrollable" if static_prefs::pref!("layout.css.overflow-moz-hidden-unscrollable.enabled") => {
+ Overflow::Clip
+ },
+ })
+ }
+}
+
+impl Overflow {
+ /// Return true if the value will create a scrollable box.
+ #[inline]
+ pub fn is_scrollable(&self) -> bool {
+ matches!(*self, Self::Hidden | Self::Scroll | Self::Auto)
+ }
+ /// Convert the value to a scrollable value if it's not already scrollable.
+ /// This maps `visible` to `auto` and `clip` to `hidden`.
+ #[inline]
+ pub fn to_scrollable(&self) -> Self {
+ match *self {
+ Self::Hidden | Self::Scroll | Self::Auto => *self,
+ Self::Visible => Self::Auto,
+ #[cfg(feature = "gecko")]
+ Self::Clip => Self::Hidden,
+ }
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[css(bitflags(
+ single = "auto",
+ mixed = "stable,both-edges",
+ validate_mixed = "Self::has_stable"
+))]
+/// Values for scrollbar-gutter:
+/// <https://drafts.csswg.org/css-overflow-3/#scrollbar-gutter-property>
+pub struct ScrollbarGutter(u8);
+bitflags! {
+ impl ScrollbarGutter: u8 {
+ /// `auto` variant. Just for convenience if there is no flag set.
+ const AUTO = 0;
+ /// `stable` variant.
+ const STABLE = 1 << 0;
+ /// `both-edges` variant.
+ const BOTH_EDGES = 1 << 1;
+ }
+}
+
+impl ScrollbarGutter {
+ #[inline]
+ fn has_stable(&self) -> bool {
+ self.intersects(Self::STABLE)
+ }
+}
+
+/// A specified value for the zoom property.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum Zoom {
+ Normal,
+ /// An internal value that resets the effective zoom to 1. Used for scrollbar parts, which
+ /// disregard zoom. We use this name because WebKit has this value exposed to the web.
+ #[parse(condition = "ParserContext::in_ua_sheet")]
+ Document,
+ Value(NonNegativeNumberOrPercentage),
+}
+
+impl Zoom {
+ /// Return a particular number value of the zoom property.
+ #[inline]
+ pub fn new_number(n: f32) -> Self {
+ Self::Value(NonNegativeNumberOrPercentage::new_number(n))
+ }
+}
diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs
new file mode 100644
index 0000000000..2660864319
--- /dev/null
+++ b/servo/components/style/values/specified/calc.rs
@@ -0,0 +1,1086 @@
+/* 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/. */
+
+//! [Calc expressions][calc].
+//!
+//! [calc]: https://drafts.csswg.org/css-values/#calc-notation
+
+use crate::color::parsing::{AngleOrNumber, NumberOrPercentage};
+use crate::parser::ParserContext;
+use crate::values::generics::calc::{
+ self as generic, CalcNodeLeaf, CalcUnits, MinMaxOp, ModRemOp, PositivePercentageBasis,
+ RoundingStrategy, SortKey,
+};
+use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength};
+use crate::values::specified::length::{ContainerRelativeLength, ViewportPercentageLength};
+use crate::values::specified::{self, Angle, Resolution, Time};
+use crate::values::{serialize_number, serialize_percentage, CSSFloat, CSSInteger};
+use cssparser::{CowRcStr, Parser, Token};
+use smallvec::SmallVec;
+use std::cmp;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+/// The name of the mathematical function that we're parsing.
+#[derive(Clone, Copy, Debug, Parse)]
+pub enum MathFunction {
+ /// `calc()`: https://drafts.csswg.org/css-values-4/#funcdef-calc
+ Calc,
+ /// `min()`: https://drafts.csswg.org/css-values-4/#funcdef-min
+ Min,
+ /// `max()`: https://drafts.csswg.org/css-values-4/#funcdef-max
+ Max,
+ /// `clamp()`: https://drafts.csswg.org/css-values-4/#funcdef-clamp
+ Clamp,
+ /// `round()`: https://drafts.csswg.org/css-values-4/#funcdef-round
+ Round,
+ /// `mod()`: https://drafts.csswg.org/css-values-4/#funcdef-mod
+ Mod,
+ /// `rem()`: https://drafts.csswg.org/css-values-4/#funcdef-rem
+ Rem,
+ /// `sin()`: https://drafts.csswg.org/css-values-4/#funcdef-sin
+ Sin,
+ /// `cos()`: https://drafts.csswg.org/css-values-4/#funcdef-cos
+ Cos,
+ /// `tan()`: https://drafts.csswg.org/css-values-4/#funcdef-tan
+ Tan,
+ /// `asin()`: https://drafts.csswg.org/css-values-4/#funcdef-asin
+ Asin,
+ /// `acos()`: https://drafts.csswg.org/css-values-4/#funcdef-acos
+ Acos,
+ /// `atan()`: https://drafts.csswg.org/css-values-4/#funcdef-atan
+ Atan,
+ /// `atan2()`: https://drafts.csswg.org/css-values-4/#funcdef-atan2
+ Atan2,
+ /// `pow()`: https://drafts.csswg.org/css-values-4/#funcdef-pow
+ Pow,
+ /// `sqrt()`: https://drafts.csswg.org/css-values-4/#funcdef-sqrt
+ Sqrt,
+ /// `hypot()`: https://drafts.csswg.org/css-values-4/#funcdef-hypot
+ Hypot,
+ /// `log()`: https://drafts.csswg.org/css-values-4/#funcdef-log
+ Log,
+ /// `exp()`: https://drafts.csswg.org/css-values-4/#funcdef-exp
+ Exp,
+ /// `abs()`: https://drafts.csswg.org/css-values-4/#funcdef-abs
+ Abs,
+ /// `sign()`: https://drafts.csswg.org/css-values-4/#funcdef-sign
+ Sign,
+}
+
+/// A leaf node inside a `Calc` expression's AST.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum Leaf {
+ /// `<length>`
+ Length(NoCalcLength),
+ /// `<angle>`
+ Angle(Angle),
+ /// `<time>`
+ Time(Time),
+ /// `<resolution>`
+ Resolution(Resolution),
+ /// `<percentage>`
+ Percentage(CSSFloat),
+ /// `<number>`
+ Number(CSSFloat),
+}
+
+impl Leaf {
+ fn as_length(&self) -> Option<&NoCalcLength> {
+ match *self {
+ Self::Length(ref l) => Some(l),
+ _ => None,
+ }
+ }
+}
+
+impl ToCss for Leaf {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Self::Length(ref l) => l.to_css(dest),
+ Self::Number(n) => serialize_number(n, /* was_calc = */ false, dest),
+ Self::Resolution(ref r) => r.to_css(dest),
+ Self::Percentage(p) => serialize_percentage(p, dest),
+ Self::Angle(ref a) => a.to_css(dest),
+ Self::Time(ref t) => t.to_css(dest),
+ }
+ }
+}
+
+/// A struct to hold a simplified `<length>` or `<percentage>` expression.
+///
+/// In some cases, e.g. DOMMatrix, we support calc(), but reject all the
+/// relative lengths, and to_computed_pixel_length_without_context() handles
+/// this case. Therefore, if you want to add a new field, please make sure this
+/// function work properly.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+#[allow(missing_docs)]
+pub struct CalcLengthPercentage {
+ #[css(skip)]
+ pub clamping_mode: AllowedNumericType,
+ pub node: CalcNode,
+}
+
+impl CalcLengthPercentage {
+ fn same_unit_length_as(a: &Self, b: &Self) -> Option<(CSSFloat, CSSFloat)> {
+ debug_assert_eq!(a.clamping_mode, b.clamping_mode);
+ debug_assert_eq!(a.clamping_mode, AllowedNumericType::All);
+
+ let a = a.node.as_leaf()?;
+ let b = b.node.as_leaf()?;
+
+ if a.sort_key() != b.sort_key() {
+ return None;
+ }
+
+ let a = a.as_length()?.unitless_value();
+ let b = b.as_length()?.unitless_value();
+ return Some((a, b));
+ }
+}
+
+impl SpecifiedValueInfo for CalcLengthPercentage {}
+
+impl generic::CalcNodeLeaf for Leaf {
+ fn unit(&self) -> CalcUnits {
+ match self {
+ Leaf::Length(_) => CalcUnits::LENGTH,
+ Leaf::Angle(_) => CalcUnits::ANGLE,
+ Leaf::Time(_) => CalcUnits::TIME,
+ Leaf::Resolution(_) => CalcUnits::RESOLUTION,
+ Leaf::Percentage(_) => CalcUnits::PERCENTAGE,
+ Leaf::Number(_) => CalcUnits::empty(),
+ }
+ }
+
+ fn unitless_value(&self) -> f32 {
+ match *self {
+ Self::Length(ref l) => l.unitless_value(),
+ Self::Percentage(n) | Self::Number(n) => n,
+ Self::Resolution(ref r) => r.dppx(),
+ Self::Angle(ref a) => a.degrees(),
+ Self::Time(ref t) => t.seconds(),
+ }
+ }
+
+ fn new_number(value: f32) -> Self {
+ Self::Number(value)
+ }
+
+ fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option<cmp::Ordering> {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) {
+ return None;
+ }
+
+ let self_negative = self.is_negative();
+ if self_negative != other.is_negative() {
+ return Some(if self_negative { cmp::Ordering::Less } else { cmp::Ordering::Greater });
+ }
+
+ match (self, other) {
+ (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other),
+ (&Length(ref one), &Length(ref other)) => one.partial_cmp(other),
+ (&Angle(ref one), &Angle(ref other)) => one.degrees().partial_cmp(&other.degrees()),
+ (&Time(ref one), &Time(ref other)) => one.seconds().partial_cmp(&other.seconds()),
+ (&Resolution(ref one), &Resolution(ref other)) => one.dppx().partial_cmp(&other.dppx()),
+ (&Number(ref one), &Number(ref other)) => one.partial_cmp(other),
+ _ => {
+ match *self {
+ Length(..) | Percentage(..) | Angle(..) | Time(..) | Number(..) |
+ Resolution(..) => {},
+ }
+ unsafe {
+ debug_unreachable!("Forgot a branch?");
+ }
+ },
+ }
+ }
+
+ fn as_number(&self) -> Option<f32> {
+ match *self {
+ Leaf::Length(_) |
+ Leaf::Angle(_) |
+ Leaf::Time(_) |
+ Leaf::Resolution(_) |
+ Leaf::Percentage(_) => None,
+ Leaf::Number(value) => Some(value),
+ }
+ }
+
+ fn sort_key(&self) -> SortKey {
+ match *self {
+ Self::Number(..) => SortKey::Number,
+ Self::Percentage(..) => SortKey::Percentage,
+ Self::Time(..) => SortKey::Sec,
+ Self::Resolution(..) => SortKey::Dppx,
+ Self::Angle(..) => SortKey::Deg,
+ Self::Length(ref l) => match *l {
+ NoCalcLength::Absolute(..) => SortKey::Px,
+ NoCalcLength::FontRelative(ref relative) => match *relative {
+ FontRelativeLength::Ch(..) => SortKey::Ch,
+ FontRelativeLength::Em(..) => SortKey::Em,
+ FontRelativeLength::Ex(..) => SortKey::Ex,
+ FontRelativeLength::Cap(..) => SortKey::Cap,
+ FontRelativeLength::Ic(..) => SortKey::Ic,
+ FontRelativeLength::Rem(..) => SortKey::Rem,
+ FontRelativeLength::Lh(..) => SortKey::Lh,
+ FontRelativeLength::Rlh(..) => SortKey::Rlh,
+ },
+ NoCalcLength::ViewportPercentage(ref vp) => match *vp {
+ ViewportPercentageLength::Vh(..) => SortKey::Vh,
+ ViewportPercentageLength::Svh(..) => SortKey::Svh,
+ ViewportPercentageLength::Lvh(..) => SortKey::Lvh,
+ ViewportPercentageLength::Dvh(..) => SortKey::Dvh,
+ ViewportPercentageLength::Vw(..) => SortKey::Vw,
+ ViewportPercentageLength::Svw(..) => SortKey::Svw,
+ ViewportPercentageLength::Lvw(..) => SortKey::Lvw,
+ ViewportPercentageLength::Dvw(..) => SortKey::Dvw,
+ ViewportPercentageLength::Vmax(..) => SortKey::Vmax,
+ ViewportPercentageLength::Svmax(..) => SortKey::Svmax,
+ ViewportPercentageLength::Lvmax(..) => SortKey::Lvmax,
+ ViewportPercentageLength::Dvmax(..) => SortKey::Dvmax,
+ ViewportPercentageLength::Vmin(..) => SortKey::Vmin,
+ ViewportPercentageLength::Svmin(..) => SortKey::Svmin,
+ ViewportPercentageLength::Lvmin(..) => SortKey::Lvmin,
+ ViewportPercentageLength::Dvmin(..) => SortKey::Dvmin,
+ ViewportPercentageLength::Vb(..) => SortKey::Vb,
+ ViewportPercentageLength::Svb(..) => SortKey::Svb,
+ ViewportPercentageLength::Lvb(..) => SortKey::Lvb,
+ ViewportPercentageLength::Dvb(..) => SortKey::Dvb,
+ ViewportPercentageLength::Vi(..) => SortKey::Vi,
+ ViewportPercentageLength::Svi(..) => SortKey::Svi,
+ ViewportPercentageLength::Lvi(..) => SortKey::Lvi,
+ ViewportPercentageLength::Dvi(..) => SortKey::Dvi,
+ },
+ NoCalcLength::ContainerRelative(ref cq) => match *cq {
+ ContainerRelativeLength::Cqw(..) => SortKey::Cqw,
+ ContainerRelativeLength::Cqh(..) => SortKey::Cqh,
+ ContainerRelativeLength::Cqi(..) => SortKey::Cqi,
+ ContainerRelativeLength::Cqb(..) => SortKey::Cqb,
+ ContainerRelativeLength::Cqmin(..) => SortKey::Cqmin,
+ ContainerRelativeLength::Cqmax(..) => SortKey::Cqmax,
+ },
+ NoCalcLength::ServoCharacterWidth(..) => unreachable!(),
+ },
+ }
+ }
+
+ fn simplify(&mut self) {
+ if let Self::Length(NoCalcLength::Absolute(ref mut abs)) = *self {
+ *abs = AbsoluteLength::Px(abs.to_px());
+ }
+ }
+
+ /// Tries to merge one sum to another, that is, perform `x` + `y`.
+ ///
+ /// Only handles leaf nodes, it's the caller's responsibility to simplify
+ /// them before calling this if needed.
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ match (self, other) {
+ (&mut Number(ref mut one), &Number(ref other)) |
+ (&mut Percentage(ref mut one), &Percentage(ref other)) => {
+ *one += *other;
+ },
+ (&mut Angle(ref mut one), &Angle(ref other)) => {
+ *one = specified::Angle::from_calc(one.degrees() + other.degrees());
+ },
+ (&mut Time(ref mut one), &Time(ref other)) => {
+ *one = specified::Time::from_seconds(one.seconds() + other.seconds());
+ },
+ (&mut Resolution(ref mut one), &Resolution(ref other)) => {
+ *one = specified::Resolution::from_dppx(one.dppx() + other.dppx());
+ },
+ (&mut Length(ref mut one), &Length(ref other)) => {
+ *one = one.try_op(other, std::ops::Add::add)?;
+ },
+ _ => {
+ match *other {
+ Number(..) | Percentage(..) | Angle(..) | Time(..) | Resolution(..) |
+ Length(..) => {},
+ }
+ unsafe {
+ debug_unreachable!();
+ }
+ },
+ }
+
+ Ok(())
+ }
+
+ fn try_product_in_place(&mut self, other: &mut Self) -> bool {
+ if let Self::Number(ref mut left) = *self {
+ if let Self::Number(ref right) = *other {
+ // Both sides are numbers, so we can just modify the left side.
+ *left *= *right;
+ true
+ } else {
+ // The right side is not a number, so the result should be in the units of the right
+ // side.
+ other.map(|v| v * *left);
+ std::mem::swap(self, other);
+ true
+ }
+ } else if let Self::Number(ref right) = *other {
+ // The left side is not a number, but the right side is, so the result is the left
+ // side unit.
+ self.map(|v| v * *right);
+ true
+ } else {
+ // Neither side is a number, so a product is not possible.
+ false
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ match (self, other) {
+ (&Number(one), &Number(other)) => {
+ return Ok(Leaf::Number(op(one, other)));
+ },
+ (&Percentage(one), &Percentage(other)) => {
+ return Ok(Leaf::Percentage(op(one, other)));
+ },
+ (&Angle(ref one), &Angle(ref other)) => {
+ return Ok(Leaf::Angle(specified::Angle::from_calc(op(
+ one.degrees(),
+ other.degrees(),
+ ))));
+ },
+ (&Resolution(ref one), &Resolution(ref other)) => {
+ return Ok(Leaf::Resolution(specified::Resolution::from_dppx(op(
+ one.dppx(),
+ other.dppx(),
+ ))));
+ },
+ (&Time(ref one), &Time(ref other)) => {
+ return Ok(Leaf::Time(specified::Time::from_seconds(op(
+ one.seconds(),
+ other.seconds(),
+ ))));
+ },
+ (&Length(ref one), &Length(ref other)) => {
+ return Ok(Leaf::Length(one.try_op(other, op)?));
+ },
+ _ => {
+ match *other {
+ Number(..) | Percentage(..) | Angle(..) | Time(..) | Length(..) |
+ Resolution(..) => {},
+ }
+ unsafe {
+ debug_unreachable!();
+ }
+ },
+ }
+ }
+
+ fn map(&mut self, mut op: impl FnMut(f32) -> f32) {
+ match self {
+ Leaf::Length(one) => *one = one.map(op),
+ Leaf::Angle(one) => *one = specified::Angle::from_calc(op(one.degrees())),
+ Leaf::Time(one) => *one = specified::Time::from_seconds(op(one.seconds())),
+ Leaf::Resolution(one) => *one = specified::Resolution::from_dppx(op(one.dppx())),
+ Leaf::Percentage(one) => *one = op(*one),
+ Leaf::Number(one) => *one = op(*one),
+ }
+ }
+}
+
+/// A calc node representation for specified values.
+pub type CalcNode = generic::GenericCalcNode<Leaf>;
+
+impl CalcNode {
+ /// Tries to parse a single element in the expression, that is, a
+ /// `<length>`, `<angle>`, `<time>`, `<percentage>`, `<resolution>`, etc.
+ ///
+ /// May return a "complex" `CalcNode`, in the presence of a parenthesized
+ /// expression, for example.
+ fn parse_one<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match input.next()? {
+ &Token::Number { value, .. } => Ok(CalcNode::Leaf(Leaf::Number(value))),
+ &Token::Dimension {
+ value, ref unit, ..
+ } => {
+ if allowed_units.intersects(CalcUnits::LENGTH) {
+ if let Ok(l) = NoCalcLength::parse_dimension(context, value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Length(l)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::ANGLE) {
+ if let Ok(a) = Angle::parse_dimension(value, unit, /* from_calc = */ true) {
+ return Ok(CalcNode::Leaf(Leaf::Angle(a)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::TIME) {
+ if let Ok(t) = Time::parse_dimension(value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Time(t)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::RESOLUTION) {
+ if let Ok(t) = Resolution::parse_dimension(value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Resolution(t)));
+ }
+ }
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ },
+ &Token::Percentage { unit_value, .. }
+ if allowed_units.intersects(CalcUnits::PERCENTAGE) =>
+ {
+ Ok(CalcNode::Leaf(Leaf::Percentage(unit_value)))
+ },
+ &Token::ParenthesisBlock => input.parse_nested_block(|input| {
+ CalcNode::parse_argument(context, input, allowed_units)
+ }),
+ &Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse(context, input, function, allowed_units)
+ },
+ &Token::Ident(ref ident) => {
+ let number = match_ignore_ascii_case! { &**ident,
+ "e" => std::f32::consts::E,
+ "pi" => std::f32::consts::PI,
+ "infinity" => f32::INFINITY,
+ "-infinity" => f32::NEG_INFINITY,
+ "nan" => f32::NAN,
+ _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))),
+ };
+ Ok(CalcNode::Leaf(Leaf::Number(number)))
+ },
+ t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+
+ /// Parse a top-level `calc` expression, with all nested sub-expressions.
+ ///
+ /// This is in charge of parsing, for example, `2 + 3 * 100%`.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ input.parse_nested_block(|input| {
+ match function {
+ MathFunction::Calc => Self::parse_argument(context, input, allowed_units),
+ MathFunction::Clamp => {
+ let min = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let center = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let max = Self::parse_argument(context, input, allowed_units)?;
+ Ok(Self::Clamp {
+ min: Box::new(min),
+ center: Box::new(center),
+ max: Box::new(max),
+ })
+ },
+ MathFunction::Round => {
+ let strategy = input.try_parse(parse_rounding_strategy);
+
+ // <rounding-strategy> = nearest | up | down | to-zero
+ // https://drafts.csswg.org/css-values-4/#calc-syntax
+ fn parse_rounding_strategy<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ ) -> Result<RoundingStrategy, ParseError<'i>> {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "nearest" => RoundingStrategy::Nearest,
+ "up" => RoundingStrategy::Up,
+ "down" => RoundingStrategy::Down,
+ "to-zero" => RoundingStrategy::ToZero,
+ })
+ }
+
+ if strategy.is_ok() {
+ input.expect_comma()?;
+ }
+
+ let value = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let step = Self::parse_argument(context, input, allowed_units)?;
+
+ Ok(Self::Round {
+ strategy: strategy.unwrap_or(RoundingStrategy::Nearest),
+ value: Box::new(value),
+ step: Box::new(step),
+ })
+ },
+ MathFunction::Mod | MathFunction::Rem => {
+ let dividend = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let divisor = Self::parse_argument(context, input, allowed_units)?;
+
+ let op = match function {
+ MathFunction::Mod => ModRemOp::Mod,
+ MathFunction::Rem => ModRemOp::Rem,
+ _ => unreachable!(),
+ };
+ Ok(Self::ModRem {
+ dividend: Box::new(dividend),
+ divisor: Box::new(divisor),
+ op,
+ })
+ },
+ MathFunction::Min | MathFunction::Max => {
+ // TODO(emilio): The common case for parse_comma_separated
+ // is just one element, but for min / max is two, really...
+ //
+ // Consider adding an API to cssparser to specify the
+ // initial vector capacity?
+ let arguments = input.parse_comma_separated(|input| {
+ Self::parse_argument(context, input, allowed_units)
+ })?;
+
+ let op = match function {
+ MathFunction::Min => MinMaxOp::Min,
+ MathFunction::Max => MinMaxOp::Max,
+ _ => unreachable!(),
+ };
+
+ Ok(Self::MinMax(arguments.into(), op))
+ },
+ MathFunction::Sin | MathFunction::Cos | MathFunction::Tan => {
+ let a = Self::parse_angle_argument(context, input)?;
+
+ let number = match function {
+ MathFunction::Sin => a.sin(),
+ MathFunction::Cos => a.cos(),
+ MathFunction::Tan => a.tan(),
+ _ => unsafe {
+ debug_unreachable!("We just checked!");
+ },
+ };
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Asin | MathFunction::Acos | MathFunction::Atan => {
+ let a = Self::parse_number_argument(context, input)?;
+
+ let radians = match function {
+ MathFunction::Asin => a.asin(),
+ MathFunction::Acos => a.acos(),
+ MathFunction::Atan => a.atan(),
+ _ => unsafe {
+ debug_unreachable!("We just checked!");
+ },
+ };
+
+ Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians))))
+ },
+ MathFunction::Atan2 => {
+ let a = Self::parse_argument(context, input, CalcUnits::ALL)?;
+ input.expect_comma()?;
+ let b = Self::parse_argument(context, input, CalcUnits::ALL)?;
+
+ let radians = Self::try_resolve(input, || {
+ if let Ok(a) = a.to_number() {
+ let b = b.to_number()?;
+ return Ok(a.atan2(b));
+ }
+
+ if let Ok(a) = a.to_percentage() {
+ let b = b.to_percentage()?;
+ return Ok(a.atan2(b));
+ }
+
+ if let Ok(a) = a.to_time(None) {
+ let b = b.to_time(None)?;
+ return Ok(a.seconds().atan2(b.seconds()));
+ }
+
+ if let Ok(a) = a.to_angle() {
+ let b = b.to_angle()?;
+ return Ok(a.radians().atan2(b.radians()));
+ }
+
+ if let Ok(a) = a.to_resolution() {
+ let b = b.to_resolution()?;
+ return Ok(a.dppx().atan2(b.dppx()));
+ }
+
+ let a = a.into_length_or_percentage(AllowedNumericType::All)?;
+ let b = b.into_length_or_percentage(AllowedNumericType::All)?;
+ let (a, b) = CalcLengthPercentage::same_unit_length_as(&a, &b).ok_or(())?;
+
+ Ok(a.atan2(b))
+ })?;
+
+ Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians))))
+ },
+ MathFunction::Pow => {
+ let a = Self::parse_number_argument(context, input)?;
+ input.expect_comma()?;
+ let b = Self::parse_number_argument(context, input)?;
+
+ let number = a.powf(b);
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Sqrt => {
+ let a = Self::parse_number_argument(context, input)?;
+
+ let number = a.sqrt();
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Hypot => {
+ let arguments = input.parse_comma_separated(|input| {
+ Self::parse_argument(context, input, allowed_units)
+ })?;
+
+ Ok(Self::Hypot(arguments.into()))
+ },
+ MathFunction::Log => {
+ let a = Self::parse_number_argument(context, input)?;
+ let b = input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ Self::parse_number_argument(context, input)
+ })
+ .ok();
+
+ let number = match b {
+ Some(b) => a.log(b),
+ None => a.ln(),
+ };
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Exp => {
+ let a = Self::parse_number_argument(context, input)?;
+ let number = a.exp();
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Abs => {
+ let node = Self::parse_argument(context, input, allowed_units)?;
+ Ok(Self::Abs(Box::new(node)))
+ },
+ MathFunction::Sign => {
+ // The sign of a percentage is dependent on the percentage basis, so if
+ // percentages aren't allowed (so there's no basis) we shouldn't allow them in
+ // sign(). The rest of the units are safe tho.
+ let sign_units = allowed_units | (CalcUnits::ALL - CalcUnits::PERCENTAGE);
+ let node = Self::parse_argument(context, input, sign_units)?;
+ Ok(Self::Sign(Box::new(node)))
+ },
+ }
+ })
+ }
+
+ fn parse_angle_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ let argument = Self::parse_argument(context, input, CalcUnits::ANGLE)?;
+ argument
+ .to_number()
+ .or_else(|()| Ok(argument.to_angle()?.radians()))
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ fn parse_number_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse_argument(context, input, CalcUnits::empty())?
+ .to_number()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ fn parse_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut sum = SmallVec::<[CalcNode; 1]>::new();
+ sum.push(Self::parse_product(context, input, allowed_units)?);
+
+ loop {
+ let start = input.state();
+ match input.next_including_whitespace() {
+ Ok(&Token::WhiteSpace(_)) => {
+ if input.is_exhausted() {
+ break; // allow trailing whitespace
+ }
+ match *input.next()? {
+ Token::Delim('+') => {
+ sum.push(Self::parse_product(context, input, allowed_units)?);
+ },
+ Token::Delim('-') => {
+ let mut rhs = Self::parse_product(context, input, allowed_units)?;
+ rhs.negate();
+ sum.push(rhs);
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ }
+
+ Ok(if sum.len() == 1 {
+ sum.drain(..).next().unwrap()
+ } else {
+ Self::Sum(sum.into_boxed_slice().into())
+ })
+ }
+
+ /// Parse a top-level `calc` expression, and all the products that may
+ /// follow, and stop as soon as a non-product expression is found.
+ ///
+ /// This should parse correctly:
+ ///
+ /// * `2`
+ /// * `2 * 2`
+ /// * `2 * 2 + 2` (but will leave the `+ 2` unparsed).
+ ///
+ fn parse_product<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut product = SmallVec::<[CalcNode; 1]>::new();
+ product.push(Self::parse_one(context, input, allowed_units)?);
+
+ loop {
+ let start = input.state();
+ match input.next() {
+ Ok(&Token::Delim('*')) => {
+ let mut rhs = Self::parse_one(context, input, allowed_units)?;
+
+ // We can unwrap here, becuase we start the function by adding a node to
+ // the list.
+ if !product.last_mut().unwrap().try_product_in_place(&mut rhs) {
+ product.push(rhs);
+ }
+ },
+ Ok(&Token::Delim('/')) => {
+ let rhs = Self::parse_one(context, input, allowed_units)?;
+
+ enum InPlaceDivisionResult {
+ /// The right was merged into the left.
+ Merged,
+ /// The right is not a number or could not be resolved, so the left is
+ /// unchanged.
+ Unchanged,
+ /// The right was resolved, but was not a number, so the calculation is
+ /// invalid.
+ Invalid,
+ }
+
+ fn try_division_in_place(
+ left: &mut CalcNode,
+ right: &CalcNode,
+ ) -> InPlaceDivisionResult {
+ if let Ok(resolved) = right.resolve() {
+ if let Some(number) = resolved.as_number() {
+ if number != 1.0 && left.is_product_distributive() {
+ left.map(|l| l / number);
+ return InPlaceDivisionResult::Merged;
+ }
+ } else {
+ return InPlaceDivisionResult::Invalid;
+ }
+ }
+ InPlaceDivisionResult::Unchanged
+ }
+
+ // The right hand side of a division *must* be a number, so if we can
+ // already resolve it, then merge it with the last node on the product list.
+ // We can unwrap here, becuase we start the function by adding a node to
+ // the list.
+ match try_division_in_place(product.last_mut().unwrap(), &rhs) {
+ InPlaceDivisionResult::Merged => {},
+ InPlaceDivisionResult::Unchanged => {
+ product.push(Self::Invert(Box::new(rhs)))
+ },
+ InPlaceDivisionResult::Invalid => {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ )
+ },
+ }
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ }
+
+ Ok(if product.len() == 1 {
+ product.drain(..).next().unwrap()
+ } else {
+ Self::Product(product.into_boxed_slice().into())
+ })
+ }
+
+ fn try_resolve<'i, 't, F>(
+ input: &Parser<'i, 't>,
+ closure: F,
+ ) -> Result<CSSFloat, ParseError<'i>>
+ where
+ F: FnOnce() -> Result<CSSFloat, ()>,
+ {
+ closure().map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Tries to simplify this expression into a `<length>` or `<percentage>`
+ /// value.
+ pub fn into_length_or_percentage(
+ mut self,
+ clamping_mode: AllowedNumericType,
+ ) -> Result<CalcLengthPercentage, ()> {
+ self.simplify_and_sort();
+
+ // Although we allow numbers inside CalcLengthPercentage, calculations that resolve to a
+ // number result is still not allowed.
+ let unit = self.unit()?;
+ if !CalcUnits::LENGTH_PERCENTAGE.intersects(unit) {
+ Err(())
+ } else {
+ Ok(CalcLengthPercentage {
+ clamping_mode,
+ node: self,
+ })
+ }
+ }
+
+ /// Tries to simplify this expression into a `<time>` value.
+ fn to_time(&self, clamping_mode: Option<AllowedNumericType>) -> Result<Time, ()> {
+ let seconds = if let Leaf::Time(time) = self.resolve()? {
+ time.seconds()
+ } else {
+ return Err(());
+ };
+
+ Ok(Time::from_seconds_with_calc_clamping_mode(
+ seconds,
+ clamping_mode,
+ ))
+ }
+
+ /// Tries to simplify the expression into a `<resolution>` value.
+ fn to_resolution(&self) -> Result<Resolution, ()> {
+ let dppx = if let Leaf::Resolution(resolution) = self.resolve()? {
+ resolution.dppx()
+ } else {
+ return Err(());
+ };
+
+ Ok(Resolution::from_dppx_calc(dppx))
+ }
+
+ /// Tries to simplify this expression into an `Angle` value.
+ fn to_angle(&self) -> Result<Angle, ()> {
+ let degrees = if let Leaf::Angle(angle) = self.resolve()? {
+ angle.degrees()
+ } else {
+ return Err(());
+ };
+
+ let result = Angle::from_calc(degrees);
+ Ok(result)
+ }
+
+ /// Tries to simplify this expression into a `<number>` value.
+ fn to_number(&self) -> Result<CSSFloat, ()> {
+ let number = if let Leaf::Number(number) = self.resolve()? {
+ number
+ } else {
+ return Err(());
+ };
+
+ let result = number;
+
+ Ok(result)
+ }
+
+ /// Tries to simplify this expression into a `<percentage>` value.
+ fn to_percentage(&self) -> Result<CSSFloat, ()> {
+ if let Leaf::Percentage(percentage) = self.resolve()? {
+ Ok(percentage)
+ } else {
+ Err(())
+ }
+ }
+
+ /// Given a function name, and the location from where the token came from,
+ /// return a mathematical function corresponding to that name or an error.
+ #[inline]
+ pub fn math_function<'i>(
+ _: &ParserContext,
+ name: &CowRcStr<'i>,
+ location: cssparser::SourceLocation,
+ ) -> Result<MathFunction, ParseError<'i>> {
+ let function = match MathFunction::from_ident(&*name) {
+ Ok(f) => f,
+ Err(()) => {
+ return Err(location.new_unexpected_token_error(Token::Function(name.clone())))
+ },
+ };
+
+ Ok(function)
+ }
+
+ /// Convenience parsing function for integers.
+ pub fn parse_integer<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSInteger, ParseError<'i>> {
+ Self::parse_number(context, input, function).map(|n| (n + 0.5).floor() as CSSInteger)
+ }
+
+ /// Convenience parsing function for `<length> | <percentage>`.
+ pub fn parse_length_or_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<CalcLengthPercentage, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::LENGTH_PERCENTAGE)?
+ .into_length_or_percentage(clamping_mode)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for percentages.
+ pub fn parse_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::PERCENTAGE)?
+ .to_percentage()
+ .map(crate::values::normalize)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<length>`.
+ pub fn parse_length<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<CalcLengthPercentage, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::LENGTH)?
+ .into_length_or_percentage(clamping_mode)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<number>`.
+ pub fn parse_number<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::empty())?
+ .to_number()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<angle>`.
+ pub fn parse_angle<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<Angle, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::ANGLE)?
+ .to_angle()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<time>`.
+ pub fn parse_time<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<Time, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::TIME)?
+ .to_time(Some(clamping_mode))
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<resolution>`.
+ pub fn parse_resolution<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<Resolution, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::RESOLUTION)?
+ .to_resolution()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<number>` or `<percentage>`.
+ pub fn parse_number_or_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<NumberOrPercentage, ParseError<'i>> {
+ let node = Self::parse(context, input, function, CalcUnits::PERCENTAGE)?;
+
+ if let Ok(value) = node.to_number() {
+ return Ok(NumberOrPercentage::Number { value });
+ }
+
+ match node.to_percentage() {
+ Ok(unit_value) => Ok(NumberOrPercentage::Percentage { unit_value }),
+ Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+
+ /// Convenience parsing function for `<number>` or `<angle>`.
+ pub fn parse_angle_or_number<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<AngleOrNumber, ParseError<'i>> {
+ let node = Self::parse(context, input, function, CalcUnits::ANGLE)?;
+
+ if let Ok(angle) = node.to_angle() {
+ let degrees = angle.degrees();
+ return Ok(AngleOrNumber::Angle { degrees });
+ }
+
+ match node.to_number() {
+ Ok(value) => Ok(AngleOrNumber::Number { value }),
+ Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/color.rs b/servo/components/style/values/specified/color.rs
new file mode 100644
index 0000000000..3a19a2f4a3
--- /dev/null
+++ b/servo/components/style/values/specified/color.rs
@@ -0,0 +1,1175 @@
+/* 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 color values.
+
+use super::AllowQuirks;
+use crate::color::parsing::{
+ self, AngleOrNumber, Color as CSSParserColor, FromParsedColor, NumberOrPercentage,
+};
+use crate::color::{mix::ColorInterpolationMethod, AbsoluteColor, ColorSpace};
+use crate::media_queries::Device;
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue};
+use crate::values::generics::color::{
+ ColorMixFlags, GenericCaretColor, GenericColorMix, GenericColorOrAuto,
+};
+use crate::values::specified::calc::CalcNode;
+use crate::values::specified::Percentage;
+use crate::values::{normalize, CustomIdent};
+use cssparser::{color::PredefinedColorSpace, BasicParseErrorKind, ParseErrorKind, Parser, Token};
+use itoa;
+use std::fmt::{self, Write};
+use std::io::Write as IoWrite;
+use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind};
+use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind};
+
+/// A specified color-mix().
+pub type ColorMix = GenericColorMix<Color, Percentage>;
+
+impl ColorMix {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ preserve_authored: PreserveAuthored,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("color-mix")?;
+
+ input.parse_nested_block(|input| {
+ let interpolation = ColorInterpolationMethod::parse(context, input)?;
+ input.expect_comma()?;
+
+ let try_parse_percentage = |input: &mut Parser| -> Option<Percentage> {
+ input
+ .try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input))
+ .ok()
+ };
+
+ let mut left_percentage = try_parse_percentage(input);
+
+ let left = Color::parse_internal(context, input, preserve_authored)?;
+ if left_percentage.is_none() {
+ left_percentage = try_parse_percentage(input);
+ }
+
+ input.expect_comma()?;
+
+ let mut right_percentage = try_parse_percentage(input);
+
+ let right = Color::parse_internal(context, input, preserve_authored)?;
+
+ if right_percentage.is_none() {
+ right_percentage = try_parse_percentage(input);
+ }
+
+ let right_percentage = right_percentage
+ .unwrap_or_else(|| Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get())));
+
+ let left_percentage =
+ left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get()));
+
+ if left_percentage.get() + right_percentage.get() <= 0.0 {
+ // If the percentages sum to zero, the function is invalid.
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ // Pass RESULT_IN_MODERN_SYNTAX here, because the result of the color-mix() function
+ // should always be in the modern color syntax to allow for out of gamut results and
+ // to preserve floating point precision.
+ Ok(ColorMix {
+ interpolation,
+ left,
+ left_percentage,
+ right,
+ right_percentage,
+ flags: ColorMixFlags::NORMALIZE_WEIGHTS | ColorMixFlags::RESULT_IN_MODERN_SYNTAX,
+ })
+ })
+ }
+}
+
+/// Container holding an absolute color and the text specified by an author.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Absolute {
+ /// The specified color.
+ pub color: AbsoluteColor,
+ /// Authored representation.
+ pub authored: Option<Box<str>>,
+}
+
+impl ToCss for Absolute {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if let Some(ref authored) = self.authored {
+ dest.write_str(authored)
+ } else {
+ self.color.to_css(dest)
+ }
+ }
+}
+
+/// Specified color value
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum Color {
+ /// The 'currentColor' keyword
+ CurrentColor,
+ /// An absolute color.
+ /// https://w3c.github.io/csswg-drafts/css-color-4/#typedef-absolute-color-function
+ Absolute(Box<Absolute>),
+ /// A system color.
+ #[cfg(feature = "gecko")]
+ System(SystemColor),
+ /// A color mix.
+ ColorMix(Box<ColorMix>),
+ /// A light-dark() color.
+ LightDark(Box<LightDark>),
+ /// Quirksmode-only rule for inheriting color from the body
+ #[cfg(feature = "gecko")]
+ InheritFromBodyQuirk,
+}
+
+/// A light-dark(<light-color>, <dark-color>) function.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, ToCss)]
+#[css(function, comma)]
+pub struct LightDark {
+ /// The <color> that is returned when using a light theme.
+ pub light: Color,
+ /// The <color> that is returned when using a dark theme.
+ pub dark: Color,
+}
+
+impl LightDark {
+ fn compute(&self, cx: &Context) -> ComputedColor {
+ let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme();
+ let dark = cx.device().is_dark_color_scheme(&style_color_scheme);
+ let used = if dark { &self.dark } else { &self.light };
+ used.to_computed_value(cx)
+ }
+
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ preserve_authored: PreserveAuthored,
+ ) -> Result<Self, ParseError<'i>> {
+ let enabled =
+ context.chrome_rules_enabled() || static_prefs::pref!("layout.css.light-dark.enabled");
+ if !enabled {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ input.expect_function_matching("light-dark")?;
+ input.parse_nested_block(|input| {
+ let light = Color::parse_internal(context, input, preserve_authored)?;
+ input.expect_comma()?;
+ let dark = Color::parse_internal(context, input, preserve_authored)?;
+ Ok(LightDark { light, dark })
+ })
+ }
+}
+
+impl From<AbsoluteColor> for Color {
+ #[inline]
+ fn from(value: AbsoluteColor) -> Self {
+ Self::from_absolute_color(value)
+ }
+}
+
+/// System colors. A bunch of these are ad-hoc, others come from Windows:
+///
+/// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor
+///
+/// Others are HTML/CSS specific. Spec is:
+///
+/// https://drafts.csswg.org/css-color/#css-system-colors
+/// https://drafts.csswg.org/css-color/#deprecated-system-colors
+#[allow(missing_docs)]
+#[cfg(feature = "gecko")]
+#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
+#[repr(u8)]
+pub enum SystemColor {
+ Activeborder,
+ /// Background in the (active) titlebar.
+ Activecaption,
+ Appworkspace,
+ Background,
+ Buttonface,
+ Buttonhighlight,
+ Buttonshadow,
+ Buttontext,
+ Buttonborder,
+ /// Text color in the (active) titlebar.
+ Captiontext,
+ #[parse(aliases = "-moz-field")]
+ Field,
+ /// Used for disabled field backgrounds.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozDisabledfield,
+ #[parse(aliases = "-moz-fieldtext")]
+ Fieldtext,
+
+ Mark,
+ Marktext,
+
+ /// Combobox widgets
+ MozComboboxtext,
+ MozCombobox,
+
+ Graytext,
+ Highlight,
+ Highlighttext,
+ Inactiveborder,
+ /// Background in the (inactive) titlebar.
+ Inactivecaption,
+ /// Text color in the (inactive) titlebar.
+ Inactivecaptiontext,
+ Infobackground,
+ Infotext,
+ Menu,
+ Menutext,
+ Scrollbar,
+ Threeddarkshadow,
+ Threedface,
+ Threedhighlight,
+ Threedlightshadow,
+ Threedshadow,
+ Window,
+ Windowframe,
+ Windowtext,
+ #[parse(aliases = "-moz-default-color")]
+ Canvastext,
+ #[parse(aliases = "-moz-default-background-color")]
+ Canvas,
+ MozDialog,
+ MozDialogtext,
+ /// Used for selected but not focused cell backgrounds.
+ #[parse(aliases = "-moz-html-cellhighlight")]
+ MozCellhighlight,
+ /// Used for selected but not focused cell text.
+ #[parse(aliases = "-moz-html-cellhighlighttext")]
+ MozCellhighlighttext,
+ /// Used for selected and focused html cell backgrounds.
+ Selecteditem,
+ /// Used for selected and focused html cell text.
+ Selecteditemtext,
+ /// Used to button text background when hovered.
+ MozButtonhoverface,
+ /// Used to button text color when hovered.
+ MozButtonhovertext,
+ /// Used for menu item backgrounds when hovered.
+ MozMenuhover,
+ /// Used for menu item backgrounds when hovered and disabled.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMenuhoverdisabled,
+ /// Used for menu item text when hovered.
+ MozMenuhovertext,
+ /// Used for menubar item text when hovered.
+ MozMenubarhovertext,
+
+ /// On platforms where these colors are the same as -moz-field, use
+ /// -moz-fieldtext as foreground color
+ MozEventreerow,
+ MozOddtreerow,
+
+ /// Used for button text when pressed.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozButtonactivetext,
+
+ /// Used for button background when pressed.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozButtonactiveface,
+
+ /// Used for button background when disabled.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozButtondisabledface,
+
+ /// Colors used for the header bar (sorta like the tab bar / menubar).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozHeaderbar,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozHeaderbartext,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozHeaderbarinactive,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozHeaderbarinactivetext,
+
+ /// Foreground color of default buttons.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacDefaultbuttontext,
+ /// Ring color around text fields and lists.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacFocusring,
+ /// Text color of disabled text on toolbars.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacDisabledtoolbartext,
+ /// The background of a sidebar.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozSidebar,
+ /// The foreground color of a sidebar.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozSidebartext,
+ /// The border color of a sidebar.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozSidebarborder,
+
+ /// Theme accent color.
+ /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolor
+ Accentcolor,
+
+ /// Foreground for the accent color.
+ /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolortext
+ Accentcolortext,
+
+ /// The background-color for :autofill-ed inputs.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozAutofillBackground,
+
+ /// Hyperlink color extracted from the system, not affected by the browser.anchor_color user
+ /// pref.
+ ///
+ /// There is no OS-specified safe background color for this text, but it is used regularly
+ /// within Windows and the Gnome DE on Dialog and Window colors.
+ #[css(skip)]
+ MozNativehyperlinktext,
+
+ /// As above, but visited link color.
+ #[css(skip)]
+ MozNativevisitedhyperlinktext,
+
+ #[parse(aliases = "-moz-hyperlinktext")]
+ Linktext,
+ #[parse(aliases = "-moz-activehyperlinktext")]
+ Activetext,
+ #[parse(aliases = "-moz-visitedhyperlinktext")]
+ Visitedtext,
+
+ /// Color of tree column headers
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheader,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheadertext,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheaderhover,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheaderhovertext,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheaderactive,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheaderactivetext,
+
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ TextSelectDisabledBackground,
+ #[css(skip)]
+ TextSelectAttentionBackground,
+ #[css(skip)]
+ TextSelectAttentionForeground,
+ #[css(skip)]
+ TextHighlightBackground,
+ #[css(skip)]
+ TextHighlightForeground,
+ #[css(skip)]
+ IMERawInputBackground,
+ #[css(skip)]
+ IMERawInputForeground,
+ #[css(skip)]
+ IMERawInputUnderline,
+ #[css(skip)]
+ IMESelectedRawTextBackground,
+ #[css(skip)]
+ IMESelectedRawTextForeground,
+ #[css(skip)]
+ IMESelectedRawTextUnderline,
+ #[css(skip)]
+ IMEConvertedTextBackground,
+ #[css(skip)]
+ IMEConvertedTextForeground,
+ #[css(skip)]
+ IMEConvertedTextUnderline,
+ #[css(skip)]
+ IMESelectedConvertedTextBackground,
+ #[css(skip)]
+ IMESelectedConvertedTextForeground,
+ #[css(skip)]
+ IMESelectedConvertedTextUnderline,
+ #[css(skip)]
+ SpellCheckerUnderline,
+ #[css(skip)]
+ ThemedScrollbar,
+ #[css(skip)]
+ ThemedScrollbarInactive,
+ #[css(skip)]
+ ThemedScrollbarThumb,
+ #[css(skip)]
+ ThemedScrollbarThumbHover,
+ #[css(skip)]
+ ThemedScrollbarThumbActive,
+ #[css(skip)]
+ ThemedScrollbarThumbInactive,
+
+ #[css(skip)]
+ End, // Just for array-indexing purposes.
+}
+
+#[cfg(feature = "gecko")]
+impl SystemColor {
+ #[inline]
+ fn compute(&self, cx: &Context) -> ComputedColor {
+ use crate::gecko::values::convert_nscolor_to_absolute_color;
+ use crate::gecko_bindings::bindings;
+
+ // TODO: We should avoid cloning here most likely, though it's cheap-ish.
+ let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme();
+ let color = cx.device().system_nscolor(*self, &style_color_scheme);
+ if color == bindings::NS_SAME_AS_FOREGROUND_COLOR {
+ return ComputedColor::currentcolor();
+ }
+ ComputedColor::Absolute(convert_nscolor_to_absolute_color(color))
+ }
+}
+
+impl FromParsedColor for Color {
+ fn from_current_color() -> Self {
+ Color::CurrentColor
+ }
+
+ fn from_rgba(r: u8, g: u8, b: u8, a: f32) -> Self {
+ AbsoluteColor::srgb_legacy(r, g, b, a).into()
+ }
+
+ fn from_hsl(
+ hue: Option<f32>,
+ saturation: Option<f32>,
+ lightness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Hsl, hue, saturation, lightness, alpha).into()
+ }
+
+ fn from_hwb(
+ hue: Option<f32>,
+ whiteness: Option<f32>,
+ blackness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Hwb, hue, whiteness, blackness, alpha).into()
+ }
+
+ fn from_lab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Lab, lightness, a, b, alpha).into()
+ }
+
+ fn from_lch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Lch, lightness, chroma, hue, alpha).into()
+ }
+
+ fn from_oklab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Oklab, lightness, a, b, alpha).into()
+ }
+
+ fn from_oklch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Oklch, lightness, chroma, hue, alpha).into()
+ }
+
+ fn from_color_function(
+ color_space: PredefinedColorSpace,
+ c1: Option<f32>,
+ c2: Option<f32>,
+ c3: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(color_space.into(), c1, c2, c3, alpha).into()
+ }
+}
+
+struct ColorParser<'a, 'b: 'a>(&'a ParserContext<'b>);
+impl<'a, 'b: 'a, 'i: 'a> parsing::ColorParser<'i> for ColorParser<'a, 'b> {
+ type Output = Color;
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_angle_or_number<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<AngleOrNumber, ParseError<'i>> {
+ use crate::values::specified::Angle;
+
+ let location = input.current_source_location();
+ let token = input.next()?.clone();
+ match token {
+ Token::Dimension {
+ value, ref unit, ..
+ } => {
+ let angle = Angle::parse_dimension(value, unit, /* from_calc = */ false);
+
+ let degrees = match angle {
+ Ok(angle) => angle.degrees(),
+ Err(()) => return Err(location.new_unexpected_token_error(token.clone())),
+ };
+
+ Ok(AngleOrNumber::Angle { degrees })
+ },
+ Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }),
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(self.0, name, location)?;
+ CalcNode::parse_angle_or_number(self.0, input, function)
+ },
+ t => return Err(location.new_unexpected_token_error(t)),
+ }
+ }
+
+ fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> {
+ Ok(Percentage::parse(self.0, input)?.get())
+ }
+
+ fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> {
+ use crate::values::specified::Number;
+
+ Ok(Number::parse(self.0, input)?.get())
+ }
+
+ fn parse_number_or_percentage<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<NumberOrPercentage, ParseError<'i>> {
+ let location = input.current_source_location();
+
+ match *input.next()? {
+ Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }),
+ Token::Percentage { unit_value, .. } => {
+ Ok(NumberOrPercentage::Percentage { unit_value })
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(self.0, name, location)?;
+ CalcNode::parse_number_or_percentage(self.0, input, function)
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+}
+
+/// Whether to preserve authored colors during parsing. That's useful only if we
+/// plan to serialize the color back.
+#[derive(Copy, Clone)]
+enum PreserveAuthored {
+ No,
+ Yes,
+}
+
+impl Parse for Color {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, PreserveAuthored::Yes)
+ }
+}
+
+impl Color {
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ preserve_authored: PreserveAuthored,
+ ) -> Result<Self, ParseError<'i>> {
+ let authored = match preserve_authored {
+ PreserveAuthored::No => None,
+ PreserveAuthored::Yes => {
+ // Currently we only store authored value for color keywords,
+ // because all browsers serialize those values as keywords for
+ // specified value.
+ let start = input.state();
+ let authored = input.expect_ident_cloned().ok();
+ input.reset(&start);
+ authored
+ },
+ };
+
+ let color_parser = ColorParser(&*context);
+ match input.try_parse(|i| parsing::parse_color_with(&color_parser, i)) {
+ Ok(mut color) => {
+ if let Color::Absolute(ref mut absolute) = color {
+ // Because we can't set the `authored` value at construction time, we have to set it
+ // here.
+ absolute.authored = authored.map(|s| s.to_ascii_lowercase().into_boxed_str());
+ }
+ Ok(color)
+ },
+ Err(e) => {
+ #[cfg(feature = "gecko")]
+ {
+ if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) {
+ return Ok(Color::System(system));
+ }
+ }
+
+ if let Ok(mix) = input.try_parse(|i| ColorMix::parse(context, i, preserve_authored))
+ {
+ return Ok(Color::ColorMix(Box::new(mix)));
+ }
+
+ if let Ok(ld) = input.try_parse(|i| LightDark::parse(context, i, preserve_authored))
+ {
+ return Ok(Color::LightDark(Box::new(ld)));
+ }
+
+ match e.kind {
+ ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => {
+ Err(e.location.new_custom_error(StyleParseErrorKind::ValueError(
+ ValueParseErrorKind::InvalidColor(t),
+ )))
+ },
+ _ => Err(e),
+ }
+ },
+ }
+ }
+
+ /// Returns whether a given color is valid for authors.
+ pub fn is_valid(context: &ParserContext, input: &mut Parser) -> bool {
+ input
+ .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No))
+ .is_ok()
+ }
+
+ /// Tries to parse a color and compute it with a given device.
+ pub fn parse_and_compute(
+ context: &ParserContext,
+ input: &mut Parser,
+ device: Option<&Device>,
+ ) -> Option<ComputedColor> {
+ use crate::error_reporting::ContextualParseError;
+ let start = input.position();
+ let result = input
+ .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No));
+
+ let specified = match result {
+ Ok(s) => s,
+ Err(e) => {
+ if !context.error_reporting_enabled() {
+ return None;
+ }
+ // Ignore other kinds of errors that might be reported, such as
+ // ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken),
+ // since Gecko didn't use to report those to the error console.
+ //
+ // TODO(emilio): Revise whether we want to keep this at all, we
+ // use this only for canvas, this warnings are disabled by
+ // default and not available on OffscreenCanvas anyways...
+ if let ParseErrorKind::Custom(StyleParseErrorKind::ValueError(..)) = e.kind {
+ let location = e.location.clone();
+ let error = ContextualParseError::UnsupportedValue(input.slice_from(start), e);
+ context.log_css_error(location, error);
+ }
+ return None;
+ },
+ };
+
+ match device {
+ Some(device) => {
+ Context::for_media_query_evaluation(device, device.quirks_mode(), |context| {
+ specified.to_computed_color(Some(&context))
+ })
+ },
+ None => specified.to_computed_color(None),
+ }
+ }
+}
+
+impl ToCss for Color {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Color::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest),
+ Color::Absolute(ref absolute) => absolute.to_css(dest),
+ Color::ColorMix(ref mix) => mix.to_css(dest),
+ Color::LightDark(ref ld) => ld.to_css(dest),
+ #[cfg(feature = "gecko")]
+ Color::System(system) => system.to_css(dest),
+ #[cfg(feature = "gecko")]
+ Color::InheritFromBodyQuirk => Ok(()),
+ }
+ }
+}
+
+impl Color {
+ /// Returns whether this color is allowed in forced-colors mode.
+ pub fn honored_in_forced_colors_mode(&self, allow_transparent: bool) -> bool {
+ match *self {
+ Self::InheritFromBodyQuirk => false,
+ Self::CurrentColor | Color::System(..) => true,
+ Self::Absolute(ref absolute) => allow_transparent && absolute.color.is_transparent(),
+ Self::LightDark(ref ld) => {
+ ld.light.honored_in_forced_colors_mode(allow_transparent) &&
+ ld.dark.honored_in_forced_colors_mode(allow_transparent)
+ },
+ Self::ColorMix(ref mix) => {
+ mix.left.honored_in_forced_colors_mode(allow_transparent) &&
+ mix.right.honored_in_forced_colors_mode(allow_transparent)
+ },
+ }
+ }
+
+ /// Returns currentcolor value.
+ #[inline]
+ pub fn currentcolor() -> Self {
+ Self::CurrentColor
+ }
+
+ /// Returns transparent value.
+ #[inline]
+ pub fn transparent() -> Self {
+ // We should probably set authored to "transparent", but maybe it doesn't matter.
+ Self::from_absolute_color(AbsoluteColor::TRANSPARENT_BLACK)
+ }
+
+ /// Create a color from an [`AbsoluteColor`].
+ pub fn from_absolute_color(color: AbsoluteColor) -> Self {
+ Color::Absolute(Box::new(Absolute {
+ color,
+ authored: None,
+ }))
+ }
+
+ /// Parse a color, with quirks.
+ ///
+ /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk>
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ input.try_parse(|i| Self::parse(context, i)).or_else(|e| {
+ if !allow_quirks.allowed(context.quirks_mode) {
+ return Err(e);
+ }
+ Color::parse_quirky_color(input).map_err(|_| e)
+ })
+ }
+
+ fn parse_hash<'i>(
+ bytes: &[u8],
+ loc: &cssparser::SourceLocation,
+ ) -> Result<Self, ParseError<'i>> {
+ match cssparser::color::parse_hash_color(bytes) {
+ Ok((r, g, b, a)) => Ok(Self::from_rgba(r, g, b, a)),
+ Err(()) => Err(loc.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+
+ /// Parse a <quirky-color> value.
+ ///
+ /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk>
+ fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let (value, unit) = match *input.next()? {
+ Token::Number {
+ int_value: Some(integer),
+ ..
+ } => (integer, None),
+ Token::Dimension {
+ int_value: Some(integer),
+ ref unit,
+ ..
+ } => (integer, Some(unit)),
+ Token::Ident(ref ident) => {
+ if ident.len() != 3 && ident.len() != 6 {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ return Self::parse_hash(ident.as_bytes(), &location);
+ },
+ ref t => {
+ return Err(location.new_unexpected_token_error(t.clone()));
+ },
+ };
+ if value < 0 {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ let length = if value <= 9 {
+ 1
+ } else if value <= 99 {
+ 2
+ } else if value <= 999 {
+ 3
+ } else if value <= 9999 {
+ 4
+ } else if value <= 99999 {
+ 5
+ } else if value <= 999999 {
+ 6
+ } else {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ };
+ let total = length + unit.as_ref().map_or(0, |d| d.len());
+ if total > 6 {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ let mut serialization = [b'0'; 6];
+ let space_padding = 6 - total;
+ let mut written = space_padding;
+ let mut buf = itoa::Buffer::new();
+ let s = buf.format(value);
+ (&mut serialization[written..])
+ .write_all(s.as_bytes())
+ .unwrap();
+ written += s.len();
+ if let Some(unit) = unit {
+ written += (&mut serialization[written..])
+ .write(unit.as_bytes())
+ .unwrap();
+ }
+ debug_assert_eq!(written, 6);
+ Self::parse_hash(&serialization, &location)
+ }
+}
+
+impl Color {
+ /// Converts this Color into a ComputedColor.
+ ///
+ /// If `context` is `None`, and the specified color requires data from
+ /// the context to resolve, then `None` is returned.
+ pub fn to_computed_color(&self, context: Option<&Context>) -> Option<ComputedColor> {
+ Some(match *self {
+ Color::CurrentColor => ComputedColor::CurrentColor,
+ Color::Absolute(ref absolute) => {
+ let mut color = absolute.color;
+
+ // Computed lightness values can not be NaN.
+ if matches!(
+ color.color_space,
+ ColorSpace::Lab | ColorSpace::Oklab | ColorSpace::Lch | ColorSpace::Oklch
+ ) {
+ color.components.0 = normalize(color.components.0);
+ }
+
+ // Computed RGB and XYZ components can not be NaN.
+ if !color.is_legacy_syntax() && color.color_space.is_rgb_or_xyz_like() {
+ color.components = color.components.map(normalize);
+ }
+
+ color.alpha = normalize(color.alpha);
+
+ ComputedColor::Absolute(color)
+ },
+ Color::LightDark(ref ld) => ld.compute(context?),
+ Color::ColorMix(ref mix) => {
+ use crate::values::computed::percentage::Percentage;
+
+ let left = mix.left.to_computed_color(context)?;
+ let right = mix.right.to_computed_color(context)?;
+
+ ComputedColor::from_color_mix(GenericColorMix {
+ interpolation: mix.interpolation,
+ left,
+ left_percentage: Percentage(mix.left_percentage.get()),
+ right,
+ right_percentage: Percentage(mix.right_percentage.get()),
+ flags: mix.flags,
+ })
+ },
+ #[cfg(feature = "gecko")]
+ Color::System(system) => system.compute(context?),
+ #[cfg(feature = "gecko")]
+ Color::InheritFromBodyQuirk => {
+ ComputedColor::Absolute(context?.device().body_text_color())
+ },
+ })
+ }
+}
+
+impl ToComputedValue for Color {
+ type ComputedValue = ComputedColor;
+
+ fn to_computed_value(&self, context: &Context) -> ComputedColor {
+ self.to_computed_color(Some(context)).unwrap()
+ }
+
+ fn from_computed_value(computed: &ComputedColor) -> Self {
+ match *computed {
+ ComputedColor::Absolute(ref color) => Self::from_absolute_color(color.clone()),
+ ComputedColor::CurrentColor => Color::CurrentColor,
+ ComputedColor::ColorMix(ref mix) => {
+ Color::ColorMix(Box::new(ToComputedValue::from_computed_value(&**mix)))
+ },
+ }
+ }
+}
+
+impl SpecifiedValueInfo for Color {
+ const SUPPORTED_TYPES: u8 = CssType::COLOR;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ // We are not going to insert all the color names here. Caller and
+ // devtools should take care of them. XXX Actually, transparent
+ // should probably be handled that way as well.
+ // XXX `currentColor` should really be `currentcolor`. But let's
+ // keep it consistent with the old system for now.
+ f(&[
+ "rgb",
+ "rgba",
+ "hsl",
+ "hsla",
+ "hwb",
+ "currentColor",
+ "transparent",
+ "color-mix",
+ "color",
+ "lab",
+ "lch",
+ "oklab",
+ "oklch",
+ ]);
+ }
+}
+
+/// Specified value for the "color" property, which resolves the `currentcolor`
+/// keyword to the parent color instead of self's color.
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct ColorPropertyValue(pub Color);
+
+impl ToComputedValue for ColorPropertyValue {
+ type ComputedValue = AbsoluteColor;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let current_color = context.builder.get_parent_inherited_text().clone_color();
+ self.0
+ .to_computed_value(context)
+ .resolve_to_absolute(&current_color)
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ ColorPropertyValue(Color::from_absolute_color(*computed).into())
+ }
+}
+
+impl Parse for ColorPropertyValue {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Color::parse_quirky(context, input, AllowQuirks::Yes).map(ColorPropertyValue)
+ }
+}
+
+/// auto | <color>
+pub type ColorOrAuto = GenericColorOrAuto<Color>;
+
+/// caret-color
+pub type CaretColor = GenericCaretColor<Color>;
+
+impl Parse for CaretColor {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ ColorOrAuto::parse(context, input).map(GenericCaretColor)
+ }
+}
+
+/// Various flags to represent the color-scheme property in an efficient
+/// way.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Default,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[value_info(other_values = "light,dark,only")]
+pub struct ColorSchemeFlags(u8);
+bitflags! {
+ impl ColorSchemeFlags: u8 {
+ /// Whether the author specified `light`.
+ const LIGHT = 1 << 0;
+ /// Whether the author specified `dark`.
+ const DARK = 1 << 1;
+ /// Whether the author specified `only`.
+ const ONLY = 1 << 2;
+ }
+}
+
+/// <https://drafts.csswg.org/css-color-adjust/#color-scheme-prop>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[value_info(other_values = "normal")]
+pub struct ColorScheme {
+ #[ignore_malloc_size_of = "Arc"]
+ idents: crate::ArcSlice<CustomIdent>,
+ bits: ColorSchemeFlags,
+}
+
+impl ColorScheme {
+ /// Returns the `normal` value.
+ pub fn normal() -> Self {
+ Self {
+ idents: Default::default(),
+ bits: ColorSchemeFlags::empty(),
+ }
+ }
+
+ /// Returns the raw bitfield.
+ pub fn raw_bits(&self) -> u8 {
+ self.bits.bits()
+ }
+}
+
+impl Parse for ColorScheme {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut idents = vec![];
+ let mut bits = ColorSchemeFlags::empty();
+
+ let mut location = input.current_source_location();
+ while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
+ let mut is_only = false;
+ match_ignore_ascii_case! { &ident,
+ "normal" => {
+ if idents.is_empty() && bits.is_empty() {
+ return Ok(Self::normal());
+ }
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ },
+ "light" => bits.insert(ColorSchemeFlags::LIGHT),
+ "dark" => bits.insert(ColorSchemeFlags::DARK),
+ "only" => {
+ if bits.intersects(ColorSchemeFlags::ONLY) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ bits.insert(ColorSchemeFlags::ONLY);
+ is_only = true;
+ },
+ _ => {},
+ };
+
+ if is_only {
+ if !idents.is_empty() {
+ // Only is allowed either at the beginning or at the end,
+ // but not in the middle.
+ break;
+ }
+ } else {
+ idents.push(CustomIdent::from_ident(location, &ident, &[])?);
+ }
+ location = input.current_source_location();
+ }
+
+ if idents.is_empty() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(Self {
+ idents: crate::ArcSlice::from_iter(idents.into_iter()),
+ bits,
+ })
+ }
+}
+
+impl ToCss for ColorScheme {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.idents.is_empty() {
+ debug_assert!(self.bits.is_empty());
+ return dest.write_str("normal");
+ }
+ let mut first = true;
+ for ident in self.idents.iter() {
+ if !first {
+ dest.write_char(' ')?;
+ }
+ first = false;
+ ident.to_css(dest)?;
+ }
+ if self.bits.intersects(ColorSchemeFlags::ONLY) {
+ dest.write_str(" only")?;
+ }
+ Ok(())
+ }
+}
+
+/// https://drafts.csswg.org/css-color-adjust/#print-color-adjust
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum PrintColorAdjust {
+ /// Ignore backgrounds and darken text.
+ Economy,
+ /// Respect specified colors.
+ Exact,
+}
+
+/// https://drafts.csswg.org/css-color-adjust-1/#forced-color-adjust-prop
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ForcedColorAdjust {
+ /// Adjust colors if needed.
+ Auto,
+ /// Respect specified colors.
+ None,
+}
diff --git a/servo/components/style/values/specified/column.rs b/servo/components/style/values/specified/column.rs
new file mode 100644
index 0000000000..2dd7bb0144
--- /dev/null
+++ b/servo/components/style/values/specified/column.rs
@@ -0,0 +1,11 @@
+/* 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 types for the column properties.
+
+use crate::values::generics::column::ColumnCount as GenericColumnCount;
+use crate::values::specified::PositiveInteger;
+
+/// A specified type for `column-count` values.
+pub type ColumnCount = GenericColumnCount<PositiveInteger>;
diff --git a/servo/components/style/values/specified/counters.rs b/servo/components/style/values/specified/counters.rs
new file mode 100644
index 0000000000..9d8261ce6c
--- /dev/null
+++ b/servo/components/style/values/specified/counters.rs
@@ -0,0 +1,279 @@
+/* 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 types for counter properties.
+
+#[cfg(feature = "servo-layout-2013")]
+use crate::computed_values::list_style_type::T as ListStyleType;
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::counters as generics;
+use crate::values::generics::counters::CounterPair;
+#[cfg(feature = "gecko")]
+use crate::values::generics::CounterStyle;
+use crate::values::specified::image::Image;
+#[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+use crate::values::specified::Attr;
+use crate::values::specified::Integer;
+use crate::values::CustomIdent;
+use cssparser::{Parser, Token};
+#[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+use selectors::parser::SelectorParseErrorKind;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+#[derive(PartialEq)]
+enum CounterType {
+ Increment,
+ Set,
+ Reset,
+}
+
+impl CounterType {
+ fn default_value(&self) -> i32 {
+ match *self {
+ Self::Increment => 1,
+ Self::Reset | Self::Set => 0,
+ }
+ }
+}
+
+/// A specified value for the `counter-increment` property.
+pub type CounterIncrement = generics::GenericCounterIncrement<Integer>;
+
+impl Parse for CounterIncrement {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self::new(parse_counters(
+ context,
+ input,
+ CounterType::Increment,
+ )?))
+ }
+}
+
+/// A specified value for the `counter-set` property.
+pub type CounterSet = generics::GenericCounterSet<Integer>;
+
+impl Parse for CounterSet {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self::new(parse_counters(context, input, CounterType::Set)?))
+ }
+}
+
+/// A specified value for the `counter-reset` property.
+pub type CounterReset = generics::GenericCounterReset<Integer>;
+
+impl Parse for CounterReset {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self::new(parse_counters(
+ context,
+ input,
+ CounterType::Reset,
+ )?))
+ }
+}
+
+fn parse_counters<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ counter_type: CounterType,
+) -> Result<Vec<CounterPair<Integer>>, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(vec![]);
+ }
+
+ let mut counters = Vec::new();
+ loop {
+ let location = input.current_source_location();
+ let (name, is_reversed) = match input.next() {
+ Ok(&Token::Ident(ref ident)) => {
+ (CustomIdent::from_ident(location, ident, &["none"])?, false)
+ },
+ Ok(&Token::Function(ref name))
+ if counter_type == CounterType::Reset && name.eq_ignore_ascii_case("reversed") =>
+ {
+ input
+ .parse_nested_block(|input| Ok((CustomIdent::parse(input, &["none"])?, true)))?
+ },
+ Ok(t) => {
+ let t = t.clone();
+ return Err(location.new_unexpected_token_error(t));
+ },
+ Err(_) => break,
+ };
+
+ let value = match input.try_parse(|input| Integer::parse(context, input)) {
+ Ok(start) => {
+ if start.value == i32::min_value() {
+ // The spec says that values must be clamped to the valid range,
+ // and we reserve i32::min_value() as an internal magic value.
+ // https://drafts.csswg.org/css-lists/#auto-numbering
+ Integer::new(i32::min_value() + 1)
+ } else {
+ start
+ }
+ },
+ _ => Integer::new(if is_reversed {
+ i32::min_value()
+ } else {
+ counter_type.default_value()
+ }),
+ };
+ counters.push(CounterPair {
+ name,
+ value,
+ is_reversed,
+ });
+ }
+
+ if !counters.is_empty() {
+ Ok(counters)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+/// The specified value for the `content` property.
+pub type Content = generics::GenericContent<Image>;
+
+/// The specified value for a content item in the `content` property.
+pub type ContentItem = generics::GenericContentItem<Image>;
+
+impl Content {
+ #[cfg(feature = "servo-layout-2013")]
+ fn parse_counter_style(_: &ParserContext, input: &mut Parser) -> ListStyleType {
+ input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ ListStyleType::parse(input)
+ })
+ .unwrap_or(ListStyleType::Decimal)
+ }
+
+ #[cfg(feature = "gecko")]
+ fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyle {
+ input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ CounterStyle::parse(context, input)
+ })
+ .unwrap_or(CounterStyle::decimal())
+ }
+}
+
+impl Parse for Content {
+ // normal | none | [ <string> | <counter> | open-quote | close-quote | no-open-quote |
+ // no-close-quote ]+
+ // TODO: <uri>, attr(<identifier>)
+ #[cfg_attr(feature = "servo", allow(unused_mut))]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(generics::Content::Normal);
+ }
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(generics::Content::None);
+ }
+
+ let mut content = vec![];
+ let mut has_alt_content = false;
+ loop {
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+ {
+ if let Ok(image) = input.try_parse(|i| Image::parse_forbid_none(context, i)) {
+ content.push(generics::ContentItem::Image(image));
+ continue;
+ }
+ }
+ match input.next() {
+ Ok(&Token::QuotedString(ref value)) => {
+ content.push(generics::ContentItem::String(
+ value.as_ref().to_owned().into(),
+ ));
+ },
+ Ok(&Token::Function(ref name)) => {
+ let result = match_ignore_ascii_case! { &name,
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ "counter" => input.parse_nested_block(|input| {
+ let name = CustomIdent::parse(input, &[])?;
+ let style = Content::parse_counter_style(context, input);
+ Ok(generics::ContentItem::Counter(name, style))
+ }),
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ "counters" => input.parse_nested_block(|input| {
+ let name = CustomIdent::parse(input, &[])?;
+ input.expect_comma()?;
+ let separator = input.expect_string()?.as_ref().to_owned().into();
+ let style = Content::parse_counter_style(context, input);
+ Ok(generics::ContentItem::Counters(name, separator, style))
+ }),
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+ "attr" => input.parse_nested_block(|input| {
+ Ok(generics::ContentItem::Attr(Attr::parse_function(context, input)?))
+ }),
+ _ => {
+ use style_traits::StyleParseErrorKind;
+ let name = name.clone();
+ return Err(input.new_custom_error(
+ StyleParseErrorKind::UnexpectedFunction(name),
+ ))
+ }
+ }?;
+ content.push(result);
+ },
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ Ok(&Token::Ident(ref ident)) => {
+ content.push(match_ignore_ascii_case! { &ident,
+ "open-quote" => generics::ContentItem::OpenQuote,
+ "close-quote" => generics::ContentItem::CloseQuote,
+ "no-open-quote" => generics::ContentItem::NoOpenQuote,
+ "no-close-quote" => generics::ContentItem::NoCloseQuote,
+ #[cfg(feature = "gecko")]
+ "-moz-alt-content" => {
+ has_alt_content = true;
+ generics::ContentItem::MozAltContent
+ },
+ "-moz-label-content" if context.chrome_rules_enabled() => {
+ generics::ContentItem::MozLabelContent
+ },
+ _ =>{
+ let ident = ident.clone();
+ return Err(input.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident)
+ ));
+ }
+ });
+ },
+ Err(_) => break,
+ Ok(t) => {
+ let t = t.clone();
+ return Err(input.new_unexpected_token_error(t));
+ },
+ }
+ }
+ // We don't allow to parse `-moz-alt-content` in multiple positions.
+ if content.is_empty() || (has_alt_content && content.len() != 1) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(generics::Content::Items(content.into()))
+ }
+}
diff --git a/servo/components/style/values/specified/easing.rs b/servo/components/style/values/specified/easing.rs
new file mode 100644
index 0000000000..5e4d8ae1ea
--- /dev/null
+++ b/servo/components/style/values/specified/easing.rs
@@ -0,0 +1,192 @@
+/* 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 types for CSS Easing functions.
+use crate::parser::{Parse, ParserContext};
+use crate::piecewise_linear::{PiecewiseLinearFunction, PiecewiseLinearFunctionBuilder};
+use crate::values::computed::easing::TimingFunction as ComputedTimingFunction;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::easing::TimingFunction as GenericTimingFunction;
+use crate::values::generics::easing::{StepPosition, TimingKeyword};
+use crate::values::specified::{Integer, Number, Percentage};
+use cssparser::{Delimiter, Parser, Token};
+use selectors::parser::SelectorParseErrorKind;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// A specified timing function.
+pub type TimingFunction = GenericTimingFunction<Integer, Number, PiecewiseLinearFunction>;
+
+impl Parse for TimingFunction {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(keyword) = input.try_parse(TimingKeyword::parse) {
+ return Ok(GenericTimingFunction::Keyword(keyword));
+ }
+ if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
+ let position = match_ignore_ascii_case! { &ident,
+ "step-start" => StepPosition::Start,
+ "step-end" => StepPosition::End,
+ _ => {
+ return Err(input.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+ ));
+ },
+ };
+ return Ok(GenericTimingFunction::Steps(Integer::new(1), position));
+ }
+ let location = input.current_source_location();
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(move |i| {
+ match_ignore_ascii_case! { &function,
+ "cubic-bezier" => Self::parse_cubic_bezier(context, i),
+ "steps" => Self::parse_steps(context, i),
+ "linear" => Self::parse_linear_function(context, i),
+ _ => Err(location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
+ }
+ })
+ }
+}
+
+impl TimingFunction {
+ fn parse_cubic_bezier<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let x1 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let y1 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let x2 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let y2 = Number::parse(context, input)?;
+
+ if x1.get() < 0.0 || x1.get() > 1.0 || x2.get() < 0.0 || x2.get() > 1.0 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(GenericTimingFunction::CubicBezier { x1, y1, x2, y2 })
+ }
+
+ fn parse_steps<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let steps = Integer::parse_positive(context, input)?;
+ let position = input
+ .try_parse(|i| {
+ i.expect_comma()?;
+ StepPosition::parse(context, i)
+ })
+ .unwrap_or(StepPosition::End);
+
+ // jump-none accepts a positive integer greater than 1.
+ // FIXME(emilio): The spec asks us to avoid rejecting it at parse
+ // time except until computed value time.
+ //
+ // It's not totally clear it's worth it though, and no other browser
+ // does this.
+ if position == StepPosition::JumpNone && steps.value() <= 1 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(GenericTimingFunction::Steps(steps, position))
+ }
+
+ fn parse_linear_function<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut builder = PiecewiseLinearFunctionBuilder::default();
+ let mut num_specified_stops = 0;
+ // Closely follows `parse_comma_separated`, but can generate multiple entries for one comma-separated entry.
+ loop {
+ input.parse_until_before(Delimiter::Comma, |i| {
+ let builder = &mut builder;
+ let mut input_start = i.try_parse(|i| Percentage::parse(context, i)).ok();
+ let mut input_end = i.try_parse(|i| Percentage::parse(context, i)).ok();
+
+ let output = Number::parse(context, i)?;
+ if input_start.is_none() {
+ debug_assert!(input_end.is_none(), "Input end parsed without input start?");
+ input_start = i.try_parse(|i| Percentage::parse(context, i)).ok();
+ input_end = i.try_parse(|i| Percentage::parse(context, i)).ok();
+ }
+ builder.push(output.into(), input_start.map(|v| v.get()).into());
+ num_specified_stops += 1;
+ if input_end.is_some() {
+ debug_assert!(
+ input_start.is_some(),
+ "Input end valid but not input start?"
+ );
+ builder.push(output.into(), input_end.map(|v| v.get()).into());
+ }
+
+ Ok(())
+ })?;
+
+ match input.next() {
+ Err(_) => break,
+ Ok(&Token::Comma) => continue,
+ Ok(_) => unreachable!(),
+ }
+ }
+ // By spec, specifying only a single stop makes the function invalid, even if that single entry may generate
+ // two entries.
+ if num_specified_stops < 2 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(GenericTimingFunction::LinearFunction(builder.build()))
+ }
+}
+
+// We need this for converting the specified TimingFunction into computed TimingFunction without
+// Context (for some FFIs in glue.rs). In fact, we don't really need Context to get the computed
+// value of TimingFunction.
+impl TimingFunction {
+ /// Generate the ComputedTimingFunction without Context.
+ pub fn to_computed_value_without_context(&self) -> ComputedTimingFunction {
+ match &self {
+ GenericTimingFunction::Steps(steps, pos) => {
+ GenericTimingFunction::Steps(steps.value(), *pos)
+ },
+ GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => {
+ GenericTimingFunction::CubicBezier {
+ x1: x1.get(),
+ y1: y1.get(),
+ x2: x2.get(),
+ y2: y2.get(),
+ }
+ },
+ GenericTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword),
+ GenericTimingFunction::LinearFunction(function) => {
+ GenericTimingFunction::LinearFunction(function.clone())
+ },
+ }
+ }
+}
+
+impl ToComputedValue for TimingFunction {
+ type ComputedValue = ComputedTimingFunction;
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ self.to_computed_value_without_context()
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match &computed {
+ ComputedTimingFunction::Steps(steps, pos) => Self::Steps(Integer::new(*steps), *pos),
+ ComputedTimingFunction::CubicBezier { x1, y1, x2, y2 } => Self::CubicBezier {
+ x1: Number::new(*x1),
+ y1: Number::new(*y1),
+ x2: Number::new(*x2),
+ y2: Number::new(*y2),
+ },
+ ComputedTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword),
+ ComputedTimingFunction::LinearFunction(function) => {
+ GenericTimingFunction::LinearFunction(function.clone())
+ },
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/effects.rs b/servo/components/style/values/specified/effects.rs
new file mode 100644
index 0000000000..0453582768
--- /dev/null
+++ b/servo/components/style/values/specified/effects.rs
@@ -0,0 +1,453 @@
+/* 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 types for CSS values related to effects.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::effects::BoxShadow as ComputedBoxShadow;
+use crate::values::computed::effects::SimpleShadow as ComputedSimpleShadow;
+#[cfg(feature = "gecko")]
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::Angle as ComputedAngle;
+use crate::values::computed::CSSPixelLength as ComputedCSSPixelLength;
+use crate::values::computed::Filter as ComputedFilter;
+use crate::values::computed::NonNegativeLength as ComputedNonNegativeLength;
+use crate::values::computed::NonNegativeNumber as ComputedNonNegativeNumber;
+use crate::values::computed::ZeroToOneNumber as ComputedZeroToOneNumber;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::effects::BoxShadow as GenericBoxShadow;
+use crate::values::generics::effects::Filter as GenericFilter;
+use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow;
+use crate::values::generics::NonNegative;
+use crate::values::specified::color::Color;
+use crate::values::specified::length::{Length, NonNegativeLength};
+#[cfg(feature = "gecko")]
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::{Angle, Number, NumberOrPercentage};
+#[cfg(feature = "servo")]
+use crate::values::Impossible;
+use crate::Zero;
+use cssparser::{self, BasicParseErrorKind, Parser, Token};
+use style_traits::{ParseError, StyleParseErrorKind, ValueParseErrorKind};
+
+/// A specified value for a single shadow of the `box-shadow` property.
+pub type BoxShadow =
+ GenericBoxShadow<Option<Color>, Length, Option<NonNegativeLength>, Option<Length>>;
+
+/// A specified value for a single `filter`.
+#[cfg(feature = "gecko")]
+pub type SpecifiedFilter = GenericFilter<
+ Angle,
+ NonNegativeFactor,
+ ZeroToOneFactor,
+ NonNegativeLength,
+ SimpleShadow,
+ SpecifiedUrl,
+>;
+
+/// A specified value for a single `filter`.
+#[cfg(feature = "servo")]
+pub type SpecifiedFilter = GenericFilter<
+ Angle,
+ NonNegativeFactor,
+ ZeroToOneFactor,
+ NonNegativeLength,
+ Impossible,
+ Impossible,
+>;
+
+pub use self::SpecifiedFilter as Filter;
+
+/// A value for the `<factor>` parts in `Filter`.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct NonNegativeFactor(NumberOrPercentage);
+
+/// A value for the `<factor>` parts in `Filter` which clamps to one.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct ZeroToOneFactor(NumberOrPercentage);
+
+/// Clamp the value to 1 if the value is over 100%.
+#[inline]
+fn clamp_to_one(number: NumberOrPercentage) -> NumberOrPercentage {
+ match number {
+ NumberOrPercentage::Percentage(percent) => {
+ NumberOrPercentage::Percentage(percent.clamp_to_hundred())
+ },
+ NumberOrPercentage::Number(number) => NumberOrPercentage::Number(number.clamp_to_one()),
+ }
+}
+
+macro_rules! factor_impl_common {
+ ($ty:ty, $computed_ty:ty) => {
+ impl $ty {
+ #[inline]
+ fn one() -> Self {
+ Self(NumberOrPercentage::Number(Number::new(1.)))
+ }
+ }
+
+ impl ToComputedValue for $ty {
+ type ComputedValue = $computed_ty;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ use crate::values::computed::NumberOrPercentage;
+ match self.0.to_computed_value(context) {
+ NumberOrPercentage::Number(n) => n.into(),
+ NumberOrPercentage::Percentage(p) => p.0.into(),
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self(NumberOrPercentage::Number(
+ ToComputedValue::from_computed_value(&computed.0),
+ ))
+ }
+ }
+ };
+}
+factor_impl_common!(NonNegativeFactor, ComputedNonNegativeNumber);
+factor_impl_common!(ZeroToOneFactor, ComputedZeroToOneNumber);
+
+impl Parse for NonNegativeFactor {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ NumberOrPercentage::parse_non_negative(context, input).map(Self)
+ }
+}
+
+impl Parse for ZeroToOneFactor {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ NumberOrPercentage::parse_non_negative(context, input)
+ .map(clamp_to_one)
+ .map(Self)
+ }
+}
+
+/// A specified value for the `drop-shadow()` filter.
+pub type SimpleShadow = GenericSimpleShadow<Option<Color>, Length, Option<NonNegativeLength>>;
+
+impl Parse for BoxShadow {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut lengths = None;
+ let mut color = None;
+ let mut inset = false;
+
+ loop {
+ if !inset {
+ if input
+ .try_parse(|input| input.expect_ident_matching("inset"))
+ .is_ok()
+ {
+ inset = true;
+ continue;
+ }
+ }
+ if lengths.is_none() {
+ let value = input.try_parse::<_, _, ParseError>(|i| {
+ let horizontal = Length::parse(context, i)?;
+ let vertical = Length::parse(context, i)?;
+ let (blur, spread) =
+ match i.try_parse(|i| Length::parse_non_negative(context, i)) {
+ Ok(blur) => {
+ let spread = i.try_parse(|i| Length::parse(context, i)).ok();
+ (Some(blur.into()), spread)
+ },
+ Err(_) => (None, None),
+ };
+ Ok((horizontal, vertical, blur, spread))
+ });
+ if let Ok(value) = value {
+ lengths = Some(value);
+ continue;
+ }
+ }
+ if color.is_none() {
+ if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) {
+ color = Some(value);
+ continue;
+ }
+ }
+ break;
+ }
+
+ let lengths =
+ lengths.ok_or(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?;
+ Ok(BoxShadow {
+ base: SimpleShadow {
+ color: color,
+ horizontal: lengths.0,
+ vertical: lengths.1,
+ blur: lengths.2,
+ },
+ spread: lengths.3,
+ inset: inset,
+ })
+ }
+}
+
+impl ToComputedValue for BoxShadow {
+ type ComputedValue = ComputedBoxShadow;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ ComputedBoxShadow {
+ base: self.base.to_computed_value(context),
+ spread: self
+ .spread
+ .as_ref()
+ .unwrap_or(&Length::zero())
+ .to_computed_value(context),
+ inset: self.inset,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &ComputedBoxShadow) -> Self {
+ BoxShadow {
+ base: ToComputedValue::from_computed_value(&computed.base),
+ spread: Some(ToComputedValue::from_computed_value(&computed.spread)),
+ inset: computed.inset,
+ }
+ }
+}
+
+// We need this for converting the specified Filter into computed Filter without Context (for
+// some FFIs in glue.rs). This can fail because in some circumstances, we still need Context to
+// determine the computed value.
+impl Filter {
+ /// Generate the ComputedFilter without Context.
+ pub fn to_computed_value_without_context(&self) -> Result<ComputedFilter, ()> {
+ match *self {
+ Filter::Blur(ref length) => Ok(ComputedFilter::Blur(ComputedNonNegativeLength::new(
+ length.0.to_computed_pixel_length_without_context()?,
+ ))),
+ Filter::Brightness(ref factor) => Ok(ComputedFilter::Brightness(
+ ComputedNonNegativeNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Contrast(ref factor) => Ok(ComputedFilter::Contrast(
+ ComputedNonNegativeNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Grayscale(ref factor) => Ok(ComputedFilter::Grayscale(
+ ComputedZeroToOneNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::HueRotate(ref angle) => Ok(ComputedFilter::HueRotate(
+ ComputedAngle::from_degrees(angle.degrees()),
+ )),
+ Filter::Invert(ref factor) => Ok(ComputedFilter::Invert(
+ ComputedZeroToOneNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Opacity(ref factor) => Ok(ComputedFilter::Opacity(
+ ComputedZeroToOneNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Saturate(ref factor) => Ok(ComputedFilter::Saturate(
+ ComputedNonNegativeNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Sepia(ref factor) => Ok(ComputedFilter::Sepia(ComputedZeroToOneNumber::from(
+ factor.0.to_number().get(),
+ ))),
+ Filter::DropShadow(ref shadow) => {
+ if cfg!(feature = "gecko") {
+ let color = match shadow
+ .color
+ .as_ref()
+ .unwrap_or(&Color::currentcolor())
+ .to_computed_color(None)
+ {
+ Some(c) => c,
+ None => return Err(()),
+ };
+
+ let horizontal = ComputedCSSPixelLength::new(
+ shadow
+ .horizontal
+ .to_computed_pixel_length_without_context()?,
+ );
+ let vertical = ComputedCSSPixelLength::new(
+ shadow.vertical.to_computed_pixel_length_without_context()?,
+ );
+ let blur = ComputedNonNegativeLength::new(
+ shadow
+ .blur
+ .as_ref()
+ .unwrap_or(&NonNegativeLength::zero())
+ .0
+ .to_computed_pixel_length_without_context()?,
+ );
+
+ Ok(ComputedFilter::DropShadow(ComputedSimpleShadow {
+ color,
+ horizontal,
+ vertical,
+ blur,
+ }))
+ } else {
+ Err(())
+ }
+ },
+ Filter::Url(ref url) => {
+ if cfg!(feature = "gecko") {
+ Ok(ComputedFilter::Url(ComputedUrl(url.clone())))
+ } else {
+ Err(())
+ }
+ },
+ }
+ }
+}
+
+impl Parse for Filter {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ #[cfg(feature = "gecko")]
+ {
+ if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
+ return Ok(GenericFilter::Url(url));
+ }
+ }
+ let location = input.current_source_location();
+ let function = match input.expect_function() {
+ Ok(f) => f.clone(),
+ Err(cssparser::BasicParseError {
+ kind: BasicParseErrorKind::UnexpectedToken(t),
+ location,
+ }) => return Err(location.new_custom_error(ValueParseErrorKind::InvalidFilter(t))),
+ Err(e) => return Err(e.into()),
+ };
+ input.parse_nested_block(|i| {
+ match_ignore_ascii_case! { &*function,
+ "blur" => Ok(GenericFilter::Blur(
+ i.try_parse(|i| NonNegativeLength::parse(context, i))
+ .unwrap_or(Zero::zero()),
+ )),
+ "brightness" => Ok(GenericFilter::Brightness(
+ i.try_parse(|i| NonNegativeFactor::parse(context, i))
+ .unwrap_or(NonNegativeFactor::one()),
+ )),
+ "contrast" => Ok(GenericFilter::Contrast(
+ i.try_parse(|i| NonNegativeFactor::parse(context, i))
+ .unwrap_or(NonNegativeFactor::one()),
+ )),
+ "grayscale" => {
+ // Values of amount over 100% are allowed but UAs must clamp the values to 1.
+ // https://drafts.fxtf.org/filter-effects/#funcdef-filter-grayscale
+ Ok(GenericFilter::Grayscale(
+ i.try_parse(|i| ZeroToOneFactor::parse(context, i))
+ .unwrap_or(ZeroToOneFactor::one()),
+ ))
+ },
+ "hue-rotate" => {
+ // We allow unitless zero here, see:
+ // https://github.com/w3c/fxtf-drafts/issues/228
+ Ok(GenericFilter::HueRotate(
+ i.try_parse(|i| Angle::parse_with_unitless(context, i))
+ .unwrap_or(Zero::zero()),
+ ))
+ },
+ "invert" => {
+ // Values of amount over 100% are allowed but UAs must clamp the values to 1.
+ // https://drafts.fxtf.org/filter-effects/#funcdef-filter-invert
+ Ok(GenericFilter::Invert(
+ i.try_parse(|i| ZeroToOneFactor::parse(context, i))
+ .unwrap_or(ZeroToOneFactor::one()),
+ ))
+ },
+ "opacity" => {
+ // Values of amount over 100% are allowed but UAs must clamp the values to 1.
+ // https://drafts.fxtf.org/filter-effects/#funcdef-filter-opacity
+ Ok(GenericFilter::Opacity(
+ i.try_parse(|i| ZeroToOneFactor::parse(context, i))
+ .unwrap_or(ZeroToOneFactor::one()),
+ ))
+ },
+ "saturate" => Ok(GenericFilter::Saturate(
+ i.try_parse(|i| NonNegativeFactor::parse(context, i))
+ .unwrap_or(NonNegativeFactor::one()),
+ )),
+ "sepia" => {
+ // Values of amount over 100% are allowed but UAs must clamp the values to 1.
+ // https://drafts.fxtf.org/filter-effects/#funcdef-filter-sepia
+ Ok(GenericFilter::Sepia(
+ i.try_parse(|i| ZeroToOneFactor::parse(context, i))
+ .unwrap_or(ZeroToOneFactor::one()),
+ ))
+ },
+ "drop-shadow" => Ok(GenericFilter::DropShadow(Parse::parse(context, i)?)),
+ _ => Err(location.new_custom_error(
+ ValueParseErrorKind::InvalidFilter(Token::Function(function.clone()))
+ )),
+ }
+ })
+ }
+}
+
+impl Parse for SimpleShadow {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let color = input.try_parse(|i| Color::parse(context, i)).ok();
+ let horizontal = Length::parse(context, input)?;
+ let vertical = Length::parse(context, input)?;
+ let blur = input
+ .try_parse(|i| Length::parse_non_negative(context, i))
+ .ok();
+ let blur = blur.map(NonNegative::<Length>);
+ let color = color.or_else(|| input.try_parse(|i| Color::parse(context, i)).ok());
+
+ Ok(SimpleShadow {
+ color,
+ horizontal,
+ vertical,
+ blur,
+ })
+ }
+}
+
+impl ToComputedValue for SimpleShadow {
+ type ComputedValue = ComputedSimpleShadow;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ ComputedSimpleShadow {
+ color: self
+ .color
+ .as_ref()
+ .unwrap_or(&Color::currentcolor())
+ .to_computed_value(context),
+ horizontal: self.horizontal.to_computed_value(context),
+ vertical: self.vertical.to_computed_value(context),
+ blur: self
+ .blur
+ .as_ref()
+ .unwrap_or(&NonNegativeLength::zero())
+ .to_computed_value(context),
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ SimpleShadow {
+ color: Some(ToComputedValue::from_computed_value(&computed.color)),
+ horizontal: ToComputedValue::from_computed_value(&computed.horizontal),
+ vertical: ToComputedValue::from_computed_value(&computed.vertical),
+ blur: Some(ToComputedValue::from_computed_value(&computed.blur)),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/flex.rs b/servo/components/style/values/specified/flex.rs
new file mode 100644
index 0000000000..7c767cdf34
--- /dev/null
+++ b/servo/components/style/values/specified/flex.rs
@@ -0,0 +1,25 @@
+/* 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 types for CSS values related to flexbox.
+
+use crate::values::generics::flex::FlexBasis as GenericFlexBasis;
+use crate::values::specified::Size;
+
+/// A specified value for the `flex-basis` property.
+pub type FlexBasis = GenericFlexBasis<Size>;
+
+impl FlexBasis {
+ /// `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ GenericFlexBasis::Size(Size::auto())
+ }
+
+ /// `0%`
+ #[inline]
+ pub fn zero_percent() -> Self {
+ GenericFlexBasis::Size(Size::zero_percent())
+ }
+}
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())
+ },
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/gecko.rs b/servo/components/style/values/specified/gecko.rs
new file mode 100644
index 0000000000..e721add59c
--- /dev/null
+++ b/servo/components/style/values/specified/gecko.rs
@@ -0,0 +1,82 @@
+/* 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 types for legacy Gecko-only properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{self, Length, LengthPercentage};
+use crate::values::generics::rect::Rect;
+use cssparser::{Parser, Token};
+use std::fmt;
+use style_traits::values::SequenceWriter;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+fn parse_pixel_or_percent<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<LengthPercentage, ParseError<'i>> {
+ let location = input.current_source_location();
+ let token = input.next()?;
+ let value = match *token {
+ Token::Dimension {
+ value, ref unit, ..
+ } => {
+ match_ignore_ascii_case! { unit,
+ "px" => Ok(LengthPercentage::new_length(Length::new(value))),
+ _ => Err(()),
+ }
+ },
+ Token::Percentage { unit_value, .. } => Ok(LengthPercentage::new_percent(
+ computed::Percentage(unit_value),
+ )),
+ _ => Err(()),
+ };
+ value.map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+}
+
+/// The value of an IntersectionObserver's rootMargin property.
+///
+/// Only bare px or percentage values are allowed. Other length units and
+/// calc() values are not allowed.
+///
+/// <https://w3c.github.io/IntersectionObserver/#parse-a-root-margin>
+#[repr(transparent)]
+pub struct IntersectionObserverRootMargin(pub Rect<LengthPercentage>);
+
+impl Parse for IntersectionObserverRootMargin {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::Zero;
+ if input.is_exhausted() {
+ // If there are zero elements in tokens, set tokens to ["0px"].
+ return Ok(IntersectionObserverRootMargin(Rect::all(
+ LengthPercentage::zero(),
+ )));
+ }
+ let rect = Rect::parse_with(context, input, parse_pixel_or_percent)?;
+ Ok(IntersectionObserverRootMargin(rect))
+ }
+}
+
+// Strictly speaking this is not ToCss. It's serializing for DOM. But
+// we can just reuse the infrastructure of this.
+//
+// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin>
+impl ToCss for IntersectionObserverRootMargin {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ // We cannot use the ToCss impl of Rect, because that would
+ // merge items when they are equal. We want to list them all.
+ let mut writer = SequenceWriter::new(dest, " ");
+ let rect = &self.0;
+ writer.item(&rect.0)?;
+ writer.item(&rect.1)?;
+ writer.item(&rect.2)?;
+ writer.item(&rect.3)
+ }
+}
diff --git a/servo/components/style/values/specified/grid.rs b/servo/components/style/values/specified/grid.rs
new file mode 100644
index 0000000000..5c78ff399b
--- /dev/null
+++ b/servo/components/style/values/specified/grid.rs
@@ -0,0 +1,441 @@
+/* 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/. */
+
+//! CSS handling for the computed value of
+//! [grids](https://drafts.csswg.org/css-grid/)
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::grid::{GridTemplateComponent, ImplicitGridTracks, RepeatCount};
+use crate::values::generics::grid::{LineNameList, LineNameListValue, NameRepeat, TrackBreadth};
+use crate::values::generics::grid::{TrackList, TrackListValue, TrackRepeat, TrackSize};
+use crate::values::specified::{Integer, LengthPercentage};
+use crate::values::{CSSFloat, CustomIdent};
+use cssparser::{Parser, Token};
+use std::mem;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// Parse a single flexible length.
+pub fn parse_flex<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CSSFloat, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Dimension {
+ value, ref unit, ..
+ } if unit.eq_ignore_ascii_case("fr") && value.is_sign_positive() => Ok(value),
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+}
+
+impl<L> TrackBreadth<L> {
+ fn parse_keyword<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ #[derive(Parse)]
+ enum TrackKeyword {
+ Auto,
+ MaxContent,
+ MinContent,
+ }
+
+ Ok(match TrackKeyword::parse(input)? {
+ TrackKeyword::Auto => TrackBreadth::Auto,
+ TrackKeyword::MaxContent => TrackBreadth::MaxContent,
+ TrackKeyword::MinContent => TrackBreadth::MinContent,
+ })
+ }
+}
+
+impl Parse for TrackBreadth<LengthPercentage> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // FIXME: This and other callers in this file should use
+ // NonNegativeLengthPercentage instead.
+ //
+ // Though it seems these cannot be animated so it's ~ok.
+ if let Ok(lp) = input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) {
+ return Ok(TrackBreadth::Breadth(lp));
+ }
+
+ if let Ok(f) = input.try_parse(parse_flex) {
+ return Ok(TrackBreadth::Fr(f));
+ }
+
+ Self::parse_keyword(input)
+ }
+}
+
+impl Parse for TrackSize<LengthPercentage> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(b) = input.try_parse(|i| TrackBreadth::parse(context, i)) {
+ return Ok(TrackSize::Breadth(b));
+ }
+
+ if input
+ .try_parse(|i| i.expect_function_matching("minmax"))
+ .is_ok()
+ {
+ return input.parse_nested_block(|input| {
+ let inflexible_breadth =
+ match input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) {
+ Ok(lp) => TrackBreadth::Breadth(lp),
+ Err(..) => TrackBreadth::parse_keyword(input)?,
+ };
+
+ input.expect_comma()?;
+ Ok(TrackSize::Minmax(
+ inflexible_breadth,
+ TrackBreadth::parse(context, input)?,
+ ))
+ });
+ }
+
+ input.expect_function_matching("fit-content")?;
+ let lp = input.parse_nested_block(|i| LengthPercentage::parse_non_negative(context, i))?;
+ Ok(TrackSize::FitContent(TrackBreadth::Breadth(lp)))
+ }
+}
+
+impl Parse for ImplicitGridTracks<TrackSize<LengthPercentage>> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use style_traits::{Separator, Space};
+ let track_sizes = Space::parse(input, |i| TrackSize::parse(context, i))?;
+ if track_sizes.len() == 1 && track_sizes[0].is_initial() {
+ // A single track with the initial value is always represented by an empty slice.
+ return Ok(Default::default());
+ }
+ return Ok(ImplicitGridTracks(track_sizes.into()));
+ }
+}
+
+/// Parse the grid line names into a vector of owned strings.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-line-names>
+pub fn parse_line_names<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<crate::OwnedSlice<CustomIdent>, ParseError<'i>> {
+ input.expect_square_bracket_block()?;
+ input.parse_nested_block(|input| {
+ let mut values = vec![];
+ while let Ok(ident) = input.try_parse(|i| CustomIdent::parse(i, &["span", "auto"])) {
+ values.push(ident);
+ }
+
+ Ok(values.into())
+ })
+}
+
+/// The type of `repeat` function (only used in parsing).
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-repeat>
+#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+enum RepeatType {
+ /// [`<auto-repeat>`](https://drafts.csswg.org/css-grid/#typedef-auto-repeat)
+ Auto,
+ /// [`<track-repeat>`](https://drafts.csswg.org/css-grid/#typedef-track-repeat)
+ Normal,
+ /// [`<fixed-repeat>`](https://drafts.csswg.org/css-grid/#typedef-fixed-repeat)
+ Fixed,
+}
+
+impl TrackRepeat<LengthPercentage, Integer> {
+ fn parse_with_repeat_type<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(Self, RepeatType), ParseError<'i>> {
+ input
+ .try_parse(|i| i.expect_function_matching("repeat").map_err(|e| e.into()))
+ .and_then(|_| {
+ input.parse_nested_block(|input| {
+ let count = RepeatCount::parse(context, input)?;
+ input.expect_comma()?;
+
+ let is_auto = count == RepeatCount::AutoFit || count == RepeatCount::AutoFill;
+ let mut repeat_type = if is_auto {
+ RepeatType::Auto
+ } else {
+ // <fixed-size> is a subset of <track-size>, so it should work for both
+ RepeatType::Fixed
+ };
+
+ let mut names = vec![];
+ let mut values = vec![];
+ let mut current_names;
+
+ loop {
+ current_names = input.try_parse(parse_line_names).unwrap_or_default();
+ if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) {
+ if !track_size.is_fixed() {
+ if is_auto {
+ // should be <fixed-size> for <auto-repeat>
+ return Err(input
+ .new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if repeat_type == RepeatType::Fixed {
+ repeat_type = RepeatType::Normal // <track-size> for sure
+ }
+ }
+
+ values.push(track_size);
+ names.push(current_names);
+ } else {
+ if values.is_empty() {
+ // expecting at least one <track-size>
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ );
+ }
+
+ names.push(current_names); // final `<line-names>`
+ break; // no more <track-size>, breaking
+ }
+ }
+
+ let repeat = TrackRepeat {
+ count,
+ track_sizes: values.into(),
+ line_names: names.into(),
+ };
+
+ Ok((repeat, repeat_type))
+ })
+ })
+ }
+}
+
+impl Parse for TrackList<LengthPercentage, Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut current_names = vec![];
+ let mut names = vec![];
+ let mut values = vec![];
+
+ // Whether we've parsed an `<auto-repeat>` value.
+ let mut auto_repeat_index = None;
+ // assume that everything is <fixed-size>. This flag is useful when we encounter <auto-repeat>
+ let mut at_least_one_not_fixed = false;
+ loop {
+ current_names
+ .extend_from_slice(&mut input.try_parse(parse_line_names).unwrap_or_default());
+ if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) {
+ if !track_size.is_fixed() {
+ at_least_one_not_fixed = true;
+ if auto_repeat_index.is_some() {
+ // <auto-track-list> only accepts <fixed-size> and <fixed-repeat>
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ let vec = mem::replace(&mut current_names, vec![]);
+ names.push(vec.into());
+ values.push(TrackListValue::TrackSize(track_size));
+ } else if let Ok((repeat, type_)) =
+ input.try_parse(|i| TrackRepeat::parse_with_repeat_type(context, i))
+ {
+ match type_ {
+ RepeatType::Normal => {
+ at_least_one_not_fixed = true;
+ if auto_repeat_index.is_some() {
+ // only <fixed-repeat>
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ );
+ }
+ },
+ RepeatType::Auto => {
+ if auto_repeat_index.is_some() || at_least_one_not_fixed {
+ // We've either seen <auto-repeat> earlier, or there's at least one non-fixed value
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ );
+ }
+ auto_repeat_index = Some(values.len());
+ },
+ RepeatType::Fixed => {},
+ }
+
+ let vec = mem::replace(&mut current_names, vec![]);
+ names.push(vec.into());
+ values.push(TrackListValue::TrackRepeat(repeat));
+ } else {
+ if values.is_empty() && auto_repeat_index.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ names.push(current_names.into());
+ break;
+ }
+ }
+
+ Ok(TrackList {
+ auto_repeat_index: auto_repeat_index.unwrap_or(std::usize::MAX),
+ values: values.into(),
+ line_names: names.into(),
+ })
+ }
+}
+
+#[cfg(feature = "gecko")]
+#[inline]
+fn allow_grid_template_subgrids() -> bool {
+ true
+}
+
+#[cfg(feature = "servo")]
+#[inline]
+fn allow_grid_template_subgrids() -> bool {
+ false
+}
+
+#[cfg(feature = "gecko")]
+#[inline]
+fn allow_grid_template_masonry() -> bool {
+ static_prefs::pref!("layout.css.grid-template-masonry-value.enabled")
+}
+
+#[cfg(feature = "servo")]
+#[inline]
+fn allow_grid_template_masonry() -> bool {
+ false
+}
+
+impl Parse for GridTemplateComponent<LengthPercentage, Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(GridTemplateComponent::None);
+ }
+
+ Self::parse_without_none(context, input)
+ }
+}
+
+impl GridTemplateComponent<LengthPercentage, Integer> {
+ /// Parses a `GridTemplateComponent<LengthPercentage>` except `none` keyword.
+ pub fn parse_without_none<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if allow_grid_template_subgrids() {
+ if let Ok(t) = input.try_parse(|i| LineNameList::parse(context, i)) {
+ return Ok(GridTemplateComponent::Subgrid(Box::new(t)));
+ }
+ }
+ if allow_grid_template_masonry() {
+ if input
+ .try_parse(|i| i.expect_ident_matching("masonry"))
+ .is_ok()
+ {
+ return Ok(GridTemplateComponent::Masonry);
+ }
+ }
+ let track_list = TrackList::parse(context, input)?;
+ Ok(GridTemplateComponent::TrackList(Box::new(track_list)))
+ }
+}
+
+impl Parse for NameRepeat<Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("repeat")?;
+ input.parse_nested_block(|i| {
+ let count = RepeatCount::parse(context, i)?;
+ // NameRepeat doesn't accept `auto-fit`
+ // https://drafts.csswg.org/css-grid/#typedef-name-repeat
+ if matches!(count, RepeatCount::AutoFit) {
+ return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ i.expect_comma()?;
+ let mut names_list = vec![];
+ names_list.push(parse_line_names(i)?); // there should be at least one
+ while let Ok(names) = i.try_parse(parse_line_names) {
+ names_list.push(names);
+ }
+
+ Ok(NameRepeat {
+ count,
+ line_names: names_list.into(),
+ })
+ })
+ }
+}
+
+impl Parse for LineNameListValue<Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(repeat) = input.try_parse(|i| NameRepeat::parse(context, i)) {
+ return Ok(LineNameListValue::Repeat(repeat));
+ }
+
+ parse_line_names(input).map(LineNameListValue::LineNames)
+ }
+}
+
+impl LineNameListValue<Integer> {
+ /// Returns the length of `<line-names>` after expanding repeat(N, ...). This returns zero for
+ /// repeat(auto-fill, ...).
+ #[inline]
+ pub fn line_names_length(&self) -> usize {
+ match *self {
+ Self::LineNames(..) => 1,
+ Self::Repeat(ref r) => {
+ match r.count {
+ // Note: RepeatCount is always >= 1.
+ RepeatCount::Number(v) => r.line_names.len() * v.value() as usize,
+ _ => 0,
+ }
+ },
+ }
+ }
+}
+
+impl Parse for LineNameList<Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_ident_matching("subgrid")?;
+
+ let mut auto_repeat = false;
+ let mut expanded_line_names_length = 0;
+ let mut line_names = vec![];
+ while let Ok(value) = input.try_parse(|i| LineNameListValue::parse(context, i)) {
+ match value {
+ LineNameListValue::Repeat(ref r) if r.is_auto_fill() => {
+ if auto_repeat {
+ // On a subgridded axis, the auto-fill keyword is only valid once per
+ // <line-name-list>.
+ // https://drafts.csswg.org/css-grid/#auto-repeat
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ auto_repeat = true;
+ },
+ _ => (),
+ };
+
+ expanded_line_names_length += value.line_names_length();
+ line_names.push(value);
+ }
+
+ Ok(LineNameList {
+ expanded_line_names_length,
+ line_names: line_names.into(),
+ })
+ }
+}
diff --git a/servo/components/style/values/specified/image.rs b/servo/components/style/values/specified/image.rs
new file mode 100644
index 0000000000..76bbbf85df
--- /dev/null
+++ b/servo/components/style/values/specified/image.rs
@@ -0,0 +1,1340 @@
+/* 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/. */
+
+//! CSS handling for the specified value of
+//! [`image`][image]s
+//!
+//! [image]: https://drafts.csswg.org/css-images/#image-values
+
+use crate::color::mix::ColorInterpolationMethod;
+use crate::parser::{Parse, ParserContext};
+use crate::stylesheets::CorsMode;
+use crate::values::generics::color::ColorMixFlags;
+use crate::values::generics::image::{
+ self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent,
+};
+use crate::values::generics::image::{GradientFlags, PaintWorklet};
+use crate::values::generics::position::Position as GenericPosition;
+use crate::values::generics::NonNegative;
+use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
+use crate::values::specified::position::{Position, PositionComponent, Side};
+use crate::values::specified::url::SpecifiedImageUrl;
+use crate::values::specified::{
+ Angle, AngleOrPercentage, Color, Length, LengthPercentage, NonNegativeLength,
+ NonNegativeLengthPercentage, Resolution,
+};
+use crate::values::specified::{Number, NumberOrPercentage, Percentage};
+use crate::Atom;
+use cssparser::{Delimiter, Parser, Token};
+use selectors::parser::SelectorParseErrorKind;
+#[cfg(feature = "servo")]
+use servo_url::ServoUrl;
+use std::cmp::Ordering;
+use std::fmt::{self, Write};
+use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError};
+use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+#[inline]
+fn gradient_color_interpolation_method_enabled() -> bool {
+ static_prefs::pref!("layout.css.gradient-color-interpolation-method.enabled")
+}
+
+/// Specified values for an image according to CSS-IMAGES.
+/// <https://drafts.csswg.org/css-images/#image-values>
+pub type Image = generic::Image<Gradient, SpecifiedImageUrl, Color, Percentage, Resolution>;
+
+// Images should remain small, see https://github.com/servo/servo/pull/18430
+size_of_test!(Image, 16);
+
+/// Specified values for a CSS gradient.
+/// <https://drafts.csswg.org/css-images/#gradients>
+pub type Gradient = generic::Gradient<
+ LineDirection,
+ LengthPercentage,
+ NonNegativeLength,
+ NonNegativeLengthPercentage,
+ Position,
+ Angle,
+ AngleOrPercentage,
+ Color,
+>;
+
+/// Specified values for CSS cross-fade
+/// cross-fade( CrossFadeElement, ...)
+/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
+pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;
+/// CrossFadeElement = percent? CrossFadeImage
+pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>;
+/// CrossFadeImage = image | color
+pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>;
+
+/// `image-set()`
+pub type ImageSet = generic::ImageSet<Image, Resolution>;
+
+/// Each of the arguments to `image-set()`
+pub type ImageSetItem = generic::ImageSetItem<Image, Resolution>;
+
+type LengthPercentageItemList = crate::OwnedSlice<generic::GradientItem<Color, LengthPercentage>>;
+
+impl Color {
+ fn has_modern_syntax(&self) -> bool {
+ match self {
+ Self::Absolute(absolute) => !absolute.color.is_legacy_syntax(),
+ Self::ColorMix(mix) => {
+ if mix.flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
+ true
+ } else {
+ mix.left.has_modern_syntax() || mix.right.has_modern_syntax()
+ }
+ },
+ Self::LightDark(ld) => ld.light.has_modern_syntax() || ld.dark.has_modern_syntax(),
+
+ // The default is that this color doesn't have any modern syntax.
+ _ => false,
+ }
+ }
+}
+
+fn default_color_interpolation_method<T>(
+ items: &[generic::GradientItem<Color, T>],
+) -> ColorInterpolationMethod {
+ let has_modern_syntax_item = items.iter().any(|item| match item {
+ generic::GenericGradientItem::SimpleColorStop(color) => color.has_modern_syntax(),
+ generic::GenericGradientItem::ComplexColorStop { color, .. } => color.has_modern_syntax(),
+ generic::GenericGradientItem::InterpolationHint(_) => false,
+ });
+
+ if has_modern_syntax_item {
+ ColorInterpolationMethod::oklab()
+ } else {
+ ColorInterpolationMethod::srgb()
+ }
+}
+
+#[cfg(feature = "gecko")]
+fn cross_fade_enabled() -> bool {
+ static_prefs::pref!("layout.css.cross-fade.enabled")
+}
+
+#[cfg(feature = "servo")]
+fn cross_fade_enabled() -> bool {
+ false
+}
+
+impl SpecifiedValueInfo for Gradient {
+ const SUPPORTED_TYPES: u8 = CssType::GRADIENT;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ // This list here should keep sync with that in Gradient::parse.
+ f(&[
+ "linear-gradient",
+ "-webkit-linear-gradient",
+ "-moz-linear-gradient",
+ "repeating-linear-gradient",
+ "-webkit-repeating-linear-gradient",
+ "-moz-repeating-linear-gradient",
+ "radial-gradient",
+ "-webkit-radial-gradient",
+ "-moz-radial-gradient",
+ "repeating-radial-gradient",
+ "-webkit-repeating-radial-gradient",
+ "-moz-repeating-radial-gradient",
+ "-webkit-gradient",
+ "conic-gradient",
+ "repeating-conic-gradient",
+ ]);
+ }
+}
+
+// Need to manually implement as whether or not cross-fade shows up in
+// completions & etc is dependent on it being enabled.
+impl<Image, Color, Percentage> SpecifiedValueInfo for generic::CrossFade<Image, Color, Percentage> {
+ const SUPPORTED_TYPES: u8 = 0;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ if cross_fade_enabled() {
+ f(&["cross-fade"]);
+ }
+ }
+}
+
+impl<Image, Resolution> SpecifiedValueInfo for generic::ImageSet<Image, Resolution> {
+ const SUPPORTED_TYPES: u8 = 0;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["image-set"]);
+ }
+}
+
+/// A specified gradient line direction.
+///
+/// FIXME(emilio): This should be generic over Angle.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum LineDirection {
+ /// An angular direction.
+ Angle(Angle),
+ /// A horizontal direction.
+ Horizontal(HorizontalPositionKeyword),
+ /// A vertical direction.
+ Vertical(VerticalPositionKeyword),
+ /// A direction towards a corner of a box.
+ Corner(HorizontalPositionKeyword, VerticalPositionKeyword),
+}
+
+/// A specified ending shape.
+pub type EndingShape = generic::EndingShape<NonNegativeLength, NonNegativeLengthPercentage>;
+
+bitflags! {
+ #[derive(Clone, Copy)]
+ struct ParseImageFlags: u8 {
+ const FORBID_NONE = 1 << 0;
+ const FORBID_IMAGE_SET = 1 << 1;
+ const FORBID_NON_URL = 1 << 2;
+ }
+}
+
+impl Parse for Image {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Image, ParseError<'i>> {
+ Image::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::empty())
+ }
+}
+
+impl Image {
+ fn parse_with_cors_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Image, ParseError<'i>> {
+ if !flags.contains(ParseImageFlags::FORBID_NONE) &&
+ input.try_parse(|i| i.expect_ident_matching("none")).is_ok()
+ {
+ return Ok(generic::Image::None);
+ }
+
+ if let Ok(url) = input
+ .try_parse(|input| SpecifiedImageUrl::parse_with_cors_mode(context, input, cors_mode))
+ {
+ return Ok(generic::Image::Url(url));
+ }
+
+ if !flags.contains(ParseImageFlags::FORBID_IMAGE_SET) {
+ if let Ok(is) =
+ input.try_parse(|input| ImageSet::parse(context, input, cors_mode, flags))
+ {
+ return Ok(generic::Image::ImageSet(Box::new(is)));
+ }
+ }
+
+ if flags.contains(ParseImageFlags::FORBID_NON_URL) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) {
+ return Ok(generic::Image::Gradient(Box::new(gradient)));
+ }
+
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(|input| {
+ Ok(match_ignore_ascii_case! { &function,
+ #[cfg(feature = "servo-layout-2013")]
+ "paint" => Self::PaintWorklet(PaintWorklet::parse_args(context, input)?),
+ "cross-fade" if cross_fade_enabled() => Self::CrossFade(Box::new(CrossFade::parse_args(context, input, cors_mode, flags)?)),
+ #[cfg(feature = "gecko")]
+ "-moz-element" => Self::Element(Self::parse_element(input)?),
+ _ => return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function))),
+ })
+ })
+ }
+}
+
+impl Image {
+ /// Creates an already specified image value from an already resolved URL
+ /// for insertion in the cascade.
+ #[cfg(feature = "servo")]
+ pub fn for_cascade(url: ServoUrl) -> Self {
+ use crate::values::CssUrl;
+ generic::Image::Url(CssUrl::for_cascade(url))
+ }
+
+ /// Parses a `-moz-element(# <element-id>)`.
+ #[cfg(feature = "gecko")]
+ fn parse_element<'i>(input: &mut Parser<'i, '_>) -> Result<Atom, ParseError<'i>> {
+ let location = input.current_source_location();
+ Ok(match *input.next()? {
+ Token::IDHash(ref id) => Atom::from(id.as_ref()),
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ })
+ }
+
+ /// Provides an alternate method for parsing that associates the URL with
+ /// anonymous CORS headers.
+ pub fn parse_with_cors_anonymous<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Image, ParseError<'i>> {
+ Self::parse_with_cors_mode(
+ context,
+ input,
+ CorsMode::Anonymous,
+ ParseImageFlags::empty(),
+ )
+ }
+
+ /// Provides an alternate method for parsing, but forbidding `none`
+ pub fn parse_forbid_none<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Image, ParseError<'i>> {
+ Self::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::FORBID_NONE)
+ }
+
+ /// Provides an alternate method for parsing, but only for urls.
+ pub fn parse_only_url<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Image, ParseError<'i>> {
+ Self::parse_with_cors_mode(
+ context,
+ input,
+ CorsMode::None,
+ ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_NON_URL,
+ )
+ }
+}
+
+impl CrossFade {
+ /// cross-fade() = cross-fade( <cf-image># )
+ fn parse_args<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ let elements = crate::OwnedSlice::from(input.parse_comma_separated(|input| {
+ CrossFadeElement::parse(context, input, cors_mode, flags)
+ })?);
+ Ok(Self { elements })
+ }
+}
+
+impl CrossFadeElement {
+ fn parse_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Option<Percentage> {
+ // We clamp our values here as this is the way that Safari and Chrome's
+ // implementation handle out-of-bounds percentages but whether or not
+ // this behavior follows the specification is still being discussed.
+ // See: <https://github.com/w3c/csswg-drafts/issues/5333>
+ input
+ .try_parse(|input| Percentage::parse_non_negative(context, input))
+ .ok()
+ .map(|p| p.clamp_to_hundred())
+ }
+
+ /// <cf-image> = <percentage>? && [ <image> | <color> ]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ // Try and parse a leading percent sign.
+ let mut percent = Self::parse_percentage(context, input);
+ // Parse the image
+ let image = CrossFadeImage::parse(context, input, cors_mode, flags)?;
+ // Try and parse a trailing percent sign.
+ if percent.is_none() {
+ percent = Self::parse_percentage(context, input);
+ }
+ Ok(Self {
+ percent: percent.into(),
+ image,
+ })
+ }
+}
+
+impl CrossFadeImage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(image) = input.try_parse(|input| {
+ Image::parse_with_cors_mode(
+ context,
+ input,
+ cors_mode,
+ flags | ParseImageFlags::FORBID_NONE,
+ )
+ }) {
+ return Ok(Self::Image(image));
+ }
+ Ok(Self::Color(Color::parse(context, input)?))
+ }
+}
+
+impl ImageSet {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ let function = input.expect_function()?;
+ match_ignore_ascii_case! { &function,
+ "-webkit-image-set" | "image-set" => {},
+ _ => {
+ let func = function.clone();
+ return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func)));
+ }
+ }
+ let items = input.parse_nested_block(|input| {
+ input.parse_comma_separated(|input| {
+ ImageSetItem::parse(context, input, cors_mode, flags)
+ })
+ })?;
+ Ok(Self {
+ selected_index: std::usize::MAX,
+ items: items.into(),
+ })
+ }
+}
+
+impl ImageSetItem {
+ fn parse_type<'i>(p: &mut Parser<'i, '_>) -> Result<crate::OwnedStr, ParseError<'i>> {
+ p.expect_function_matching("type")?;
+ p.parse_nested_block(|input| Ok(input.expect_string()?.as_ref().to_owned().into()))
+ }
+
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ let image = match input.try_parse(|i| i.expect_url_or_string()) {
+ Ok(url) => Image::Url(SpecifiedImageUrl::parse_from_string(
+ url.as_ref().into(),
+ context,
+ cors_mode,
+ )),
+ Err(..) => Image::parse_with_cors_mode(
+ context,
+ input,
+ cors_mode,
+ flags | ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_IMAGE_SET,
+ )?,
+ };
+
+ let mut resolution = input
+ .try_parse(|input| Resolution::parse(context, input))
+ .ok();
+ let mime_type = input.try_parse(Self::parse_type).ok();
+
+ // Try to parse resolution after type().
+ if mime_type.is_some() && resolution.is_none() {
+ resolution = input
+ .try_parse(|input| Resolution::parse(context, input))
+ .ok();
+ }
+
+ let resolution = resolution.unwrap_or_else(|| Resolution::from_x(1.0));
+ let has_mime_type = mime_type.is_some();
+ let mime_type = mime_type.unwrap_or_default();
+
+ Ok(Self {
+ image,
+ resolution,
+ has_mime_type,
+ mime_type,
+ })
+ }
+}
+
+impl Parse for Gradient {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ enum Shape {
+ Linear,
+ Radial,
+ Conic,
+ }
+
+ let func = input.expect_function()?;
+ let (shape, repeating, compat_mode) = match_ignore_ascii_case! { &func,
+ "linear-gradient" => {
+ (Shape::Linear, false, GradientCompatMode::Modern)
+ },
+ "-webkit-linear-gradient" => {
+ (Shape::Linear, false, GradientCompatMode::WebKit)
+ },
+ #[cfg(feature = "gecko")]
+ "-moz-linear-gradient" => {
+ (Shape::Linear, false, GradientCompatMode::Moz)
+ },
+ "repeating-linear-gradient" => {
+ (Shape::Linear, true, GradientCompatMode::Modern)
+ },
+ "-webkit-repeating-linear-gradient" => {
+ (Shape::Linear, true, GradientCompatMode::WebKit)
+ },
+ #[cfg(feature = "gecko")]
+ "-moz-repeating-linear-gradient" => {
+ (Shape::Linear, true, GradientCompatMode::Moz)
+ },
+ "radial-gradient" => {
+ (Shape::Radial, false, GradientCompatMode::Modern)
+ },
+ "-webkit-radial-gradient" => {
+ (Shape::Radial, false, GradientCompatMode::WebKit)
+ },
+ #[cfg(feature = "gecko")]
+ "-moz-radial-gradient" => {
+ (Shape::Radial, false, GradientCompatMode::Moz)
+ },
+ "repeating-radial-gradient" => {
+ (Shape::Radial, true, GradientCompatMode::Modern)
+ },
+ "-webkit-repeating-radial-gradient" => {
+ (Shape::Radial, true, GradientCompatMode::WebKit)
+ },
+ #[cfg(feature = "gecko")]
+ "-moz-repeating-radial-gradient" => {
+ (Shape::Radial, true, GradientCompatMode::Moz)
+ },
+ "conic-gradient" => {
+ (Shape::Conic, false, GradientCompatMode::Modern)
+ },
+ "repeating-conic-gradient" => {
+ (Shape::Conic, true, GradientCompatMode::Modern)
+ },
+ "-webkit-gradient" => {
+ return input.parse_nested_block(|i| {
+ Self::parse_webkit_gradient_argument(context, i)
+ });
+ },
+ _ => {
+ let func = func.clone();
+ return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func)));
+ }
+ };
+
+ Ok(input.parse_nested_block(|i| {
+ Ok(match shape {
+ Shape::Linear => Self::parse_linear(context, i, repeating, compat_mode)?,
+ Shape::Radial => Self::parse_radial(context, i, repeating, compat_mode)?,
+ Shape::Conic => Self::parse_conic(context, i, repeating)?,
+ })
+ })?)
+ }
+}
+
+impl Gradient {
+ fn parse_webkit_gradient_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::position::{
+ HorizontalPositionKeyword as X, VerticalPositionKeyword as Y,
+ };
+ type Point = GenericPosition<Component<X>, Component<Y>>;
+
+ #[derive(Clone, Copy, Parse)]
+ enum Component<S> {
+ Center,
+ Number(NumberOrPercentage),
+ Side(S),
+ }
+
+ impl LineDirection {
+ fn from_points(first: Point, second: Point) -> Self {
+ let h_ord = first.horizontal.partial_cmp(&second.horizontal);
+ let v_ord = first.vertical.partial_cmp(&second.vertical);
+ let (h, v) = match (h_ord, v_ord) {
+ (Some(h), Some(v)) => (h, v),
+ _ => return LineDirection::Vertical(Y::Bottom),
+ };
+ match (h, v) {
+ (Ordering::Less, Ordering::Less) => LineDirection::Corner(X::Right, Y::Bottom),
+ (Ordering::Less, Ordering::Equal) => LineDirection::Horizontal(X::Right),
+ (Ordering::Less, Ordering::Greater) => LineDirection::Corner(X::Right, Y::Top),
+ (Ordering::Equal, Ordering::Greater) => LineDirection::Vertical(Y::Top),
+ (Ordering::Equal, Ordering::Equal) | (Ordering::Equal, Ordering::Less) => {
+ LineDirection::Vertical(Y::Bottom)
+ },
+ (Ordering::Greater, Ordering::Less) => {
+ LineDirection::Corner(X::Left, Y::Bottom)
+ },
+ (Ordering::Greater, Ordering::Equal) => LineDirection::Horizontal(X::Left),
+ (Ordering::Greater, Ordering::Greater) => {
+ LineDirection::Corner(X::Left, Y::Top)
+ },
+ }
+ }
+ }
+
+ impl From<Point> for Position {
+ fn from(point: Point) -> Self {
+ Self::new(point.horizontal.into(), point.vertical.into())
+ }
+ }
+
+ impl Parse for Point {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.try_parse(|i| {
+ let x = Component::parse(context, i)?;
+ let y = Component::parse(context, i)?;
+
+ Ok(Self::new(x, y))
+ })
+ }
+ }
+
+ impl<S: Side> From<Component<S>> for NumberOrPercentage {
+ fn from(component: Component<S>) -> Self {
+ match component {
+ Component::Center => NumberOrPercentage::Percentage(Percentage::new(0.5)),
+ Component::Number(number) => number,
+ Component::Side(side) => {
+ let p = if side.is_start() {
+ Percentage::zero()
+ } else {
+ Percentage::hundred()
+ };
+ NumberOrPercentage::Percentage(p)
+ },
+ }
+ }
+ }
+
+ impl<S: Side> From<Component<S>> for PositionComponent<S> {
+ fn from(component: Component<S>) -> Self {
+ match component {
+ Component::Center => PositionComponent::Center,
+ Component::Number(NumberOrPercentage::Number(number)) => {
+ PositionComponent::Length(Length::from_px(number.value).into())
+ },
+ Component::Number(NumberOrPercentage::Percentage(p)) => {
+ PositionComponent::Length(p.into())
+ },
+ Component::Side(side) => PositionComponent::Side(side, None),
+ }
+ }
+ }
+
+ impl<S: Copy + Side> Component<S> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ match (
+ NumberOrPercentage::from(*self),
+ NumberOrPercentage::from(*other),
+ ) {
+ (NumberOrPercentage::Percentage(a), NumberOrPercentage::Percentage(b)) => {
+ a.get().partial_cmp(&b.get())
+ },
+ (NumberOrPercentage::Number(a), NumberOrPercentage::Number(b)) => {
+ a.value.partial_cmp(&b.value)
+ },
+ (_, _) => None,
+ }
+ }
+ }
+
+ let ident = input.expect_ident_cloned()?;
+ input.expect_comma()?;
+
+ Ok(match_ignore_ascii_case! { &ident,
+ "linear" => {
+ let first = Point::parse(context, input)?;
+ input.expect_comma()?;
+ let second = Point::parse(context, input)?;
+
+ let direction = LineDirection::from_points(first, second);
+ let items = Gradient::parse_webkit_gradient_stops(context, input, false)?;
+
+ generic::Gradient::Linear {
+ direction,
+ color_interpolation_method: ColorInterpolationMethod::srgb(),
+ items,
+ // Legacy gradients always use srgb as a default.
+ flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ compat_mode: GradientCompatMode::Modern,
+ }
+ },
+ "radial" => {
+ let first_point = Point::parse(context, input)?;
+ input.expect_comma()?;
+ let first_radius = Number::parse_non_negative(context, input)?;
+ input.expect_comma()?;
+ let second_point = Point::parse(context, input)?;
+ input.expect_comma()?;
+ let second_radius = Number::parse_non_negative(context, input)?;
+
+ let (reverse_stops, point, radius) = if second_radius.value >= first_radius.value {
+ (false, second_point, second_radius)
+ } else {
+ (true, first_point, first_radius)
+ };
+
+ let rad = Circle::Radius(NonNegative(Length::from_px(radius.value)));
+ let shape = generic::EndingShape::Circle(rad);
+ let position: Position = point.into();
+ let items = Gradient::parse_webkit_gradient_stops(context, input, reverse_stops)?;
+
+ generic::Gradient::Radial {
+ shape,
+ position,
+ color_interpolation_method: ColorInterpolationMethod::srgb(),
+ items,
+ // Legacy gradients always use srgb as a default.
+ flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ compat_mode: GradientCompatMode::Modern,
+ }
+ },
+ _ => {
+ let e = SelectorParseErrorKind::UnexpectedIdent(ident.clone());
+ return Err(input.new_custom_error(e));
+ },
+ })
+ }
+
+ fn parse_webkit_gradient_stops<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ reverse_stops: bool,
+ ) -> Result<LengthPercentageItemList, ParseError<'i>> {
+ let mut items = input
+ .try_parse(|i| {
+ i.expect_comma()?;
+ i.parse_comma_separated(|i| {
+ let function = i.expect_function()?.clone();
+ let (color, mut p) = i.parse_nested_block(|i| {
+ let p = match_ignore_ascii_case! { &function,
+ "color-stop" => {
+ let p = NumberOrPercentage::parse(context, i)?.to_percentage();
+ i.expect_comma()?;
+ p
+ },
+ "from" => Percentage::zero(),
+ "to" => Percentage::hundred(),
+ _ => {
+ return Err(i.new_custom_error(
+ StyleParseErrorKind::UnexpectedFunction(function.clone())
+ ))
+ },
+ };
+ let color = Color::parse(context, i)?;
+ if color == Color::CurrentColor {
+ return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok((color.into(), p))
+ })?;
+ if reverse_stops {
+ p.reverse();
+ }
+ Ok(generic::GradientItem::ComplexColorStop {
+ color,
+ position: p.into(),
+ })
+ })
+ })
+ .unwrap_or(vec![]);
+
+ if items.is_empty() {
+ items = vec![
+ generic::GradientItem::ComplexColorStop {
+ color: Color::transparent(),
+ position: LengthPercentage::zero_percent(),
+ },
+ generic::GradientItem::ComplexColorStop {
+ color: Color::transparent(),
+ position: LengthPercentage::hundred_percent(),
+ },
+ ];
+ } else if items.len() == 1 {
+ let first = items[0].clone();
+ items.push(first);
+ } else {
+ items.sort_by(|a, b| {
+ match (a, b) {
+ (
+ &generic::GradientItem::ComplexColorStop {
+ position: ref a_position,
+ ..
+ },
+ &generic::GradientItem::ComplexColorStop {
+ position: ref b_position,
+ ..
+ },
+ ) => match (a_position, b_position) {
+ (&LengthPercentage::Percentage(a), &LengthPercentage::Percentage(b)) => {
+ return a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal);
+ },
+ _ => {},
+ },
+ _ => {},
+ }
+ if reverse_stops {
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+ })
+ }
+ Ok(items.into())
+ }
+
+ /// Not used for -webkit-gradient syntax and conic-gradient
+ fn parse_stops<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<LengthPercentageItemList, ParseError<'i>> {
+ let items =
+ generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?;
+ if items.len() < 2 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(items)
+ }
+
+ /// Try to parse a color interpolation method.
+ fn try_parse_color_interpolation_method<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Option<ColorInterpolationMethod> {
+ if gradient_color_interpolation_method_enabled() {
+ input
+ .try_parse(|i| ColorInterpolationMethod::parse(context, i))
+ .ok()
+ } else {
+ None
+ }
+ }
+
+ /// Parses a linear gradient.
+ /// GradientCompatMode can change during `-moz-` prefixed gradient parsing if it come across a `to` keyword.
+ fn parse_linear<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ repeating: bool,
+ mut compat_mode: GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut flags = GradientFlags::empty();
+ flags.set(GradientFlags::REPEATING, repeating);
+
+ let mut color_interpolation_method =
+ Self::try_parse_color_interpolation_method(context, input);
+
+ let direction = input
+ .try_parse(|p| LineDirection::parse(context, p, &mut compat_mode))
+ .ok();
+
+ if direction.is_some() && color_interpolation_method.is_none() {
+ color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
+ }
+
+ // If either of the 2 options were specified, we require a comma.
+ if color_interpolation_method.is_some() || direction.is_some() {
+ input.expect_comma()?;
+ }
+
+ let items = Gradient::parse_stops(context, input)?;
+
+ let default = default_color_interpolation_method(&items);
+ let color_interpolation_method = color_interpolation_method.unwrap_or(default);
+ flags.set(
+ GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ default == color_interpolation_method,
+ );
+
+ let direction = direction.unwrap_or(match compat_mode {
+ GradientCompatMode::Modern => LineDirection::Vertical(VerticalPositionKeyword::Bottom),
+ _ => LineDirection::Vertical(VerticalPositionKeyword::Top),
+ });
+
+ Ok(Gradient::Linear {
+ direction,
+ color_interpolation_method,
+ items,
+ flags,
+ compat_mode,
+ })
+ }
+
+ /// Parses a radial gradient.
+ fn parse_radial<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ repeating: bool,
+ compat_mode: GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut flags = GradientFlags::empty();
+ flags.set(GradientFlags::REPEATING, repeating);
+
+ let mut color_interpolation_method =
+ Self::try_parse_color_interpolation_method(context, input);
+
+ let (shape, position) = match compat_mode {
+ GradientCompatMode::Modern => {
+ let shape = input.try_parse(|i| EndingShape::parse(context, i, compat_mode));
+ let position = input.try_parse(|i| {
+ i.expect_ident_matching("at")?;
+ Position::parse(context, i)
+ });
+ (shape, position.ok())
+ },
+ _ => {
+ let position = input.try_parse(|i| Position::parse(context, i));
+ let shape = input.try_parse(|i| {
+ if position.is_ok() {
+ i.expect_comma()?;
+ }
+ EndingShape::parse(context, i, compat_mode)
+ });
+ (shape, position.ok())
+ },
+ };
+
+ let has_shape_or_position = shape.is_ok() || position.is_some();
+ if has_shape_or_position && color_interpolation_method.is_none() {
+ color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
+ }
+
+ if has_shape_or_position || color_interpolation_method.is_some() {
+ input.expect_comma()?;
+ }
+
+ let shape = shape.unwrap_or({
+ generic::EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner))
+ });
+
+ let position = position.unwrap_or(Position::center());
+
+ let items = Gradient::parse_stops(context, input)?;
+
+ let default = default_color_interpolation_method(&items);
+ let color_interpolation_method = color_interpolation_method.unwrap_or(default);
+ flags.set(
+ GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ default == color_interpolation_method,
+ );
+
+ Ok(Gradient::Radial {
+ shape,
+ position,
+ color_interpolation_method,
+ items,
+ flags,
+ compat_mode,
+ })
+ }
+
+ /// Parse a conic gradient.
+ fn parse_conic<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ repeating: bool,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut flags = GradientFlags::empty();
+ flags.set(GradientFlags::REPEATING, repeating);
+
+ let mut color_interpolation_method =
+ Self::try_parse_color_interpolation_method(context, input);
+
+ let angle = input.try_parse(|i| {
+ i.expect_ident_matching("from")?;
+ // Spec allows unitless zero start angles
+ // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle
+ Angle::parse_with_unitless(context, i)
+ });
+ let position = input.try_parse(|i| {
+ i.expect_ident_matching("at")?;
+ Position::parse(context, i)
+ });
+
+ let has_angle_or_position = angle.is_ok() || position.is_ok();
+ if has_angle_or_position && color_interpolation_method.is_none() {
+ color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
+ }
+
+ if has_angle_or_position || color_interpolation_method.is_some() {
+ input.expect_comma()?;
+ }
+
+ let angle = angle.unwrap_or(Angle::zero());
+
+ let position = position.unwrap_or(Position::center());
+
+ let items = generic::GradientItem::parse_comma_separated(
+ context,
+ input,
+ AngleOrPercentage::parse_with_unitless,
+ )?;
+
+ if items.len() < 2 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let default = default_color_interpolation_method(&items);
+ let color_interpolation_method = color_interpolation_method.unwrap_or(default);
+ flags.set(
+ GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ default == color_interpolation_method,
+ );
+
+ Ok(Gradient::Conic {
+ angle,
+ position,
+ color_interpolation_method,
+ items,
+ flags,
+ })
+ }
+}
+
+impl generic::LineDirection for LineDirection {
+ fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool {
+ match *self {
+ LineDirection::Angle(ref angle) => angle.degrees() == 180.0,
+ LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
+ compat_mode == GradientCompatMode::Modern
+ },
+ LineDirection::Vertical(VerticalPositionKeyword::Top) => {
+ compat_mode != GradientCompatMode::Modern
+ },
+ _ => false,
+ }
+ }
+
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ LineDirection::Angle(angle) => angle.to_css(dest),
+ LineDirection::Horizontal(x) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ x.to_css(dest)
+ },
+ LineDirection::Vertical(y) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ y.to_css(dest)
+ },
+ LineDirection::Corner(x, y) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ x.to_css(dest)?;
+ dest.write_char(' ')?;
+ y.to_css(dest)
+ },
+ }
+ }
+}
+
+impl LineDirection {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ compat_mode: &mut GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ // Gradients allow unitless zero angles as an exception, see:
+ // https://github.com/w3c/csswg-drafts/issues/1162
+ if let Ok(angle) = input.try_parse(|i| Angle::parse_with_unitless(context, i)) {
+ return Ok(LineDirection::Angle(angle));
+ }
+
+ input.try_parse(|i| {
+ let to_ident = i.try_parse(|i| i.expect_ident_matching("to"));
+ match *compat_mode {
+ // `to` keyword is mandatory in modern syntax.
+ GradientCompatMode::Modern => to_ident?,
+ // Fall back to Modern compatibility mode in case there is a `to` keyword.
+ // According to Gecko, `-moz-linear-gradient(to ...)` should serialize like
+ // `linear-gradient(to ...)`.
+ GradientCompatMode::Moz if to_ident.is_ok() => {
+ *compat_mode = GradientCompatMode::Modern
+ },
+ // There is no `to` keyword in webkit prefixed syntax. If it's consumed,
+ // parsing should throw an error.
+ GradientCompatMode::WebKit if to_ident.is_ok() => {
+ return Err(
+ i.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("to".into()))
+ );
+ },
+ _ => {},
+ }
+
+ if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) {
+ if let Ok(y) = i.try_parse(VerticalPositionKeyword::parse) {
+ return Ok(LineDirection::Corner(x, y));
+ }
+ return Ok(LineDirection::Horizontal(x));
+ }
+ let y = VerticalPositionKeyword::parse(i)?;
+ if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) {
+ return Ok(LineDirection::Corner(x, y));
+ }
+ Ok(LineDirection::Vertical(y))
+ })
+ }
+}
+
+impl EndingShape {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ compat_mode: GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
+ {
+ if input
+ .try_parse(|i| i.expect_ident_matching("circle"))
+ .is_ok()
+ {
+ return Ok(generic::EndingShape::Circle(Circle::Extent(extent)));
+ }
+ let _ = input.try_parse(|i| i.expect_ident_matching("ellipse"));
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent)));
+ }
+ if input
+ .try_parse(|i| i.expect_ident_matching("circle"))
+ .is_ok()
+ {
+ if let Ok(extent) =
+ input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
+ {
+ return Ok(generic::EndingShape::Circle(Circle::Extent(extent)));
+ }
+ if compat_mode == GradientCompatMode::Modern {
+ if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
+ return Ok(generic::EndingShape::Circle(Circle::Radius(length)));
+ }
+ }
+ return Ok(generic::EndingShape::Circle(Circle::Extent(
+ ShapeExtent::FarthestCorner,
+ )));
+ }
+ if input
+ .try_parse(|i| i.expect_ident_matching("ellipse"))
+ .is_ok()
+ {
+ if let Ok(extent) =
+ input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
+ {
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent)));
+ }
+ if compat_mode == GradientCompatMode::Modern {
+ let pair: Result<_, ParseError> = input.try_parse(|i| {
+ let x = NonNegativeLengthPercentage::parse(context, i)?;
+ let y = NonNegativeLengthPercentage::parse(context, i)?;
+ Ok((x, y))
+ });
+ if let Ok((x, y)) = pair {
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(x, y)));
+ }
+ }
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(
+ ShapeExtent::FarthestCorner,
+ )));
+ }
+ if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
+ if let Ok(y) = input.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) {
+ if compat_mode == GradientCompatMode::Modern {
+ let _ = input.try_parse(|i| i.expect_ident_matching("ellipse"));
+ }
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
+ NonNegative(LengthPercentage::from(length.0)),
+ y,
+ )));
+ }
+ if compat_mode == GradientCompatMode::Modern {
+ let y = input.try_parse(|i| {
+ i.expect_ident_matching("ellipse")?;
+ NonNegativeLengthPercentage::parse(context, i)
+ });
+ if let Ok(y) = y {
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
+ NonNegative(LengthPercentage::from(length.0)),
+ y,
+ )));
+ }
+ let _ = input.try_parse(|i| i.expect_ident_matching("circle"));
+ }
+
+ return Ok(generic::EndingShape::Circle(Circle::Radius(length)));
+ }
+ input.try_parse(|i| {
+ let x = Percentage::parse_non_negative(context, i)?;
+ let y = if let Ok(y) = i.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) {
+ if compat_mode == GradientCompatMode::Modern {
+ let _ = i.try_parse(|i| i.expect_ident_matching("ellipse"));
+ }
+ y
+ } else {
+ if compat_mode == GradientCompatMode::Modern {
+ i.expect_ident_matching("ellipse")?;
+ }
+ NonNegativeLengthPercentage::parse(context, i)?
+ };
+ Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
+ NonNegative(LengthPercentage::from(x)),
+ y,
+ )))
+ })
+ }
+}
+
+impl ShapeExtent {
+ fn parse_with_compat_mode<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ compat_mode: GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ match Self::parse(input)? {
+ ShapeExtent::Contain | ShapeExtent::Cover
+ if compat_mode == GradientCompatMode::Modern =>
+ {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ ShapeExtent::Contain => Ok(ShapeExtent::ClosestSide),
+ ShapeExtent::Cover => Ok(ShapeExtent::FarthestCorner),
+ keyword => Ok(keyword),
+ }
+ }
+}
+
+impl<T> generic::GradientItem<Color, T> {
+ fn parse_comma_separated<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse_position: impl for<'i1, 't1> Fn(&ParserContext, &mut Parser<'i1, 't1>) -> Result<T, ParseError<'i1>>
+ + Copy,
+ ) -> Result<crate::OwnedSlice<Self>, ParseError<'i>> {
+ let mut items = Vec::new();
+ let mut seen_stop = false;
+
+ loop {
+ input.parse_until_before(Delimiter::Comma, |input| {
+ if seen_stop {
+ if let Ok(hint) = input.try_parse(|i| parse_position(context, i)) {
+ seen_stop = false;
+ items.push(generic::GradientItem::InterpolationHint(hint));
+ return Ok(());
+ }
+ }
+
+ let stop = generic::ColorStop::parse(context, input, parse_position)?;
+
+ if let Ok(multi_position) = input.try_parse(|i| parse_position(context, i)) {
+ let stop_color = stop.color.clone();
+ items.push(stop.into_item());
+ items.push(
+ generic::ColorStop {
+ color: stop_color,
+ position: Some(multi_position),
+ }
+ .into_item(),
+ );
+ } else {
+ items.push(stop.into_item());
+ }
+
+ seen_stop = true;
+ Ok(())
+ })?;
+
+ match input.next() {
+ Err(_) => break,
+ Ok(&Token::Comma) => continue,
+ Ok(_) => unreachable!(),
+ }
+ }
+
+ if !seen_stop || items.len() < 2 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(items.into())
+ }
+}
+
+impl<T> generic::ColorStop<Color, T> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse_position: impl for<'i1, 't1> Fn(
+ &ParserContext,
+ &mut Parser<'i1, 't1>,
+ ) -> Result<T, ParseError<'i1>>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(generic::ColorStop {
+ color: Color::parse(context, input)?,
+ position: input.try_parse(|i| parse_position(context, i)).ok(),
+ })
+ }
+}
+
+impl PaintWorklet {
+ #[cfg(feature = "servo")]
+ fn parse_args<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ use crate::custom_properties::SpecifiedValue;
+ let name = Atom::from(&**input.expect_ident()?);
+ let arguments = input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ input.parse_comma_separated(SpecifiedValue::parse)
+ })
+ .unwrap_or_default();
+ Ok(Self { name, arguments })
+ }
+}
+
+/// https://drafts.csswg.org/css-images/#propdef-image-rendering
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ImageRendering {
+ Auto,
+ Smooth,
+ #[parse(aliases = "-moz-crisp-edges")]
+ CrispEdges,
+ Pixelated,
+ // From the spec:
+ //
+ // This property previously accepted the values optimizeSpeed and
+ // optimizeQuality. These are now deprecated; a user agent must accept
+ // them as valid values but must treat them as having the same behavior
+ // as crisp-edges and smooth respectively, and authors must not use
+ // them.
+ //
+ Optimizespeed,
+ Optimizequality,
+}
diff --git a/servo/components/style/values/specified/length.rs b/servo/components/style/values/specified/length.rs
new file mode 100644
index 0000000000..d2e1d7d346
--- /dev/null
+++ b/servo/components/style/values/specified/length.rs
@@ -0,0 +1,2031 @@
+/* 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/. */
+
+//! [Length values][length].
+//!
+//! [length]: https://drafts.csswg.org/css-values/#lengths
+
+use super::{AllowQuirks, Number, Percentage, ToComputedValue};
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::font_metrics::{FontMetrics, FontMetricsOrientation};
+use crate::gecko_bindings::structs::GeckoFontMetrics;
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{self, CSSPixelLength, Context};
+use crate::values::generics::length as generics;
+use crate::values::generics::length::{
+ GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize,
+};
+use crate::values::generics::NonNegative;
+use crate::values::specified::calc::{self, CalcNode};
+use crate::values::specified::NonNegativeNumber;
+use crate::values::CSSFloat;
+use crate::{Zero, ZeroNoPercent};
+use app_units::AU_PER_PX;
+use cssparser::{Parser, Token};
+use std::cmp;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+pub use super::image::Image;
+pub use super::image::{EndingShape as GradientEndingShape, Gradient};
+pub use crate::values::specified::calc::CalcLengthPercentage;
+
+/// Number of pixels per inch
+pub const PX_PER_IN: CSSFloat = 96.;
+/// Number of pixels per centimeter
+pub const PX_PER_CM: CSSFloat = PX_PER_IN / 2.54;
+/// Number of pixels per millimeter
+pub const PX_PER_MM: CSSFloat = PX_PER_IN / 25.4;
+/// Number of pixels per quarter
+pub const PX_PER_Q: CSSFloat = PX_PER_MM / 4.;
+/// Number of pixels per point
+pub const PX_PER_PT: CSSFloat = PX_PER_IN / 72.;
+/// Number of pixels per pica
+pub const PX_PER_PC: CSSFloat = PX_PER_PT * 12.;
+
+/// A font relative length. Note that if any new value is
+/// added here, `custom_properties::NonCustomReferences::from_unit`
+/// must also be updated. Consult the comment in that function as to why.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub enum FontRelativeLength {
+ /// A "em" value: https://drafts.csswg.org/css-values/#em
+ #[css(dimension)]
+ Em(CSSFloat),
+ /// A "ex" value: https://drafts.csswg.org/css-values/#ex
+ #[css(dimension)]
+ Ex(CSSFloat),
+ /// A "ch" value: https://drafts.csswg.org/css-values/#ch
+ #[css(dimension)]
+ Ch(CSSFloat),
+ /// A "cap" value: https://drafts.csswg.org/css-values/#cap
+ #[css(dimension)]
+ Cap(CSSFloat),
+ /// An "ic" value: https://drafts.csswg.org/css-values/#ic
+ #[css(dimension)]
+ Ic(CSSFloat),
+ /// A "rem" value: https://drafts.csswg.org/css-values/#rem
+ #[css(dimension)]
+ Rem(CSSFloat),
+ /// A "lh" value: https://drafts.csswg.org/css-values/#lh
+ #[css(dimension)]
+ Lh(CSSFloat),
+ /// A "rlh" value: https://drafts.csswg.org/css-values/#lh
+ #[css(dimension)]
+ Rlh(CSSFloat),
+}
+
+/// A source to resolve font-relative units against
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum FontBaseSize {
+ /// Use the font-size of the current element.
+ CurrentStyle,
+ /// Use the inherited font-size.
+ InheritedStyle,
+}
+
+/// A source to resolve font-relative line-height units against.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum LineHeightBase {
+ /// Use the line-height of the current element.
+ CurrentStyle,
+ /// Use the inherited line-height.
+ InheritedStyle,
+}
+
+impl FontBaseSize {
+ /// Calculate the actual size for a given context
+ pub fn resolve(&self, context: &Context) -> computed::FontSize {
+ match *self {
+ Self::CurrentStyle => context.style().get_font().clone_font_size(),
+ Self::InheritedStyle => context.style().get_parent_font().clone_font_size(),
+ }
+ }
+}
+
+impl FontRelativeLength {
+ /// Unit identifier for `em`.
+ pub const EM: &'static str = "em";
+ /// Unit identifier for `ex`.
+ pub const EX: &'static str = "ex";
+ /// Unit identifier for `ch`.
+ pub const CH: &'static str = "ch";
+ /// Unit identifier for `cap`.
+ pub const CAP: &'static str = "cap";
+ /// Unit identifier for `ic`.
+ pub const IC: &'static str = "ic";
+ /// Unit identifier for `rem`.
+ pub const REM: &'static str = "rem";
+ /// Unit identifier for `lh`.
+ pub const LH: &'static str = "lh";
+ /// Unit identifier for `rlh`.
+ pub const RLH: &'static str = "rlh";
+
+ /// Return the unitless, raw value.
+ fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ Self::Em(v) |
+ Self::Ex(v) |
+ Self::Ch(v) |
+ Self::Cap(v) |
+ Self::Ic(v) |
+ Self::Rem(v) |
+ Self::Lh(v) |
+ Self::Rlh(v) => v,
+ }
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Em(_) => Self::EM,
+ Self::Ex(_) => Self::EX,
+ Self::Ch(_) => Self::CH,
+ Self::Cap(_) => Self::CAP,
+ Self::Ic(_) => Self::IC,
+ Self::Rem(_) => Self::REM,
+ Self::Lh(_) => Self::LH,
+ Self::Rlh(_) => Self::RLH,
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::FontRelativeLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ Ok(match (self, other) {
+ (&Em(one), &Em(other)) => Em(op(one, other)),
+ (&Ex(one), &Ex(other)) => Ex(op(one, other)),
+ (&Ch(one), &Ch(other)) => Ch(op(one, other)),
+ (&Cap(one), &Cap(other)) => Cap(op(one, other)),
+ (&Ic(one), &Ic(other)) => Ic(op(one, other)),
+ (&Rem(one), &Rem(other)) => Rem(op(one, other)),
+ (&Lh(one), &Lh(other)) => Lh(op(one, other)),
+ (&Rlh(one), &Rlh(other)) => Rlh(op(one, other)),
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ match self {
+ Self::Em(x) => Self::Em(op(*x)),
+ Self::Ex(x) => Self::Ex(op(*x)),
+ Self::Ch(x) => Self::Ch(op(*x)),
+ Self::Cap(x) => Self::Cap(op(*x)),
+ Self::Ic(x) => Self::Ic(op(*x)),
+ Self::Rem(x) => Self::Rem(op(*x)),
+ Self::Lh(x) => Self::Lh(op(*x)),
+ Self::Rlh(x) => Self::Lh(op(*x)),
+ }
+ }
+
+ /// Computes the font-relative length.
+ pub fn to_computed_value(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> computed::Length {
+ let (reference_size, length) =
+ self.reference_font_size_and_length(context, base_size, line_height_base);
+ (reference_size * length).finite()
+ }
+
+ /// Computes the length, given a GeckoFontMetrics getter to resolve font-relative units.
+ pub fn to_computed_pixel_length_with_font_metrics(
+ &self,
+ get_font_metrics: impl Fn() -> GeckoFontMetrics,
+ ) -> Result<CSSFloat, ()> {
+ let metrics = get_font_metrics();
+ Ok(match *self {
+ Self::Em(v) => v * metrics.mComputedEmSize.px(),
+ Self::Ex(v) => v * metrics.mXSize.px(),
+ Self::Ch(v) => v * metrics.mChSize.px(),
+ Self::Cap(v) => v * metrics.mCapHeight.px(),
+ Self::Ic(v) => v * metrics.mIcWidth.px(),
+ // `lh`, `rlh` & `rem` are unsupported as we have no context for it.
+ Self::Rem(_) | Self::Lh(_) | Self::Rlh(_) => return Err(()),
+ })
+ }
+
+ /// Return reference font size.
+ ///
+ /// We use the base_size flag to pass a different size for computing
+ /// font-size and unconstrained font-size.
+ ///
+ /// This returns a pair, the first one is the reference font size, and the
+ /// second one is the unpacked relative length.
+ fn reference_font_size_and_length(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> (computed::Length, CSSFloat) {
+ fn query_font_metrics(
+ context: &Context,
+ base_size: FontBaseSize,
+ orientation: FontMetricsOrientation,
+ ) -> FontMetrics {
+ let retrieve_math_scales = false;
+ context.query_font_metrics(base_size, orientation, retrieve_math_scales)
+ }
+
+ let reference_font_size = base_size.resolve(context);
+ match *self {
+ Self::Em(length) => {
+ if context.for_non_inherited_property && base_size == FontBaseSize::CurrentStyle {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_font_size_dependency(reference_font_size.computed_size);
+ }
+
+ (reference_font_size.computed_size(), length)
+ },
+ Self::Ex(length) => {
+ // The x-height is an intrinsically horizontal metric.
+ let metrics =
+ query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal);
+ let reference_size = metrics.x_height.unwrap_or_else(|| {
+ // https://drafts.csswg.org/css-values/#ex
+ //
+ // In the cases where it is impossible or impractical to
+ // determine the x-height, a value of 0.5em must be
+ // assumed.
+ //
+ // (But note we use 0.5em of the used, not computed
+ // font-size)
+ reference_font_size.used_size() * 0.5
+ });
+ (reference_size, length)
+ },
+ Self::Ch(length) => {
+ // https://drafts.csswg.org/css-values/#ch:
+ //
+ // Equal to the used advance measure of the “0” (ZERO,
+ // U+0030) glyph in the font used to render it. (The advance
+ // measure of a glyph is its advance width or height,
+ // whichever is in the inline axis of the element.)
+ //
+ let metrics = query_font_metrics(
+ context,
+ base_size,
+ FontMetricsOrientation::MatchContextPreferHorizontal,
+ );
+ let reference_size = metrics.zero_advance_measure.unwrap_or_else(|| {
+ // https://drafts.csswg.org/css-values/#ch
+ //
+ // In the cases where it is impossible or impractical to
+ // determine the measure of the “0” glyph, it must be
+ // assumed to be 0.5em wide by 1em tall. Thus, the ch
+ // unit falls back to 0.5em in the general case, and to
+ // 1em when it would be typeset upright (i.e.
+ // writing-mode is vertical-rl or vertical-lr and
+ // text-orientation is upright).
+ //
+ // Same caveat about computed vs. used font-size applies
+ // above.
+ let wm = context.style().writing_mode;
+ if wm.is_vertical() && wm.is_upright() {
+ reference_font_size.used_size()
+ } else {
+ reference_font_size.used_size() * 0.5
+ }
+ });
+ (reference_size, length)
+ },
+ Self::Cap(length) => {
+ let metrics =
+ query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal);
+ let reference_size = metrics.cap_height.unwrap_or_else(|| {
+ // https://drafts.csswg.org/css-values/#cap
+ //
+ // In the cases where it is impossible or impractical to
+ // determine the cap-height, the font’s ascent must be
+ // used.
+ //
+ metrics.ascent
+ });
+ (reference_size, length)
+ },
+ Self::Ic(length) => {
+ let metrics = query_font_metrics(
+ context,
+ base_size,
+ FontMetricsOrientation::MatchContextPreferVertical,
+ );
+ let reference_size = metrics.ic_width.unwrap_or_else(|| {
+ // https://drafts.csswg.org/css-values/#ic
+ //
+ // In the cases where it is impossible or impractical to
+ // determine the ideographic advance measure, it must be
+ // assumed to be 1em.
+ //
+ // Same caveat about computed vs. used as for other
+ // metric-dependent units.
+ reference_font_size.used_size()
+ });
+ (reference_size, length)
+ },
+ Self::Rem(length) => {
+ // https://drafts.csswg.org/css-values/#rem:
+ //
+ // When specified on the font-size property of the root
+ // element, the rem units refer to the property's initial
+ // value.
+ //
+ let reference_size = if context.builder.is_root_element || context.in_media_query {
+ reference_font_size.computed_size()
+ } else {
+ context.device().root_font_size()
+ };
+ (reference_size, length)
+ },
+ Self::Lh(length) => {
+ // https://drafts.csswg.org/css-values-4/#lh
+ //
+ // When specified in media-query, the lh units refer to the
+ // initial values of font and line-height properties.
+ //
+ let reference_size = if context.in_media_query {
+ context
+ .device()
+ .calc_line_height(
+ &context.default_style().get_font(),
+ context.style().writing_mode,
+ None,
+ )
+ .0
+ } else {
+ let line_height = context.builder.calc_line_height(
+ context.device(),
+ line_height_base,
+ context.style().writing_mode,
+ );
+ if context.for_non_inherited_property &&
+ line_height_base == LineHeightBase::CurrentStyle
+ {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_line_height_dependency(line_height)
+ }
+ line_height.0
+ };
+ (reference_size, length)
+ },
+ Self::Rlh(length) => {
+ // https://drafts.csswg.org/css-values-4/#rlh
+ //
+ // When specified on the root element, the rlh units refer
+ // to the initial values of font and line-height properties.
+ //
+ let reference_size: CSSPixelLength =
+ if context.builder.is_root_element || context.in_media_query {
+ context
+ .device()
+ .calc_line_height(
+ &context.default_style().get_font(),
+ context.style().writing_mode,
+ None,
+ )
+ .0
+ } else {
+ context.device().root_line_height()
+ };
+ (reference_size, length)
+ },
+ }
+ }
+}
+
+/// https://drafts.csswg.org/css-values/#viewport-variants
+pub enum ViewportVariant {
+ /// https://drafts.csswg.org/css-values/#ua-default-viewport-size
+ UADefault,
+ /// https://drafts.csswg.org/css-values/#small-viewport-percentage-units
+ Small,
+ /// https://drafts.csswg.org/css-values/#large-viewport-percentage-units
+ Large,
+ /// https://drafts.csswg.org/css-values/#dynamic-viewport-percentage-units
+ Dynamic,
+}
+
+/// https://drafts.csswg.org/css-values/#viewport-relative-units
+#[derive(PartialEq)]
+enum ViewportUnit {
+ /// *vw units.
+ Vw,
+ /// *vh units.
+ Vh,
+ /// *vmin units.
+ Vmin,
+ /// *vmax units.
+ Vmax,
+ /// *vb units.
+ Vb,
+ /// *vi units.
+ Vi,
+}
+
+/// A viewport-relative length.
+///
+/// <https://drafts.csswg.org/css-values/#viewport-relative-lengths>
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub enum ViewportPercentageLength {
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vw>
+ #[css(dimension)]
+ Vw(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svw>
+ #[css(dimension)]
+ Svw(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvw>
+ #[css(dimension)]
+ Lvw(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvw>
+ #[css(dimension)]
+ Dvw(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vh>
+ #[css(dimension)]
+ Vh(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svh>
+ #[css(dimension)]
+ Svh(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvh>
+ #[css(dimension)]
+ Lvh(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvh>
+ #[css(dimension)]
+ Dvh(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vmin>
+ #[css(dimension)]
+ Vmin(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svmin>
+ #[css(dimension)]
+ Svmin(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvmin>
+ #[css(dimension)]
+ Lvmin(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvmin>
+ #[css(dimension)]
+ Dvmin(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vmax>
+ #[css(dimension)]
+ Vmax(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svmax>
+ #[css(dimension)]
+ Svmax(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvmax>
+ #[css(dimension)]
+ Lvmax(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvmax>
+ #[css(dimension)]
+ Dvmax(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vb>
+ #[css(dimension)]
+ Vb(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svb>
+ #[css(dimension)]
+ Svb(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvb>
+ #[css(dimension)]
+ Lvb(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvb>
+ #[css(dimension)]
+ Dvb(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vi>
+ #[css(dimension)]
+ Vi(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svi>
+ #[css(dimension)]
+ Svi(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvi>
+ #[css(dimension)]
+ Lvi(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvi>
+ #[css(dimension)]
+ Dvi(CSSFloat),
+}
+
+impl ViewportPercentageLength {
+ /// Return the unitless, raw value.
+ fn unitless_value(&self) -> CSSFloat {
+ self.unpack().2
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Vw(_) => "vw",
+ Self::Lvw(_) => "lvw",
+ Self::Svw(_) => "svw",
+ Self::Dvw(_) => "dvw",
+ Self::Vh(_) => "vh",
+ Self::Svh(_) => "svh",
+ Self::Lvh(_) => "lvh",
+ Self::Dvh(_) => "dvh",
+ Self::Vmin(_) => "vmin",
+ Self::Svmin(_) => "svmin",
+ Self::Lvmin(_) => "lvmin",
+ Self::Dvmin(_) => "dvmin",
+ Self::Vmax(_) => "vmax",
+ Self::Svmax(_) => "svmax",
+ Self::Lvmax(_) => "lvmax",
+ Self::Dvmax(_) => "dvmax",
+ Self::Vb(_) => "vb",
+ Self::Svb(_) => "svb",
+ Self::Lvb(_) => "lvb",
+ Self::Dvb(_) => "dvb",
+ Self::Vi(_) => "vi",
+ Self::Svi(_) => "svi",
+ Self::Lvi(_) => "lvi",
+ Self::Dvi(_) => "dvi",
+ }
+ }
+
+ fn unpack(&self) -> (ViewportVariant, ViewportUnit, CSSFloat) {
+ match *self {
+ Self::Vw(v) => (ViewportVariant::UADefault, ViewportUnit::Vw, v),
+ Self::Svw(v) => (ViewportVariant::Small, ViewportUnit::Vw, v),
+ Self::Lvw(v) => (ViewportVariant::Large, ViewportUnit::Vw, v),
+ Self::Dvw(v) => (ViewportVariant::Dynamic, ViewportUnit::Vw, v),
+ Self::Vh(v) => (ViewportVariant::UADefault, ViewportUnit::Vh, v),
+ Self::Svh(v) => (ViewportVariant::Small, ViewportUnit::Vh, v),
+ Self::Lvh(v) => (ViewportVariant::Large, ViewportUnit::Vh, v),
+ Self::Dvh(v) => (ViewportVariant::Dynamic, ViewportUnit::Vh, v),
+ Self::Vmin(v) => (ViewportVariant::UADefault, ViewportUnit::Vmin, v),
+ Self::Svmin(v) => (ViewportVariant::Small, ViewportUnit::Vmin, v),
+ Self::Lvmin(v) => (ViewportVariant::Large, ViewportUnit::Vmin, v),
+ Self::Dvmin(v) => (ViewportVariant::Dynamic, ViewportUnit::Vmin, v),
+ Self::Vmax(v) => (ViewportVariant::UADefault, ViewportUnit::Vmax, v),
+ Self::Svmax(v) => (ViewportVariant::Small, ViewportUnit::Vmax, v),
+ Self::Lvmax(v) => (ViewportVariant::Large, ViewportUnit::Vmax, v),
+ Self::Dvmax(v) => (ViewportVariant::Dynamic, ViewportUnit::Vmax, v),
+ Self::Vb(v) => (ViewportVariant::UADefault, ViewportUnit::Vb, v),
+ Self::Svb(v) => (ViewportVariant::Small, ViewportUnit::Vb, v),
+ Self::Lvb(v) => (ViewportVariant::Large, ViewportUnit::Vb, v),
+ Self::Dvb(v) => (ViewportVariant::Dynamic, ViewportUnit::Vb, v),
+ Self::Vi(v) => (ViewportVariant::UADefault, ViewportUnit::Vi, v),
+ Self::Svi(v) => (ViewportVariant::Small, ViewportUnit::Vi, v),
+ Self::Lvi(v) => (ViewportVariant::Large, ViewportUnit::Vi, v),
+ Self::Dvi(v) => (ViewportVariant::Dynamic, ViewportUnit::Vi, v),
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::ViewportPercentageLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ Ok(match (self, other) {
+ (&Vw(one), &Vw(other)) => Vw(op(one, other)),
+ (&Svw(one), &Svw(other)) => Svw(op(one, other)),
+ (&Lvw(one), &Lvw(other)) => Lvw(op(one, other)),
+ (&Dvw(one), &Dvw(other)) => Dvw(op(one, other)),
+ (&Vh(one), &Vh(other)) => Vh(op(one, other)),
+ (&Svh(one), &Svh(other)) => Svh(op(one, other)),
+ (&Lvh(one), &Lvh(other)) => Lvh(op(one, other)),
+ (&Dvh(one), &Dvh(other)) => Dvh(op(one, other)),
+ (&Vmin(one), &Vmin(other)) => Vmin(op(one, other)),
+ (&Svmin(one), &Svmin(other)) => Svmin(op(one, other)),
+ (&Lvmin(one), &Lvmin(other)) => Lvmin(op(one, other)),
+ (&Dvmin(one), &Dvmin(other)) => Dvmin(op(one, other)),
+ (&Vmax(one), &Vmax(other)) => Vmax(op(one, other)),
+ (&Svmax(one), &Svmax(other)) => Svmax(op(one, other)),
+ (&Lvmax(one), &Lvmax(other)) => Lvmax(op(one, other)),
+ (&Dvmax(one), &Dvmax(other)) => Dvmax(op(one, other)),
+ (&Vb(one), &Vb(other)) => Vb(op(one, other)),
+ (&Svb(one), &Svb(other)) => Svb(op(one, other)),
+ (&Lvb(one), &Lvb(other)) => Lvb(op(one, other)),
+ (&Dvb(one), &Dvb(other)) => Dvb(op(one, other)),
+ (&Vi(one), &Vi(other)) => Vi(op(one, other)),
+ (&Svi(one), &Svi(other)) => Svi(op(one, other)),
+ (&Lvi(one), &Lvi(other)) => Lvi(op(one, other)),
+ (&Dvi(one), &Dvi(other)) => Dvi(op(one, other)),
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Vw(..) | Svw(..) | Lvw(..) | Dvw(..) | Vh(..) | Svh(..) | Lvh(..) |
+ Dvh(..) | Vmin(..) | Svmin(..) | Lvmin(..) | Dvmin(..) | Vmax(..) |
+ Svmax(..) | Lvmax(..) | Dvmax(..) | Vb(..) | Svb(..) | Lvb(..) | Dvb(..) |
+ Vi(..) | Svi(..) | Lvi(..) | Dvi(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ match self {
+ Self::Vw(x) => Self::Vw(op(*x)),
+ Self::Svw(x) => Self::Svw(op(*x)),
+ Self::Lvw(x) => Self::Lvw(op(*x)),
+ Self::Dvw(x) => Self::Dvw(op(*x)),
+ Self::Vh(x) => Self::Vh(op(*x)),
+ Self::Svh(x) => Self::Svh(op(*x)),
+ Self::Lvh(x) => Self::Lvh(op(*x)),
+ Self::Dvh(x) => Self::Dvh(op(*x)),
+ Self::Vmin(x) => Self::Vmin(op(*x)),
+ Self::Svmin(x) => Self::Svmin(op(*x)),
+ Self::Lvmin(x) => Self::Lvmin(op(*x)),
+ Self::Dvmin(x) => Self::Dvmin(op(*x)),
+ Self::Vmax(x) => Self::Vmax(op(*x)),
+ Self::Svmax(x) => Self::Svmax(op(*x)),
+ Self::Lvmax(x) => Self::Lvmax(op(*x)),
+ Self::Dvmax(x) => Self::Dvmax(op(*x)),
+ Self::Vb(x) => Self::Vb(op(*x)),
+ Self::Svb(x) => Self::Svb(op(*x)),
+ Self::Lvb(x) => Self::Lvb(op(*x)),
+ Self::Dvb(x) => Self::Dvb(op(*x)),
+ Self::Vi(x) => Self::Vi(op(*x)),
+ Self::Svi(x) => Self::Svi(op(*x)),
+ Self::Lvi(x) => Self::Lvi(op(*x)),
+ Self::Dvi(x) => Self::Dvi(op(*x)),
+ }
+ }
+
+ /// Computes the given viewport-relative length for the given viewport size.
+ pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength {
+ let (variant, unit, factor) = self.unpack();
+ let size = context.viewport_size_for_viewport_unit_resolution(variant);
+ let length = match unit {
+ ViewportUnit::Vw => size.width,
+ ViewportUnit::Vh => size.height,
+ ViewportUnit::Vmin => cmp::min(size.width, size.height),
+ ViewportUnit::Vmax => cmp::max(size.width, size.height),
+ ViewportUnit::Vi | ViewportUnit::Vb => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if (unit == ViewportUnit::Vb) == context.style().writing_mode.is_vertical() {
+ size.width
+ } else {
+ size.height
+ }
+ },
+ };
+
+ // FIXME: Bug 1396535, we need to fix the extremely small viewport length for transform.
+ // See bug 989802. We truncate so that adding multiple viewport units
+ // that add up to 100 does not overflow due to rounding differences.
+ // We convert appUnits to CSS px manually here to avoid premature clamping by
+ // going through the Au type.
+ let trunc_scaled =
+ ((length.0 as f64 * factor as f64 / 100.).trunc() / AU_PER_PX as f64) as f32;
+ CSSPixelLength::new(crate::values::normalize(trunc_scaled))
+ }
+}
+
+/// HTML5 "character width", as defined in HTML5 § 14.5.4.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub struct CharacterWidth(pub i32);
+
+impl CharacterWidth {
+ /// Computes the given character width.
+ pub fn to_computed_value(&self, reference_font_size: computed::Length) -> computed::Length {
+ // This applies the *converting a character width to pixels* algorithm
+ // as specified in HTML5 § 14.5.4.
+ //
+ // TODO(pcwalton): Find these from the font.
+ let average_advance = reference_font_size * 0.5;
+ let max_advance = reference_font_size;
+ (average_advance * (self.0 as CSSFloat - 1.0) + max_advance).finite()
+ }
+}
+
+/// Represents an absolute length with its unit
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub enum AbsoluteLength {
+ /// An absolute length in pixels (px)
+ #[css(dimension)]
+ Px(CSSFloat),
+ /// An absolute length in inches (in)
+ #[css(dimension)]
+ In(CSSFloat),
+ /// An absolute length in centimeters (cm)
+ #[css(dimension)]
+ Cm(CSSFloat),
+ /// An absolute length in millimeters (mm)
+ #[css(dimension)]
+ Mm(CSSFloat),
+ /// An absolute length in quarter-millimeters (q)
+ #[css(dimension)]
+ Q(CSSFloat),
+ /// An absolute length in points (pt)
+ #[css(dimension)]
+ Pt(CSSFloat),
+ /// An absolute length in pica (pc)
+ #[css(dimension)]
+ Pc(CSSFloat),
+}
+
+impl AbsoluteLength {
+ /// Return the unitless, raw value.
+ fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ Self::Px(v) |
+ Self::In(v) |
+ Self::Cm(v) |
+ Self::Mm(v) |
+ Self::Q(v) |
+ Self::Pt(v) |
+ Self::Pc(v) => v,
+ }
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Px(_) => "px",
+ Self::In(_) => "in",
+ Self::Cm(_) => "cm",
+ Self::Mm(_) => "mm",
+ Self::Q(_) => "q",
+ Self::Pt(_) => "pt",
+ Self::Pc(_) => "pc",
+ }
+ }
+
+ /// Convert this into a pixel value.
+ #[inline]
+ pub fn to_px(&self) -> CSSFloat {
+ match *self {
+ Self::Px(value) => value,
+ Self::In(value) => value * PX_PER_IN,
+ Self::Cm(value) => value * PX_PER_CM,
+ Self::Mm(value) => value * PX_PER_MM,
+ Self::Q(value) => value * PX_PER_Q,
+ Self::Pt(value) => value * PX_PER_PT,
+ Self::Pc(value) => value * PX_PER_PC,
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ Ok(Self::Px(op(self.to_px(), other.to_px())))
+ }
+
+ fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ Self::Px(op(self.to_px()))
+ }
+}
+
+impl ToComputedValue for AbsoluteLength {
+ type ComputedValue = CSSPixelLength;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ CSSPixelLength::new(context.builder.effective_zoom().zoom(self.to_px())).finite()
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::Px(computed.px())
+ }
+}
+
+impl PartialOrd for AbsoluteLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ self.to_px().partial_cmp(&other.to_px())
+ }
+}
+
+/// A container query length.
+///
+/// <https://drafts.csswg.org/css-contain-3/#container-lengths>
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub enum ContainerRelativeLength {
+ /// 1% of query container's width
+ #[css(dimension)]
+ Cqw(CSSFloat),
+ /// 1% of query container's height
+ #[css(dimension)]
+ Cqh(CSSFloat),
+ /// 1% of query container's inline size
+ #[css(dimension)]
+ Cqi(CSSFloat),
+ /// 1% of query container's block size
+ #[css(dimension)]
+ Cqb(CSSFloat),
+ /// The smaller value of `cqi` or `cqb`
+ #[css(dimension)]
+ Cqmin(CSSFloat),
+ /// The larger value of `cqi` or `cqb`
+ #[css(dimension)]
+ Cqmax(CSSFloat),
+}
+
+impl ContainerRelativeLength {
+ fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ Self::Cqw(v) |
+ Self::Cqh(v) |
+ Self::Cqi(v) |
+ Self::Cqb(v) |
+ Self::Cqmin(v) |
+ Self::Cqmax(v) => v,
+ }
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Cqw(_) => "cqw",
+ Self::Cqh(_) => "cqh",
+ Self::Cqi(_) => "cqi",
+ Self::Cqb(_) => "cqb",
+ Self::Cqmin(_) => "cqmin",
+ Self::Cqmax(_) => "cqmax",
+ }
+ }
+
+ pub(crate) fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::ContainerRelativeLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ Ok(match (self, other) {
+ (&Cqw(one), &Cqw(other)) => Cqw(op(one, other)),
+ (&Cqh(one), &Cqh(other)) => Cqh(op(one, other)),
+ (&Cqi(one), &Cqi(other)) => Cqi(op(one, other)),
+ (&Cqb(one), &Cqb(other)) => Cqb(op(one, other)),
+ (&Cqmin(one), &Cqmin(other)) => Cqmin(op(one, other)),
+ (&Cqmax(one), &Cqmax(other)) => Cqmax(op(one, other)),
+
+ // See https://github.com/rust-lang/rust/issues/68867, then
+ // https://github.com/rust-lang/rust/pull/95161. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Cqw(..) | Cqh(..) | Cqi(..) | Cqb(..) | Cqmin(..) | Cqmax(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ pub(crate) fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ match self {
+ Self::Cqw(x) => Self::Cqw(op(*x)),
+ Self::Cqh(x) => Self::Cqh(op(*x)),
+ Self::Cqi(x) => Self::Cqi(op(*x)),
+ Self::Cqb(x) => Self::Cqb(op(*x)),
+ Self::Cqmin(x) => Self::Cqmin(op(*x)),
+ Self::Cqmax(x) => Self::Cqmax(op(*x)),
+ }
+ }
+
+ /// Computes the given container-relative length.
+ pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength {
+ if context.for_non_inherited_property {
+ context.rule_cache_conditions.borrow_mut().set_uncacheable();
+ }
+ context
+ .builder
+ .add_flags(ComputedValueFlags::USES_CONTAINER_UNITS);
+
+ let size = context.get_container_size_query();
+ let (factor, container_length) = match *self {
+ Self::Cqw(v) => (v, size.get_container_width(context)),
+ Self::Cqh(v) => (v, size.get_container_height(context)),
+ Self::Cqi(v) => (v, size.get_container_inline_size(context)),
+ Self::Cqb(v) => (v, size.get_container_block_size(context)),
+ Self::Cqmin(v) => (
+ v,
+ cmp::min(
+ size.get_container_inline_size(context),
+ size.get_container_block_size(context),
+ ),
+ ),
+ Self::Cqmax(v) => (
+ v,
+ cmp::max(
+ size.get_container_inline_size(context),
+ size.get_container_block_size(context),
+ ),
+ ),
+ };
+ CSSPixelLength::new((container_length.to_f64_px() * factor as f64 / 100.0) as f32).finite()
+ }
+}
+
+#[cfg(feature = "gecko")]
+fn are_container_queries_enabled() -> bool {
+ static_prefs::pref!("layout.css.container-queries.enabled")
+}
+#[cfg(feature = "servo")]
+fn are_container_queries_enabled() -> bool {
+ false
+}
+
+/// A `<length>` without taking `calc` expressions into account
+///
+/// <https://drafts.csswg.org/css-values/#lengths>
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum NoCalcLength {
+ /// An absolute length
+ ///
+ /// <https://drafts.csswg.org/css-values/#absolute-length>
+ Absolute(AbsoluteLength),
+
+ /// A font-relative length:
+ ///
+ /// <https://drafts.csswg.org/css-values/#font-relative-lengths>
+ FontRelative(FontRelativeLength),
+
+ /// A viewport-relative length.
+ ///
+ /// <https://drafts.csswg.org/css-values/#viewport-relative-lengths>
+ ViewportPercentage(ViewportPercentageLength),
+
+ /// A container query length.
+ ///
+ /// <https://drafts.csswg.org/css-contain-3/#container-lengths>
+ ContainerRelative(ContainerRelativeLength),
+ /// HTML5 "character width", as defined in HTML5 § 14.5.4.
+ ///
+ /// This cannot be specified by the user directly and is only generated by
+ /// `Stylist::synthesize_rules_for_legacy_attributes()`.
+ ServoCharacterWidth(CharacterWidth),
+}
+
+impl NoCalcLength {
+ /// Return the unitless, raw value.
+ pub fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ Self::Absolute(v) => v.unitless_value(),
+ Self::FontRelative(v) => v.unitless_value(),
+ Self::ViewportPercentage(v) => v.unitless_value(),
+ Self::ContainerRelative(v) => v.unitless_value(),
+ Self::ServoCharacterWidth(c) => c.0 as f32,
+ }
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Absolute(v) => v.unit(),
+ Self::FontRelative(v) => v.unit(),
+ Self::ViewportPercentage(v) => v.unit(),
+ Self::ContainerRelative(v) => v.unit(),
+ Self::ServoCharacterWidth(_) => "",
+ }
+ }
+
+ /// Returns whether the value of this length without unit is less than zero.
+ pub fn is_negative(&self) -> bool {
+ self.unitless_value().is_sign_negative()
+ }
+
+ /// Returns whether the value of this length without unit is equal to zero.
+ pub fn is_zero(&self) -> bool {
+ self.unitless_value() == 0.0
+ }
+
+ /// Returns whether the value of this length without unit is infinite.
+ pub fn is_infinite(&self) -> bool {
+ self.unitless_value().is_infinite()
+ }
+
+ /// Returns whether the value of this length without unit is NaN.
+ pub fn is_nan(&self) -> bool {
+ self.unitless_value().is_nan()
+ }
+
+ /// Whether text-only zoom should be applied to this length.
+ ///
+ /// Generally, font-dependent/relative units don't get text-only-zoomed,
+ /// because the font they're relative to should be zoomed already.
+ pub fn should_zoom_text(&self) -> bool {
+ match *self {
+ Self::Absolute(..) | Self::ViewportPercentage(..) | Self::ContainerRelative(..) => true,
+ Self::ServoCharacterWidth(..) | Self::FontRelative(..) => false,
+ }
+ }
+
+ /// Parse a given absolute or relative dimension.
+ pub fn parse_dimension(
+ context: &ParserContext,
+ value: CSSFloat,
+ unit: &str,
+ ) -> Result<Self, ()> {
+ Ok(match_ignore_ascii_case! { unit,
+ "px" => Self::Absolute(AbsoluteLength::Px(value)),
+ "in" => Self::Absolute(AbsoluteLength::In(value)),
+ "cm" => Self::Absolute(AbsoluteLength::Cm(value)),
+ "mm" => Self::Absolute(AbsoluteLength::Mm(value)),
+ "q" => Self::Absolute(AbsoluteLength::Q(value)),
+ "pt" => Self::Absolute(AbsoluteLength::Pt(value)),
+ "pc" => Self::Absolute(AbsoluteLength::Pc(value)),
+ // font-relative
+ "em" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Em(value)),
+ "ex" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ex(value)),
+ "ch" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ch(value)),
+ "cap" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Cap(value)),
+ "ic" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ic(value)),
+ "rem" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Rem(value)),
+ "lh" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Lh(value)),
+ "rlh" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Rlh(value)),
+ // viewport percentages
+ "vw" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vw(value))
+ },
+ "svw" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svw(value))
+ },
+ "lvw" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvw(value))
+ },
+ "dvw" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvw(value))
+ },
+ "vh" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vh(value))
+ },
+ "svh" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svh(value))
+ },
+ "lvh" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvh(value))
+ },
+ "dvh" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvh(value))
+ },
+ "vmin" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vmin(value))
+ },
+ "svmin" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svmin(value))
+ },
+ "lvmin" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvmin(value))
+ },
+ "dvmin" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvmin(value))
+ },
+ "vmax" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vmax(value))
+ },
+ "svmax" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svmax(value))
+ },
+ "lvmax" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvmax(value))
+ },
+ "dvmax" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvmax(value))
+ },
+ "vb" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vb(value))
+ },
+ "svb" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svb(value))
+ },
+ "lvb" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvb(value))
+ },
+ "dvb" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvb(value))
+ },
+ "vi" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vi(value))
+ },
+ "svi" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svi(value))
+ },
+ "lvi" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvi(value))
+ },
+ "dvi" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvi(value))
+ },
+ // Container query lengths. Inherit the limitation from viewport units since
+ // we may fall back to them.
+ "cqw" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqw(value))
+ },
+ "cqh" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqh(value))
+ },
+ "cqi" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqi(value))
+ },
+ "cqb" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqb(value))
+ },
+ "cqmin" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqmin(value))
+ },
+ "cqmax" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqmax(value))
+ },
+ _ => return Err(()),
+ })
+ }
+
+ pub(crate) fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::NoCalcLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ Ok(match (self, other) {
+ (&Absolute(ref one), &Absolute(ref other)) => Absolute(one.try_op(other, op)?),
+ (&FontRelative(ref one), &FontRelative(ref other)) => {
+ FontRelative(one.try_op(other, op)?)
+ },
+ (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => {
+ ViewportPercentage(one.try_op(other, op)?)
+ },
+ (&ContainerRelative(ref one), &ContainerRelative(ref other)) => {
+ ContainerRelative(one.try_op(other, op)?)
+ },
+ (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => {
+ ServoCharacterWidth(CharacterWidth(op(one.0 as f32, other.0 as f32) as i32))
+ },
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Absolute(..) |
+ FontRelative(..) |
+ ViewportPercentage(..) |
+ ContainerRelative(..) |
+ ServoCharacterWidth(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ pub(crate) fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ use self::NoCalcLength::*;
+
+ match self {
+ Absolute(ref one) => Absolute(one.map(op)),
+ FontRelative(ref one) => FontRelative(one.map(op)),
+ ViewportPercentage(ref one) => ViewportPercentage(one.map(op)),
+ ContainerRelative(ref one) => ContainerRelative(one.map(op)),
+ ServoCharacterWidth(ref one) => {
+ ServoCharacterWidth(CharacterWidth(op(one.0 as f32) as i32))
+ },
+ }
+ }
+
+ /// Get a px value without context (so only absolute units can be handled).
+ #[inline]
+ pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> {
+ match *self {
+ Self::Absolute(len) => Ok(CSSPixelLength::new(len.to_px()).finite().px()),
+ _ => Err(()),
+ }
+ }
+
+ /// Get a px value without a full style context; this can handle either
+ /// absolute or (if a font metrics getter is provided) font-relative units.
+ #[inline]
+ pub fn to_computed_pixel_length_with_font_metrics(
+ &self,
+ get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>,
+ ) -> Result<CSSFloat, ()> {
+ match *self {
+ Self::Absolute(len) => Ok(CSSPixelLength::new(len.to_px()).finite().px()),
+ Self::FontRelative(fr) => {
+ if let Some(getter) = get_font_metrics {
+ fr.to_computed_pixel_length_with_font_metrics(getter)
+ } else {
+ Err(())
+ }
+ },
+ _ => Err(()),
+ }
+ }
+
+ /// Get an absolute length from a px value.
+ #[inline]
+ pub fn from_px(px_value: CSSFloat) -> NoCalcLength {
+ NoCalcLength::Absolute(AbsoluteLength::Px(px_value))
+ }
+}
+
+impl ToCss for NoCalcLength {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ crate::values::serialize_specified_dimension(
+ self.unitless_value(),
+ self.unit(),
+ false,
+ dest,
+ )
+ }
+}
+
+impl SpecifiedValueInfo for NoCalcLength {}
+
+impl PartialOrd for NoCalcLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::NoCalcLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Absolute(ref one), &Absolute(ref other)) => one.to_px().partial_cmp(&other.to_px()),
+ (&FontRelative(ref one), &FontRelative(ref other)) => one.partial_cmp(other),
+ (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => {
+ one.partial_cmp(other)
+ },
+ (&ContainerRelative(ref one), &ContainerRelative(ref other)) => one.partial_cmp(other),
+ (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => {
+ one.0.partial_cmp(&other.0)
+ },
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Absolute(..) |
+ FontRelative(..) |
+ ViewportPercentage(..) |
+ ContainerRelative(..) |
+ ServoCharacterWidth(..) => {},
+ }
+ debug_unreachable!("Forgot an arm in partial_cmp?")
+ },
+ }
+ }
+}
+
+impl Zero for NoCalcLength {
+ fn zero() -> Self {
+ NoCalcLength::Absolute(AbsoluteLength::Px(0.))
+ }
+
+ fn is_zero(&self) -> bool {
+ NoCalcLength::is_zero(self)
+ }
+}
+
+/// An extension to `NoCalcLength` to parse `calc` expressions.
+/// This is commonly used for the `<length>` values.
+///
+/// <https://drafts.csswg.org/css-values/#lengths>
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum Length {
+ /// The internal length type that cannot parse `calc`
+ NoCalc(NoCalcLength),
+ /// A calc expression.
+ ///
+ /// <https://drafts.csswg.org/css-values/#calc-notation>
+ Calc(Box<CalcLengthPercentage>),
+}
+
+impl From<NoCalcLength> for Length {
+ #[inline]
+ fn from(len: NoCalcLength) -> Self {
+ Length::NoCalc(len)
+ }
+}
+
+impl PartialOrd for FontRelativeLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::FontRelativeLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Em(ref one), &Em(ref other)) => one.partial_cmp(other),
+ (&Ex(ref one), &Ex(ref other)) => one.partial_cmp(other),
+ (&Ch(ref one), &Ch(ref other)) => one.partial_cmp(other),
+ (&Cap(ref one), &Cap(ref other)) => one.partial_cmp(other),
+ (&Ic(ref one), &Ic(ref other)) => one.partial_cmp(other),
+ (&Rem(ref one), &Rem(ref other)) => one.partial_cmp(other),
+ (&Lh(ref one), &Lh(ref other)) => one.partial_cmp(other),
+ (&Rlh(ref one), &Rlh(ref other)) => one.partial_cmp(other),
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {},
+ }
+ debug_unreachable!("Forgot an arm in partial_cmp?")
+ },
+ }
+ }
+}
+
+impl PartialOrd for ContainerRelativeLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::ContainerRelativeLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Cqw(ref one), &Cqw(ref other)) => one.partial_cmp(other),
+ (&Cqh(ref one), &Cqh(ref other)) => one.partial_cmp(other),
+ (&Cqi(ref one), &Cqi(ref other)) => one.partial_cmp(other),
+ (&Cqb(ref one), &Cqb(ref other)) => one.partial_cmp(other),
+ (&Cqmin(ref one), &Cqmin(ref other)) => one.partial_cmp(other),
+ (&Cqmax(ref one), &Cqmax(ref other)) => one.partial_cmp(other),
+
+ // See https://github.com/rust-lang/rust/issues/68867, then
+ // https://github.com/rust-lang/rust/pull/95161. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Cqw(..) | Cqh(..) | Cqi(..) | Cqb(..) | Cqmin(..) | Cqmax(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in partial_cmp()")
+ },
+ }
+ }
+}
+
+impl PartialOrd for ViewportPercentageLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::ViewportPercentageLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Vw(ref one), &Vw(ref other)) => one.partial_cmp(other),
+ (&Svw(ref one), &Svw(ref other)) => one.partial_cmp(other),
+ (&Lvw(ref one), &Lvw(ref other)) => one.partial_cmp(other),
+ (&Dvw(ref one), &Dvw(ref other)) => one.partial_cmp(other),
+ (&Vh(ref one), &Vh(ref other)) => one.partial_cmp(other),
+ (&Svh(ref one), &Svh(ref other)) => one.partial_cmp(other),
+ (&Lvh(ref one), &Lvh(ref other)) => one.partial_cmp(other),
+ (&Dvh(ref one), &Dvh(ref other)) => one.partial_cmp(other),
+ (&Vmin(ref one), &Vmin(ref other)) => one.partial_cmp(other),
+ (&Svmin(ref one), &Svmin(ref other)) => one.partial_cmp(other),
+ (&Lvmin(ref one), &Lvmin(ref other)) => one.partial_cmp(other),
+ (&Dvmin(ref one), &Dvmin(ref other)) => one.partial_cmp(other),
+ (&Vmax(ref one), &Vmax(ref other)) => one.partial_cmp(other),
+ (&Svmax(ref one), &Svmax(ref other)) => one.partial_cmp(other),
+ (&Lvmax(ref one), &Lvmax(ref other)) => one.partial_cmp(other),
+ (&Dvmax(ref one), &Dvmax(ref other)) => one.partial_cmp(other),
+ (&Vb(ref one), &Vb(ref other)) => one.partial_cmp(other),
+ (&Svb(ref one), &Svb(ref other)) => one.partial_cmp(other),
+ (&Lvb(ref one), &Lvb(ref other)) => one.partial_cmp(other),
+ (&Dvb(ref one), &Dvb(ref other)) => one.partial_cmp(other),
+ (&Vi(ref one), &Vi(ref other)) => one.partial_cmp(other),
+ (&Svi(ref one), &Svi(ref other)) => one.partial_cmp(other),
+ (&Lvi(ref one), &Lvi(ref other)) => one.partial_cmp(other),
+ (&Dvi(ref one), &Dvi(ref other)) => one.partial_cmp(other),
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Vw(..) | Svw(..) | Lvw(..) | Dvw(..) | Vh(..) | Svh(..) | Lvh(..) |
+ Dvh(..) | Vmin(..) | Svmin(..) | Lvmin(..) | Dvmin(..) | Vmax(..) |
+ Svmax(..) | Lvmax(..) | Dvmax(..) | Vb(..) | Svb(..) | Lvb(..) | Dvb(..) |
+ Vi(..) | Svi(..) | Lvi(..) | Dvi(..) => {},
+ }
+ debug_unreachable!("Forgot an arm in partial_cmp?")
+ },
+ }
+ }
+}
+
+impl Length {
+ #[inline]
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ num_context: AllowedNumericType,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let token = input.next()?;
+ match *token {
+ Token::Dimension {
+ value, ref unit, ..
+ } if num_context.is_ok(context.parsing_mode, value) => {
+ NoCalcLength::parse_dimension(context, value, unit)
+ .map(Length::NoCalc)
+ .map_err(|()| location.new_unexpected_token_error(token.clone()))
+ },
+ Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => {
+ if value != 0. &&
+ !context.parsing_mode.allows_unitless_lengths() &&
+ !allow_quirks.allowed(context.quirks_mode)
+ {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(Length::NoCalc(NoCalcLength::Absolute(AbsoluteLength::Px(
+ value,
+ ))))
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let calc = CalcNode::parse_length(context, input, num_context, function)?;
+ Ok(Length::Calc(Box::new(calc)))
+ },
+ ref token => return Err(location.new_unexpected_token_error(token.clone())),
+ }
+ }
+
+ /// Parse a non-negative length
+ #[inline]
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_non_negative_quirky(context, input, AllowQuirks::No)
+ }
+
+ /// Parse a non-negative length, allowing quirks.
+ #[inline]
+ pub fn parse_non_negative_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(
+ context,
+ input,
+ AllowedNumericType::NonNegative,
+ allow_quirks,
+ )
+ }
+
+ /// Get an absolute length from a px value.
+ #[inline]
+ pub fn from_px(px_value: CSSFloat) -> Length {
+ Length::NoCalc(NoCalcLength::from_px(px_value))
+ }
+
+ /// Get a px value without context.
+ pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> {
+ match *self {
+ Self::NoCalc(ref l) => l.to_computed_pixel_length_without_context(),
+ Self::Calc(ref l) => l.to_computed_pixel_length_without_context(),
+ }
+ }
+
+ /// Get a px value, with an optional GeckoFontMetrics getter to resolve font-relative units.
+ pub fn to_computed_pixel_length_with_font_metrics(
+ &self,
+ get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>,
+ ) -> Result<CSSFloat, ()> {
+ match *self {
+ Self::NoCalc(ref l) => l.to_computed_pixel_length_with_font_metrics(get_font_metrics),
+ Self::Calc(ref l) => l.to_computed_pixel_length_with_font_metrics(get_font_metrics),
+ }
+ }
+}
+
+impl Parse for Length {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl Zero for Length {
+ fn zero() -> Self {
+ Length::NoCalc(NoCalcLength::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ // FIXME(emilio): Seems a bit weird to treat calc() unconditionally as
+ // non-zero here?
+ match *self {
+ Length::NoCalc(ref l) => l.is_zero(),
+ Length::Calc(..) => false,
+ }
+ }
+}
+
+impl Length {
+ /// Parses a length, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks)
+ }
+}
+
+/// A wrapper of Length, whose value must be >= 0.
+pub type NonNegativeLength = NonNegative<Length>;
+
+impl Parse for NonNegativeLength {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(Length::parse_non_negative(context, input)?))
+ }
+}
+
+impl From<NoCalcLength> for NonNegativeLength {
+ #[inline]
+ fn from(len: NoCalcLength) -> Self {
+ NonNegative(Length::NoCalc(len))
+ }
+}
+
+impl From<Length> for NonNegativeLength {
+ #[inline]
+ fn from(len: Length) -> Self {
+ NonNegative(len)
+ }
+}
+
+impl NonNegativeLength {
+ /// Get an absolute length from a px value.
+ #[inline]
+ pub fn from_px(px_value: CSSFloat) -> Self {
+ Length::from_px(px_value.max(0.)).into()
+ }
+
+ /// Parses a non-negative length, optionally with quirks.
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(Length::parse_non_negative_quirky(
+ context,
+ input,
+ allow_quirks,
+ )?))
+ }
+}
+
+/// A `<length-percentage>` value. This can be either a `<length>`, a
+/// `<percentage>`, or a combination of both via `calc()`.
+///
+/// https://drafts.csswg.org/css-values-4/#typedef-length-percentage
+#[allow(missing_docs)]
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum LengthPercentage {
+ Length(NoCalcLength),
+ Percentage(computed::Percentage),
+ Calc(Box<CalcLengthPercentage>),
+}
+
+impl From<Length> for LengthPercentage {
+ fn from(len: Length) -> LengthPercentage {
+ match len {
+ Length::NoCalc(l) => LengthPercentage::Length(l),
+ Length::Calc(l) => LengthPercentage::Calc(l),
+ }
+ }
+}
+
+impl From<NoCalcLength> for LengthPercentage {
+ #[inline]
+ fn from(len: NoCalcLength) -> Self {
+ LengthPercentage::Length(len)
+ }
+}
+
+impl From<Percentage> for LengthPercentage {
+ #[inline]
+ fn from(pc: Percentage) -> Self {
+ if let Some(clamping_mode) = pc.calc_clamping_mode() {
+ LengthPercentage::Calc(Box::new(CalcLengthPercentage {
+ clamping_mode,
+ node: CalcNode::Leaf(calc::Leaf::Percentage(pc.get())),
+ }))
+ } else {
+ LengthPercentage::Percentage(computed::Percentage(pc.get()))
+ }
+ }
+}
+
+impl From<computed::Percentage> for LengthPercentage {
+ #[inline]
+ fn from(pc: computed::Percentage) -> Self {
+ LengthPercentage::Percentage(pc)
+ }
+}
+
+impl Parse for LengthPercentage {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl LengthPercentage {
+ #[inline]
+ /// Returns a `0%` value.
+ pub fn zero_percent() -> LengthPercentage {
+ LengthPercentage::Percentage(computed::Percentage::zero())
+ }
+
+ #[inline]
+ /// Returns a `100%` value.
+ pub fn hundred_percent() -> LengthPercentage {
+ LengthPercentage::Percentage(computed::Percentage::hundred())
+ }
+
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ num_context: AllowedNumericType,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let token = input.next()?;
+ match *token {
+ Token::Dimension {
+ value, ref unit, ..
+ } if num_context.is_ok(context.parsing_mode, value) => {
+ return NoCalcLength::parse_dimension(context, value, unit)
+ .map(LengthPercentage::Length)
+ .map_err(|()| location.new_unexpected_token_error(token.clone()));
+ },
+ Token::Percentage { unit_value, .. }
+ if num_context.is_ok(context.parsing_mode, unit_value) =>
+ {
+ return Ok(LengthPercentage::Percentage(computed::Percentage(
+ unit_value,
+ )));
+ },
+ Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => {
+ if value != 0. &&
+ !context.parsing_mode.allows_unitless_lengths() &&
+ !allow_quirks.allowed(context.quirks_mode)
+ {
+ return Err(location.new_unexpected_token_error(token.clone()));
+ } else {
+ return Ok(LengthPercentage::Length(NoCalcLength::from_px(value)));
+ }
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let calc =
+ CalcNode::parse_length_or_percentage(context, input, num_context, function)?;
+ Ok(LengthPercentage::Calc(Box::new(calc)))
+ },
+ _ => return Err(location.new_unexpected_token_error(token.clone())),
+ }
+ }
+
+ /// Parses allowing the unitless length quirk.
+ /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk>
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks)
+ }
+
+ /// Parse a non-negative length.
+ ///
+ /// FIXME(emilio): This should be not public and we should use
+ /// NonNegativeLengthPercentage instead.
+ #[inline]
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_non_negative_quirky(context, input, AllowQuirks::No)
+ }
+
+ /// Parse a non-negative length, with quirks.
+ #[inline]
+ pub fn parse_non_negative_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(
+ context,
+ input,
+ AllowedNumericType::NonNegative,
+ allow_quirks,
+ )
+ }
+
+ /// Returns self as specified::calc::CalcNode.
+ /// Note that this expect the clamping_mode is AllowedNumericType::All for Calc. The caller
+ /// should take care about it when using this function.
+ fn to_calc_node(self) -> CalcNode {
+ match self {
+ LengthPercentage::Length(l) => CalcNode::Leaf(calc::Leaf::Length(l)),
+ LengthPercentage::Percentage(p) => CalcNode::Leaf(calc::Leaf::Percentage(p.0)),
+ LengthPercentage::Calc(p) => p.node,
+ }
+ }
+
+ /// Construct the value representing `calc(100% - self)`.
+ pub fn hundred_percent_minus(self, clamping_mode: AllowedNumericType) -> Self {
+ let mut sum = smallvec::SmallVec::<[CalcNode; 2]>::new();
+ sum.push(CalcNode::Leaf(calc::Leaf::Percentage(1.0)));
+
+ let mut node = self.to_calc_node();
+ node.negate();
+ sum.push(node);
+
+ let calc = CalcNode::Sum(sum.into_boxed_slice().into());
+ LengthPercentage::Calc(Box::new(
+ calc.into_length_or_percentage(clamping_mode).unwrap(),
+ ))
+ }
+}
+
+impl Zero for LengthPercentage {
+ fn zero() -> Self {
+ LengthPercentage::Length(NoCalcLength::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ match *self {
+ LengthPercentage::Length(l) => l.is_zero(),
+ LengthPercentage::Percentage(p) => p.0 == 0.0,
+ LengthPercentage::Calc(_) => false,
+ }
+ }
+}
+
+impl ZeroNoPercent for LengthPercentage {
+ fn is_zero_no_percent(&self) -> bool {
+ match *self {
+ LengthPercentage::Percentage(_) => false,
+ _ => self.is_zero(),
+ }
+ }
+}
+
+/// A specified type for `<length-percentage> | auto`.
+pub type LengthPercentageOrAuto = generics::LengthPercentageOrAuto<LengthPercentage>;
+
+impl LengthPercentageOrAuto {
+ /// Returns a value representing `0%`.
+ #[inline]
+ pub fn zero_percent() -> Self {
+ generics::LengthPercentageOrAuto::LengthPercentage(LengthPercentage::zero_percent())
+ }
+
+ /// Parses a length or a percentage, allowing the unitless length quirk.
+ /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk>
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, |context, input| {
+ LengthPercentage::parse_quirky(context, input, allow_quirks)
+ })
+ }
+}
+
+/// A wrapper of LengthPercentageOrAuto, whose value must be >= 0.
+pub type NonNegativeLengthPercentageOrAuto =
+ generics::LengthPercentageOrAuto<NonNegativeLengthPercentage>;
+
+impl NonNegativeLengthPercentageOrAuto {
+ /// Returns a value representing `0%`.
+ #[inline]
+ pub fn zero_percent() -> Self {
+ generics::LengthPercentageOrAuto::LengthPercentage(
+ NonNegativeLengthPercentage::zero_percent(),
+ )
+ }
+
+ /// Parses a non-negative length-percentage, allowing the unitless length
+ /// quirk.
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, |context, input| {
+ NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)
+ })
+ }
+}
+
+/// A wrapper of LengthPercentage, whose value must be >= 0.
+pub type NonNegativeLengthPercentage = NonNegative<LengthPercentage>;
+
+/// Either a NonNegativeLengthPercentage or the `normal` keyword.
+pub type NonNegativeLengthPercentageOrNormal =
+ GenericLengthPercentageOrNormal<NonNegativeLengthPercentage>;
+
+impl From<NoCalcLength> for NonNegativeLengthPercentage {
+ #[inline]
+ fn from(len: NoCalcLength) -> Self {
+ NonNegative(LengthPercentage::from(len))
+ }
+}
+
+impl Parse for NonNegativeLengthPercentage {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl NonNegativeLengthPercentage {
+ #[inline]
+ /// Returns a `0%` value.
+ pub fn zero_percent() -> Self {
+ NonNegative(LengthPercentage::zero_percent())
+ }
+
+ /// Parses a length or a percentage, allowing the unitless length quirk.
+ /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk>
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ LengthPercentage::parse_non_negative_quirky(context, input, allow_quirks).map(NonNegative)
+ }
+}
+
+/// Either a `<length>` or the `auto` keyword.
+///
+/// Note that we use LengthPercentage just for convenience, since it pretty much
+/// is everything we care about, but we could just add a similar LengthOrAuto
+/// instead if we think getting rid of this weirdness is worth it.
+pub type LengthOrAuto = generics::LengthPercentageOrAuto<Length>;
+
+impl LengthOrAuto {
+ /// Parses a length, allowing the unitless length quirk.
+ /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk>
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, |context, input| {
+ Length::parse_quirky(context, input, allow_quirks)
+ })
+ }
+}
+
+/// Either a non-negative `<length>` or the `auto` keyword.
+pub type NonNegativeLengthOrAuto = generics::LengthPercentageOrAuto<NonNegativeLength>;
+
+/// Either a `<length>` or a `<number>`.
+pub type LengthOrNumber = GenericLengthOrNumber<Length, Number>;
+
+/// A specified value for `min-width`, `min-height`, `width` or `height` property.
+pub type Size = GenericSize<NonNegativeLengthPercentage>;
+
+impl Parse for Size {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Size::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+macro_rules! parse_size_non_length {
+ ($size:ident, $input:expr, $auto_or_none:expr => $auto_or_none_ident:ident) => {{
+ let size = $input.try_parse(|input| {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ #[cfg(feature = "gecko")]
+ "min-content" | "-moz-min-content" => $size::MinContent,
+ #[cfg(feature = "gecko")]
+ "max-content" | "-moz-max-content" => $size::MaxContent,
+ #[cfg(feature = "gecko")]
+ "fit-content" | "-moz-fit-content" => $size::FitContent,
+ #[cfg(feature = "gecko")]
+ "-moz-available" => $size::MozAvailable,
+ $auto_or_none => $size::$auto_or_none_ident,
+ })
+ });
+ if size.is_ok() {
+ return size;
+ }
+ }};
+}
+
+#[cfg(feature = "gecko")]
+fn is_fit_content_function_enabled() -> bool {
+ static_prefs::pref!("layout.css.fit-content-function.enabled")
+}
+#[cfg(feature = "servo")]
+fn is_fit_content_function_enabled() -> bool {
+ false
+}
+
+macro_rules! parse_fit_content_function {
+ ($size:ident, $input:expr, $context:expr, $allow_quirks:expr) => {
+ if is_fit_content_function_enabled() {
+ if let Ok(length) = $input.try_parse(|input| {
+ input.expect_function_matching("fit-content")?;
+ input.parse_nested_block(|i| {
+ NonNegativeLengthPercentage::parse_quirky($context, i, $allow_quirks)
+ })
+ }) {
+ return Ok($size::FitContentFunction(length));
+ }
+ }
+ };
+}
+
+impl Size {
+ /// Parses, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_size_non_length!(Size, input, "auto" => Auto);
+ parse_fit_content_function!(Size, input, context, allow_quirks);
+
+ let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?;
+ Ok(GenericSize::LengthPercentage(length))
+ }
+
+ /// Returns `0%`.
+ #[inline]
+ pub fn zero_percent() -> Self {
+ GenericSize::LengthPercentage(NonNegativeLengthPercentage::zero_percent())
+ }
+}
+
+/// A specified value for `max-width` or `max-height` property.
+pub type MaxSize = GenericMaxSize<NonNegativeLengthPercentage>;
+
+impl Parse for MaxSize {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ MaxSize::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl MaxSize {
+ /// Parses, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_size_non_length!(MaxSize, input, "none" => None);
+ parse_fit_content_function!(MaxSize, input, context, allow_quirks);
+
+ let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?;
+ Ok(GenericMaxSize::LengthPercentage(length))
+ }
+}
+
+/// A specified non-negative `<length>` | `<number>`.
+pub type NonNegativeLengthOrNumber = GenericLengthOrNumber<NonNegativeLength, NonNegativeNumber>;
diff --git a/servo/components/style/values/specified/list.rs b/servo/components/style/values/specified/list.rs
new file mode 100644
index 0000000000..693471e478
--- /dev/null
+++ b/servo/components/style/values/specified/list.rs
@@ -0,0 +1,202 @@
+/* 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/. */
+
+//! `list` specified values.
+
+use crate::parser::{Parse, ParserContext};
+#[cfg(feature = "gecko")]
+use crate::values::generics::CounterStyle;
+#[cfg(feature = "gecko")]
+use crate::values::CustomIdent;
+use cssparser::{Parser, Token};
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// Specified and computed `list-style-type` property.
+#[cfg(feature = "gecko")]
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum ListStyleType {
+ /// `none`
+ None,
+ /// <counter-style>
+ CounterStyle(CounterStyle),
+ /// <string>
+ String(String),
+}
+
+#[cfg(feature = "gecko")]
+impl ListStyleType {
+ /// Initial specified value for `list-style-type`.
+ #[inline]
+ pub fn disc() -> Self {
+ ListStyleType::CounterStyle(CounterStyle::disc())
+ }
+
+ /// Convert from gecko keyword to list-style-type.
+ ///
+ /// This should only be used for mapping type attribute to
+ /// list-style-type, and thus only values possible in that
+ /// attribute is considered here.
+ pub fn from_gecko_keyword(value: u32) -> Self {
+ use crate::gecko_bindings::structs;
+ let v8 = value as u8;
+
+ if v8 == structs::ListStyle_None {
+ return ListStyleType::None;
+ }
+
+ ListStyleType::CounterStyle(CounterStyle::Name(CustomIdent(match v8 {
+ structs::ListStyle_Disc => atom!("disc"),
+ structs::ListStyle_Circle => atom!("circle"),
+ structs::ListStyle_Square => atom!("square"),
+ structs::ListStyle_Decimal => atom!("decimal"),
+ structs::ListStyle_LowerRoman => atom!("lower-roman"),
+ structs::ListStyle_UpperRoman => atom!("upper-roman"),
+ structs::ListStyle_LowerAlpha => atom!("lower-alpha"),
+ structs::ListStyle_UpperAlpha => atom!("upper-alpha"),
+ _ => unreachable!("Unknown counter style keyword value"),
+ })))
+ }
+
+ /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`)
+ #[inline]
+ pub fn is_bullet(&self) -> bool {
+ match self {
+ ListStyleType::CounterStyle(ref style) => style.is_bullet(),
+ _ => false,
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl Parse for ListStyleType {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(style) = input.try_parse(|i| CounterStyle::parse(context, i)) {
+ return Ok(ListStyleType::CounterStyle(style));
+ }
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(ListStyleType::None);
+ }
+ Ok(ListStyleType::String(
+ input.expect_string()?.as_ref().to_owned(),
+ ))
+ }
+}
+
+/// A quote pair.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct QuotePair {
+ /// The opening quote.
+ pub opening: crate::OwnedStr,
+
+ /// The closing quote.
+ pub closing: crate::OwnedStr,
+}
+
+/// List of quote pairs for the specified/computed value of `quotes` property.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct QuoteList(
+ #[css(iterable, if_empty = "none")]
+ #[ignore_malloc_size_of = "Arc"]
+ pub crate::ArcSlice<QuotePair>,
+);
+
+/// Specified and computed `quotes` property: `auto`, `none`, or a list
+/// of characters.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub enum Quotes {
+ /// list of quote pairs
+ QuoteList(QuoteList),
+ /// auto (use lang-dependent quote marks)
+ Auto,
+}
+
+impl Parse for Quotes {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Quotes, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("auto"))
+ .is_ok()
+ {
+ return Ok(Quotes::Auto);
+ }
+
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(Quotes::QuoteList(QuoteList::default()));
+ }
+
+ let mut quotes = Vec::new();
+ loop {
+ let location = input.current_source_location();
+ let opening = match input.next() {
+ Ok(&Token::QuotedString(ref value)) => value.as_ref().to_owned().into(),
+ Ok(t) => return Err(location.new_unexpected_token_error(t.clone())),
+ Err(_) => break,
+ };
+
+ let closing = input.expect_string()?.as_ref().to_owned().into();
+ quotes.push(QuotePair { opening, closing });
+ }
+
+ if !quotes.is_empty() {
+ Ok(Quotes::QuoteList(QuoteList(crate::ArcSlice::from_iter(
+ quotes.into_iter(),
+ ))))
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs
new file mode 100644
index 0000000000..7fc76b3c07
--- /dev/null
+++ b/servo/components/style/values/specified/mod.rs
@@ -0,0 +1,992 @@
+/* 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.
+//!
+//! TODO(emilio): Enhance docs.
+
+use super::computed::transform::DirectionVector;
+use super::computed::{Context, ToComputedValue};
+use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks;
+use super::generics::grid::{GridLine as GenericGridLine, TrackBreadth as GenericTrackBreadth};
+use super::generics::grid::{TrackList as GenericTrackList, TrackSize as GenericTrackSize};
+use super::generics::transform::IsParallelTo;
+use super::generics::{self, GreaterThanOrEqualToOne, NonNegative};
+use super::{CSSFloat, CSSInteger};
+use crate::context::QuirksMode;
+use crate::parser::{Parse, ParserContext};
+use crate::values::specified::calc::CalcNode;
+use crate::values::{serialize_atom_identifier, serialize_number, AtomString};
+use crate::{Atom, Namespace, One, Prefix, Zero};
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use std::ops::Add;
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+#[cfg(feature = "gecko")]
+pub use self::align::{AlignContent, AlignItems, AlignSelf, AlignTracks, ContentDistribution};
+#[cfg(feature = "gecko")]
+pub use self::align::{JustifyContent, JustifyItems, JustifySelf, JustifyTracks, SelfAlignment};
+pub use self::angle::{AllowUnitlessZeroAngle, Angle};
+pub use self::animation::{
+ AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState,
+ AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis,
+ ScrollTimelineName, TransitionProperty, ViewTimelineInset
+};
+pub use self::background::{BackgroundRepeat, BackgroundSize};
+pub use self::basic_shape::FillRule;
+pub use self::border::{
+ BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice,
+ BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle, LineWidth,
+};
+pub use self::box_::{
+ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize,
+ ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow,
+ OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign,
+ ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter,
+ TouchAction, VerticalAlign, WillChange, Zoom,
+};
+pub use self::color::{
+ Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust,
+};
+pub use self::column::ColumnCount;
+pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet};
+pub use self::easing::TimingFunction;
+pub use self::effects::{BoxShadow, Filter, SimpleShadow};
+pub use self::flex::FlexBasis;
+pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle};
+pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric};
+pub use self::font::{
+ FontSize, FontSizeAdjust, FontSizeAdjustFactor, FontSizeKeyword, FontStretch, FontSynthesis,
+};
+pub use self::font::{FontVariantAlternates, FontWeight};
+pub use self::font::{FontVariantEastAsian, FontVariationSettings, LineHeight};
+pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale};
+pub use self::image::{EndingShape as GradientEndingShape, Gradient, Image, ImageRendering};
+pub use self::length::{AbsoluteLength, CalcLengthPercentage, CharacterWidth};
+pub use self::length::{FontRelativeLength, Length, LengthOrNumber, NonNegativeLengthOrNumber};
+pub use self::length::{LengthOrAuto, LengthPercentage, LengthPercentageOrAuto};
+pub use self::length::{MaxSize, Size};
+pub use self::length::{NoCalcLength, ViewportPercentageLength, ViewportVariant};
+pub use self::length::{
+ NonNegativeLength, NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto,
+};
+#[cfg(feature = "gecko")]
+pub use self::list::ListStyleType;
+pub use self::list::Quotes;
+pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate};
+pub use self::outline::OutlineStyle;
+pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize};
+pub use self::percentage::{NonNegativePercentage, Percentage};
+pub use self::position::AspectRatio;
+pub use self::position::{GridAutoFlow, GridTemplateAreas, Position, PositionOrAuto};
+pub use self::position::{MasonryAutoFlow, MasonryItemOrder, MasonryPlacement};
+pub use self::position::{PositionComponent, ZIndex};
+pub use self::ratio::Ratio;
+pub use self::rect::NonNegativeLengthOrNumberRect;
+pub use self::resolution::Resolution;
+pub use self::svg::{DProperty, MozContextProperties};
+pub use self::svg::{SVGLength, SVGOpacity, SVGPaint};
+pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth};
+pub use self::svg_path::SVGPathData;
+pub use self::text::HyphenateCharacter;
+pub use self::text::RubyPosition;
+pub use self::text::TextAlignLast;
+pub use self::text::TextUnderlinePosition;
+pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextAlign, TextIndent};
+pub use self::text::{OverflowWrap, TextEmphasisPosition, TextEmphasisStyle, WordBreak};
+pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing};
+pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify, TextTransform};
+pub use self::time::Time;
+pub use self::transform::{Rotate, Scale, Transform};
+pub use self::transform::{TransformBox, TransformOrigin, TransformStyle, Translate};
+#[cfg(feature = "gecko")]
+pub use self::ui::CursorImage;
+pub use self::ui::{BoolInteger, Cursor, UserSelect};
+pub use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent;
+
+#[cfg(feature = "gecko")]
+pub mod align;
+pub mod angle;
+pub mod animation;
+pub mod background;
+pub mod basic_shape;
+pub mod border;
+#[path = "box.rs"]
+pub mod box_;
+pub mod calc;
+pub mod color;
+pub mod column;
+pub mod counters;
+pub mod easing;
+pub mod effects;
+pub mod flex;
+pub mod font;
+#[cfg(feature = "gecko")]
+pub mod gecko;
+pub mod grid;
+pub mod image;
+pub mod length;
+pub mod list;
+pub mod motion;
+pub mod outline;
+pub mod page;
+pub mod percentage;
+pub mod position;
+pub mod ratio;
+pub mod rect;
+pub mod resolution;
+pub mod source_size_list;
+pub mod svg;
+pub mod svg_path;
+pub mod table;
+pub mod text;
+pub mod time;
+pub mod transform;
+pub mod ui;
+pub mod url;
+
+/// <angle> | <percentage>
+/// https://drafts.csswg.org/css-values/#typedef-angle-percentage
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum AngleOrPercentage {
+ Percentage(Percentage),
+ Angle(Angle),
+}
+
+impl AngleOrPercentage {
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_unitless_zero: AllowUnitlessZeroAngle,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(per) = input.try_parse(|i| Percentage::parse(context, i)) {
+ return Ok(AngleOrPercentage::Percentage(per));
+ }
+
+ Angle::parse_internal(context, input, allow_unitless_zero).map(AngleOrPercentage::Angle)
+ }
+
+ /// Allow unitless angles, used for conic-gradients as specified by the spec.
+ /// https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle
+ pub fn parse_with_unitless<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::Yes)
+ }
+}
+
+impl Parse for AngleOrPercentage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::No)
+ }
+}
+
+/// Parse a `<number>` value, with a given clamping mode.
+fn parse_number_with_clamping_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+) -> Result<Number, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Number { value, .. } if clamping_mode.is_ok(context.parsing_mode, value) => {
+ Ok(Number {
+ value,
+ calc_clamping_mode: None,
+ })
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let value = CalcNode::parse_number(context, input, function)?;
+ Ok(Number {
+ value,
+ calc_clamping_mode: Some(clamping_mode),
+ })
+ },
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+}
+
+/// A CSS `<number>` specified value.
+///
+/// https://drafts.csswg.org/css-values-3/#number-value
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialOrd, ToShmem)]
+pub struct Number {
+ /// The numeric value itself.
+ value: CSSFloat,
+ /// If this number came from a calc() expression, this tells how clamping
+ /// should be done on the value.
+ calc_clamping_mode: Option<AllowedNumericType>,
+}
+
+impl Parse for Number {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::All)
+ }
+}
+
+impl PartialEq<Number> for Number {
+ fn eq(&self, other: &Number) -> bool {
+ if self.calc_clamping_mode != other.calc_clamping_mode {
+ return false;
+ }
+
+ self.value == other.value || (self.value.is_nan() && other.value.is_nan())
+ }
+}
+
+impl Number {
+ /// Returns a new number with the value `val`.
+ #[inline]
+ fn new_with_clamping_mode(
+ value: CSSFloat,
+ calc_clamping_mode: Option<AllowedNumericType>,
+ ) -> Self {
+ Self {
+ value,
+ calc_clamping_mode,
+ }
+ }
+
+ /// Returns this percentage as a number.
+ pub fn to_percentage(&self) -> Percentage {
+ Percentage::new_with_clamping_mode(self.value, self.calc_clamping_mode)
+ }
+
+ /// Returns a new number with the value `val`.
+ #[inline]
+ pub fn new(val: CSSFloat) -> Self {
+ Self::new_with_clamping_mode(val, None)
+ }
+
+ /// Returns whether this number came from a `calc()` expression.
+ #[inline]
+ pub fn was_calc(&self) -> bool {
+ self.calc_clamping_mode.is_some()
+ }
+
+ /// Returns the numeric value, clamped if needed.
+ #[inline]
+ pub fn get(&self) -> f32 {
+ crate::values::normalize(
+ self.calc_clamping_mode
+ .map_or(self.value, |mode| mode.clamp(self.value)),
+ )
+ .min(f32::MAX)
+ .max(f32::MIN)
+ }
+
+ #[allow(missing_docs)]
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Number, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ }
+
+ #[allow(missing_docs)]
+ pub fn parse_at_least_one<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Number, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne)
+ }
+
+ /// Clamp to 1.0 if the value is over 1.0.
+ #[inline]
+ pub fn clamp_to_one(self) -> Self {
+ Number {
+ value: self.value.min(1.),
+ calc_clamping_mode: self.calc_clamping_mode,
+ }
+ }
+}
+
+impl ToComputedValue for Number {
+ type ComputedValue = CSSFloat;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> CSSFloat {
+ self.get()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &CSSFloat) -> Self {
+ Number {
+ value: *computed,
+ calc_clamping_mode: None,
+ }
+ }
+}
+
+impl ToCss for Number {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_number(self.value, self.calc_clamping_mode.is_some(), dest)
+ }
+}
+
+impl IsParallelTo for (Number, Number, Number) {
+ fn is_parallel_to(&self, vector: &DirectionVector) -> bool {
+ use euclid::approxeq::ApproxEq;
+ // If a and b is parallel, the angle between them is 0deg, so
+ // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0.
+ let self_vector = DirectionVector::new(self.0.get(), self.1.get(), self.2.get());
+ self_vector
+ .cross(*vector)
+ .square_length()
+ .approx_eq(&0.0f32)
+ }
+}
+
+impl SpecifiedValueInfo for Number {}
+
+impl Add for Number {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self::new(self.get() + other.get())
+ }
+}
+
+impl Zero for Number {
+ #[inline]
+ fn zero() -> Self {
+ Self::new(0.)
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.get() == 0.
+ }
+}
+
+impl From<Number> for f32 {
+ #[inline]
+ fn from(n: Number) -> Self {
+ n.get()
+ }
+}
+
+impl From<Number> for f64 {
+ #[inline]
+ fn from(n: Number) -> Self {
+ n.get() as f64
+ }
+}
+
+/// A Number which is >= 0.0.
+pub type NonNegativeNumber = NonNegative<Number>;
+
+impl Parse for NonNegativeNumber {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ .map(NonNegative::<Number>)
+ }
+}
+
+impl One for NonNegativeNumber {
+ #[inline]
+ fn one() -> Self {
+ NonNegativeNumber::new(1.0)
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.get() == 1.0
+ }
+}
+
+impl NonNegativeNumber {
+ /// Returns a new non-negative number with the value `val`.
+ pub fn new(val: CSSFloat) -> Self {
+ NonNegative::<Number>(Number::new(val.max(0.)))
+ }
+
+ /// Returns the numeric value.
+ #[inline]
+ pub fn get(&self) -> f32 {
+ self.0.get()
+ }
+}
+
+/// An Integer which is >= 0.
+pub type NonNegativeInteger = NonNegative<Integer>;
+
+impl Parse for NonNegativeInteger {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(Integer::parse_non_negative(context, input)?))
+ }
+}
+
+/// A Number which is >= 1.0.
+pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<Number>;
+
+impl Parse for GreaterThanOrEqualToOneNumber {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne)
+ .map(GreaterThanOrEqualToOne::<Number>)
+ }
+}
+
+/// <number> | <percentage>
+///
+/// Accepts only non-negative numbers.
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum NumberOrPercentage {
+ Percentage(Percentage),
+ Number(Number),
+}
+
+impl NumberOrPercentage {
+ fn parse_with_clamping_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ type_: AllowedNumericType,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(per) =
+ input.try_parse(|i| Percentage::parse_with_clamping_mode(context, i, type_))
+ {
+ return Ok(NumberOrPercentage::Percentage(per));
+ }
+
+ parse_number_with_clamping_mode(context, input, type_).map(NumberOrPercentage::Number)
+ }
+
+ /// Parse a non-negative number or percentage.
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ }
+
+ /// Convert the number or the percentage to a number.
+ pub fn to_percentage(self) -> Percentage {
+ match self {
+ Self::Percentage(p) => p,
+ Self::Number(n) => n.to_percentage(),
+ }
+ }
+
+ /// Convert the number or the percentage to a number.
+ pub fn to_number(self) -> Number {
+ match self {
+ Self::Percentage(p) => p.to_number(),
+ Self::Number(n) => n,
+ }
+ }
+}
+
+impl Parse for NumberOrPercentage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::All)
+ }
+}
+
+/// A non-negative <number> | <percentage>.
+pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>;
+
+impl NonNegativeNumberOrPercentage {
+ /// Returns the `100%` value.
+ #[inline]
+ pub fn hundred_percent() -> Self {
+ NonNegative(NumberOrPercentage::Percentage(Percentage::hundred()))
+ }
+
+ /// Return a particular number.
+ #[inline]
+ pub fn new_number(n: f32) -> Self {
+ NonNegative(NumberOrPercentage::Number(Number::new(n)))
+ }
+}
+
+impl Parse for NonNegativeNumberOrPercentage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(NumberOrPercentage::parse_non_negative(
+ context, input,
+ )?))
+ }
+}
+
+/// The value of Opacity is <alpha-value>, which is "<number> | <percentage>".
+/// However, we serialize the specified value as number, so it's ok to store
+/// the Opacity as Number.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub struct Opacity(Number);
+
+impl Parse for Opacity {
+ /// Opacity accepts <number> | <percentage>, so we parse it as NumberOrPercentage,
+ /// and then convert into an Number if it's a Percentage.
+ /// https://drafts.csswg.org/cssom/#serializing-css-values
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let number = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(Opacity(number))
+ }
+}
+
+impl ToComputedValue for Opacity {
+ type ComputedValue = CSSFloat;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> CSSFloat {
+ let value = self.0.to_computed_value(context);
+ if context.for_smil_animation {
+ // SMIL expects to be able to interpolate between out-of-range
+ // opacity values.
+ value
+ } else {
+ value.min(1.0).max(0.0)
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &CSSFloat) -> Self {
+ Opacity(Number::from_computed_value(computed))
+ }
+}
+
+/// A specified `<integer>`, optionally coming from a `calc()` expression.
+///
+/// <https://drafts.csswg.org/css-values/#integers>
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, ToShmem)]
+pub struct Integer {
+ value: CSSInteger,
+ was_calc: bool,
+}
+
+impl Zero for Integer {
+ #[inline]
+ fn zero() -> Self {
+ Self::new(0)
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.value() == 0
+ }
+}
+
+impl One for Integer {
+ #[inline]
+ fn one() -> Self {
+ Self::new(1)
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.value() == 1
+ }
+}
+
+impl PartialEq<i32> for Integer {
+ fn eq(&self, value: &i32) -> bool {
+ self.value() == *value
+ }
+}
+
+impl Integer {
+ /// Trivially constructs a new `Integer` value.
+ pub fn new(val: CSSInteger) -> Self {
+ Integer {
+ value: val,
+ was_calc: false,
+ }
+ }
+
+ /// Returns the integer value associated with this value.
+ pub fn value(&self) -> CSSInteger {
+ self.value
+ }
+
+ /// Trivially constructs a new integer value from a `calc()` expression.
+ fn from_calc(val: CSSInteger) -> Self {
+ Integer {
+ value: val,
+ was_calc: true,
+ }
+ }
+}
+
+impl Parse for Integer {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Number {
+ int_value: Some(v), ..
+ } => Ok(Integer::new(v)),
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let result = CalcNode::parse_integer(context, input, function)?;
+ Ok(Integer::from_calc(result))
+ },
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+}
+
+impl Integer {
+ /// Parse an integer value which is at least `min`.
+ pub fn parse_with_minimum<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ min: i32,
+ ) -> Result<Integer, ParseError<'i>> {
+ let value = Integer::parse(context, input)?;
+ // FIXME(emilio): The spec asks us to avoid rejecting it at parse
+ // time except until computed value time.
+ //
+ // It's not totally clear it's worth it though, and no other browser
+ // does this.
+ if value.value() < min {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(value)
+ }
+
+ /// Parse a non-negative integer.
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Integer, ParseError<'i>> {
+ Integer::parse_with_minimum(context, input, 0)
+ }
+
+ /// Parse a positive integer (>= 1).
+ pub fn parse_positive<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Integer, ParseError<'i>> {
+ Integer::parse_with_minimum(context, input, 1)
+ }
+}
+
+impl ToComputedValue for Integer {
+ type ComputedValue = i32;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> i32 {
+ self.value
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &i32) -> Self {
+ Integer::new(*computed)
+ }
+}
+
+impl ToCss for Integer {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.was_calc {
+ dest.write_str("calc(")?;
+ }
+ self.value.to_css(dest)?;
+ if self.was_calc {
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+}
+
+impl SpecifiedValueInfo for Integer {}
+
+/// A wrapper of Integer, with value >= 1.
+pub type PositiveInteger = GreaterThanOrEqualToOne<Integer>;
+
+impl Parse for PositiveInteger {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne)
+ }
+}
+
+/// The specified value of a grid `<track-breadth>`
+pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>;
+
+/// The specified value of a grid `<track-size>`
+pub type TrackSize = GenericTrackSize<LengthPercentage>;
+
+/// The specified value of a grid `<track-size>+`
+pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>;
+
+/// The specified value of a grid `<track-list>`
+/// (could also be `<auto-track-list>` or `<explicit-track-list>`)
+pub type TrackList = GenericTrackList<LengthPercentage, Integer>;
+
+/// The specified value of a `<grid-line>`.
+pub type GridLine = GenericGridLine<Integer>;
+
+/// `<grid-template-rows> | <grid-template-columns>`
+pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>;
+
+/// rect(...)
+pub type ClipRect = generics::GenericClipRect<LengthOrAuto>;
+
+impl Parse for ClipRect {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl ClipRect {
+ /// Parses a rect(<top>, <left>, <bottom>, <right>), allowing quirks.
+ fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("rect")?;
+
+ fn parse_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<LengthOrAuto, ParseError<'i>> {
+ LengthOrAuto::parse_quirky(context, input, allow_quirks)
+ }
+
+ input.parse_nested_block(|input| {
+ let top = parse_argument(context, input, allow_quirks)?;
+ let right;
+ let bottom;
+ let left;
+
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ right = parse_argument(context, input, allow_quirks)?;
+ input.expect_comma()?;
+ bottom = parse_argument(context, input, allow_quirks)?;
+ input.expect_comma()?;
+ left = parse_argument(context, input, allow_quirks)?;
+ } else {
+ right = parse_argument(context, input, allow_quirks)?;
+ bottom = parse_argument(context, input, allow_quirks)?;
+ left = parse_argument(context, input, allow_quirks)?;
+ }
+
+ Ok(ClipRect {
+ top,
+ right,
+ bottom,
+ left,
+ })
+ })
+ }
+}
+
+/// rect(...) | auto
+pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>;
+
+impl ClipRectOrAuto {
+ /// Parses a ClipRect or Auto, allowing quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(v) = input.try_parse(|i| ClipRect::parse_quirky(context, i, allow_quirks)) {
+ return Ok(generics::GenericClipRectOrAuto::Rect(v));
+ }
+ input.expect_ident_matching("auto")?;
+ Ok(generics::GenericClipRectOrAuto::Auto)
+ }
+}
+
+/// Whether quirks are allowed in this context.
+#[derive(Clone, Copy, PartialEq)]
+pub enum AllowQuirks {
+ /// Quirks are not allowed.
+ No,
+ /// Quirks are allowed, in quirks mode.
+ Yes,
+ /// Quirks are always allowed, used for SVG lengths.
+ Always,
+}
+
+impl AllowQuirks {
+ /// Returns `true` if quirks are allowed in this context.
+ pub fn allowed(self, quirks_mode: QuirksMode) -> bool {
+ match self {
+ AllowQuirks::Always => true,
+ AllowQuirks::No => false,
+ AllowQuirks::Yes => quirks_mode == QuirksMode::Quirks,
+ }
+ }
+}
+
+/// An attr(...) rule
+///
+/// `[namespace? `|`]? ident`
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function)]
+#[repr(C)]
+pub struct Attr {
+ /// Optional namespace prefix.
+ pub namespace_prefix: Prefix,
+ /// Optional namespace URL.
+ pub namespace_url: Namespace,
+ /// Attribute name
+ pub attribute: Atom,
+ /// Fallback value
+ pub fallback: AtomString,
+}
+
+impl Parse for Attr {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Attr, ParseError<'i>> {
+ input.expect_function_matching("attr")?;
+ input.parse_nested_block(|i| Attr::parse_function(context, i))
+ }
+}
+
+/// Get the Namespace for a given prefix from the namespace map.
+fn get_namespace_for_prefix(prefix: &Prefix, context: &ParserContext) -> Option<Namespace> {
+ context.namespaces.prefixes.get(prefix).cloned()
+}
+
+/// Try to parse a namespace and return it if parsed, or none if there was not one present
+fn parse_namespace<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<(Prefix, Namespace), ParseError<'i>> {
+ let ns_prefix = match input.next()? {
+ Token::Ident(ref prefix) => Some(Prefix::from(prefix.as_ref())),
+ Token::Delim('|') => None,
+ _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ };
+
+ if ns_prefix.is_some() && !matches!(*input.next_including_whitespace()?, Token::Delim('|')) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if let Some(prefix) = ns_prefix {
+ let ns = match get_namespace_for_prefix(&prefix, context) {
+ Some(ns) => ns,
+ None => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ };
+ Ok((prefix, ns))
+ } else {
+ Ok((Prefix::default(), Namespace::default()))
+ }
+}
+
+impl Attr {
+ /// Parse contents of attr() assuming we have already parsed `attr` and are
+ /// within a parse_nested_block()
+ pub fn parse_function<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Attr, ParseError<'i>> {
+ // Syntax is `[namespace? '|']? ident [',' fallback]?`
+ let namespace = input
+ .try_parse(|input| parse_namespace(context, input))
+ .ok();
+ let namespace_is_some = namespace.is_some();
+ let (namespace_prefix, namespace_url) = namespace.unwrap_or_default();
+
+ // If there is a namespace, ensure no whitespace following '|'
+ let attribute = Atom::from(if namespace_is_some {
+ let location = input.current_source_location();
+ match *input.next_including_whitespace()? {
+ Token::Ident(ref ident) => ident.as_ref(),
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ } else {
+ input.expect_ident()?.as_ref()
+ });
+
+ // Fallback will always be a string value for now as we do not support
+ // attr() types yet.
+ let fallback = input
+ .try_parse(|input| -> Result<AtomString, ParseError<'i>> {
+ input.expect_comma()?;
+ Ok(input.expect_string()?.as_ref().into())
+ })
+ .unwrap_or_default();
+
+ Ok(Attr {
+ namespace_prefix,
+ namespace_url,
+ attribute,
+ fallback,
+ })
+ }
+}
+
+impl ToCss for Attr {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("attr(")?;
+ if !self.namespace_prefix.is_empty() {
+ serialize_atom_identifier(&self.namespace_prefix, dest)?;
+ dest.write_char('|')?;
+ }
+ serialize_atom_identifier(&self.attribute, dest)?;
+
+ if !self.fallback.is_empty() {
+ dest.write_str(", ")?;
+ self.fallback.to_css(dest)?;
+ }
+
+ dest.write_char(')')
+ }
+}
diff --git a/servo/components/style/values/specified/motion.rs b/servo/components/style/values/specified/motion.rs
new file mode 100644
index 0000000000..98858c712c
--- /dev/null
+++ b/servo/components/style/values/specified/motion.rs
@@ -0,0 +1,343 @@
+/* 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 types for CSS values that are related to motion path.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::motion::OffsetRotate as ComputedOffsetRotate;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::motion as generics;
+use crate::values::specified::basic_shape::BasicShape;
+use crate::values::specified::position::{HorizontalPosition, VerticalPosition};
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::{Angle, Position};
+use crate::Zero;
+use cssparser::Parser;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// The specified value of ray() function.
+pub type RayFunction = generics::GenericRayFunction<Angle, Position>;
+
+/// The specified value of <offset-path>.
+pub type OffsetPathFunction =
+ generics::GenericOffsetPathFunction<BasicShape, RayFunction, SpecifiedUrl>;
+
+/// The specified value of `offset-path`.
+pub type OffsetPath = generics::GenericOffsetPath<OffsetPathFunction>;
+
+/// The specified value of `offset-position`.
+pub type OffsetPosition = generics::GenericOffsetPosition<HorizontalPosition, VerticalPosition>;
+
+/// The <coord-box> value, which defines the box that the <offset-path> sizes into.
+/// https://drafts.fxtf.org/motion-1/#valdef-offset-path-coord-box
+///
+/// <coord-box> = content-box | padding-box | border-box | fill-box | stroke-box | view-box
+/// https://drafts.csswg.org/css-box-4/#typedef-coord-box
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum CoordBox {
+ ContentBox,
+ PaddingBox,
+ BorderBox,
+ FillBox,
+ StrokeBox,
+ ViewBox,
+}
+
+impl CoordBox {
+ /// Returns true if it is default value, border-box.
+ #[inline]
+ pub fn is_default(&self) -> bool {
+ matches!(*self, Self::BorderBox)
+ }
+}
+
+impl Parse for RayFunction {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if !static_prefs::pref!("layout.css.motion-path-ray.enabled") {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ input.expect_function_matching("ray")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
+ }
+}
+
+impl RayFunction {
+ /// Parse the inner arguments of a `ray` function.
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::PositionOrAuto;
+
+ let mut angle = None;
+ let mut size = None;
+ let mut contain = false;
+ let mut position = None;
+ loop {
+ if angle.is_none() {
+ angle = input.try_parse(|i| Angle::parse(context, i)).ok();
+ }
+
+ if size.is_none() {
+ size = input.try_parse(generics::RaySize::parse).ok();
+ if size.is_some() {
+ continue;
+ }
+ }
+
+ if !contain {
+ contain = input
+ .try_parse(|i| i.expect_ident_matching("contain"))
+ .is_ok();
+ if contain {
+ continue;
+ }
+ }
+
+ if position.is_none() {
+ if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
+ let pos = Position::parse(context, input)?;
+ position = Some(PositionOrAuto::Position(pos));
+ }
+
+ if position.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ if angle.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(RayFunction {
+ angle: angle.unwrap(),
+ // If no <ray-size> is specified it defaults to closest-side.
+ size: size.unwrap_or(generics::RaySize::ClosestSide),
+ contain,
+ position: position.unwrap_or(PositionOrAuto::auto()),
+ })
+ }
+}
+
+impl Parse for OffsetPathFunction {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::basic_shape::{AllowedBasicShapes, ShapeType};
+
+ // <offset-path> = <ray()> | <url> | <basic-shape>
+ // https://drafts.fxtf.org/motion-1/#typedef-offset-path
+
+ if static_prefs::pref!("layout.css.motion-path-ray.enabled") {
+ if let Ok(ray) = input.try_parse(|i| RayFunction::parse(context, i)) {
+ return Ok(OffsetPathFunction::Ray(ray));
+ }
+ }
+
+ if static_prefs::pref!("layout.css.motion-path-url.enabled") {
+ if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
+ return Ok(OffsetPathFunction::Url(url));
+ }
+ }
+
+ let allowed_shapes = if static_prefs::pref!("layout.css.motion-path-basic-shapes.enabled") {
+ AllowedBasicShapes::ALL
+ } else {
+ AllowedBasicShapes::PATH
+ };
+
+ BasicShape::parse(context, input, allowed_shapes, ShapeType::Outline)
+ .map(OffsetPathFunction::Shape)
+ }
+}
+
+impl Parse for OffsetPath {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Parse none.
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(OffsetPath::none());
+ }
+
+ let mut path = None;
+ let mut coord_box = None;
+ loop {
+ if path.is_none() {
+ path = input
+ .try_parse(|i| OffsetPathFunction::parse(context, i))
+ .ok();
+ }
+
+ if static_prefs::pref!("layout.css.motion-path-coord-box.enabled") &&
+ coord_box.is_none()
+ {
+ coord_box = input.try_parse(CoordBox::parse).ok();
+ if coord_box.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ if let Some(p) = path {
+ return Ok(OffsetPath::OffsetPath {
+ path: Box::new(p),
+ coord_box: coord_box.unwrap_or(CoordBox::BorderBox),
+ });
+ }
+
+ match coord_box {
+ Some(c) => Ok(OffsetPath::CoordBox(c)),
+ None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+}
+
+/// The direction of offset-rotate.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[repr(u8)]
+pub enum OffsetRotateDirection {
+ /// Unspecified direction keyword.
+ #[css(skip)]
+ None,
+ /// 0deg offset (face forward).
+ Auto,
+ /// 180deg offset (face backward).
+ Reverse,
+}
+
+impl OffsetRotateDirection {
+ /// Returns true if it is none (i.e. the keyword is not specified).
+ #[inline]
+ fn is_none(&self) -> bool {
+ *self == OffsetRotateDirection::None
+ }
+}
+
+#[inline]
+fn direction_specified_and_angle_is_zero(direction: &OffsetRotateDirection, angle: &Angle) -> bool {
+ !direction.is_none() && angle.is_zero()
+}
+
+/// The specified offset-rotate.
+/// The syntax is: "[ auto | reverse ] || <angle>"
+///
+/// https://drafts.fxtf.org/motion-1/#offset-rotate-property
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct OffsetRotate {
+ /// [auto | reverse].
+ #[css(skip_if = "OffsetRotateDirection::is_none")]
+ direction: OffsetRotateDirection,
+ /// <angle>.
+ /// If direction is None, this is a fixed angle which indicates a
+ /// constant clockwise rotation transformation applied to it by this
+ /// specified rotation angle. Otherwise, the angle will be added to
+ /// the angle of the direction in layout.
+ #[css(contextual_skip_if = "direction_specified_and_angle_is_zero")]
+ angle: Angle,
+}
+
+impl OffsetRotate {
+ /// Returns the initial value, auto.
+ #[inline]
+ pub fn auto() -> Self {
+ OffsetRotate {
+ direction: OffsetRotateDirection::Auto,
+ angle: Angle::zero(),
+ }
+ }
+
+ /// Returns true if self is auto 0deg.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ self.direction == OffsetRotateDirection::Auto && self.angle.is_zero()
+ }
+}
+
+impl Parse for OffsetRotate {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let mut direction = input.try_parse(OffsetRotateDirection::parse);
+ let angle = input.try_parse(|i| Angle::parse(context, i));
+ if direction.is_err() {
+ // The direction and angle could be any order, so give it a change to parse
+ // direction again.
+ direction = input.try_parse(OffsetRotateDirection::parse);
+ }
+
+ if direction.is_err() && angle.is_err() {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(OffsetRotate {
+ direction: direction.unwrap_or(OffsetRotateDirection::None),
+ angle: angle.unwrap_or(Zero::zero()),
+ })
+ }
+}
+
+impl ToComputedValue for OffsetRotate {
+ type ComputedValue = ComputedOffsetRotate;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ use crate::values::computed::Angle as ComputedAngle;
+
+ ComputedOffsetRotate {
+ auto: !self.direction.is_none(),
+ angle: if self.direction == OffsetRotateDirection::Reverse {
+ // The computed value should always convert "reverse" into "auto".
+ // e.g. "reverse calc(20deg + 10deg)" => "auto 210deg"
+ self.angle.to_computed_value(context) + ComputedAngle::from_degrees(180.0)
+ } else {
+ self.angle.to_computed_value(context)
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ OffsetRotate {
+ direction: if computed.auto {
+ OffsetRotateDirection::Auto
+ } else {
+ OffsetRotateDirection::None
+ },
+ angle: ToComputedValue::from_computed_value(&computed.angle),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/outline.rs b/servo/components/style/values/specified/outline.rs
new file mode 100644
index 0000000000..6e5382d4c2
--- /dev/null
+++ b/servo/components/style/values/specified/outline.rs
@@ -0,0 +1,71 @@
+/* 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 outline properties
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::specified::BorderStyle;
+use cssparser::Parser;
+use selectors::parser::SelectorParseErrorKind;
+use style_traits::ParseError;
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Ord,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// <https://drafts.csswg.org/css-ui/#propdef-outline-style>
+pub enum OutlineStyle {
+ /// auto
+ Auto,
+ /// <border-style>
+ BorderStyle(BorderStyle),
+}
+
+impl OutlineStyle {
+ #[inline]
+ /// Get default value as None
+ pub fn none() -> OutlineStyle {
+ OutlineStyle::BorderStyle(BorderStyle::None)
+ }
+
+ #[inline]
+ /// Get value for None or Hidden
+ pub fn none_or_hidden(&self) -> bool {
+ match *self {
+ OutlineStyle::Auto => false,
+ OutlineStyle::BorderStyle(ref style) => style.none_or_hidden(),
+ }
+ }
+}
+
+impl Parse for OutlineStyle {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<OutlineStyle, ParseError<'i>> {
+ if let Ok(border_style) = input.try_parse(BorderStyle::parse) {
+ if let BorderStyle::Hidden = border_style {
+ return Err(input
+ .new_custom_error(SelectorParseErrorKind::UnexpectedIdent("hidden".into())));
+ }
+
+ return Ok(OutlineStyle::BorderStyle(border_style));
+ }
+
+ input.expect_ident_matching("auto")?;
+ Ok(OutlineStyle::Auto)
+ }
+}
diff --git a/servo/components/style/values/specified/page.rs b/servo/components/style/values/specified/page.rs
new file mode 100644
index 0000000000..76d9105e8f
--- /dev/null
+++ b/servo/components/style/values/specified/page.rs
@@ -0,0 +1,99 @@
+/* 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 @page at-rule properties and named-page style properties
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::size::Size2D;
+use crate::values::specified::length::NonNegativeLength;
+use crate::values::{generics, CustomIdent};
+use cssparser::Parser;
+use style_traits::ParseError;
+
+pub use generics::page::PageOrientation;
+pub use generics::page::PageSizeOrientation;
+pub use generics::page::PaperSize;
+/// Specified value of the @page size descriptor
+pub type PageSize = generics::page::PageSize<Size2D<NonNegativeLength>>;
+
+impl Parse for PageSize {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Try to parse as <page-size> [ <orientation> ]
+ if let Ok(paper_size) = input.try_parse(PaperSize::parse) {
+ let orientation = input
+ .try_parse(PageSizeOrientation::parse)
+ .unwrap_or(PageSizeOrientation::Portrait);
+ return Ok(PageSize::PaperSize(paper_size, orientation));
+ }
+ // Try to parse as <orientation> [ <page-size> ]
+ if let Ok(orientation) = input.try_parse(PageSizeOrientation::parse) {
+ if let Ok(paper_size) = input.try_parse(PaperSize::parse) {
+ return Ok(PageSize::PaperSize(paper_size, orientation));
+ }
+ return Ok(PageSize::Orientation(orientation));
+ }
+ // Try to parse dimensions
+ if let Ok(size) =
+ input.try_parse(|i| Size2D::parse_with(context, i, NonNegativeLength::parse))
+ {
+ return Ok(PageSize::Size(size));
+ }
+ // auto value
+ input.expect_ident_matching("auto")?;
+ Ok(PageSize::Auto)
+ }
+}
+
+/// Page name value.
+///
+/// https://drafts.csswg.org/css-page-3/#using-named-pages
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum PageName {
+ /// `auto` value.
+ Auto,
+ /// Page name value
+ PageName(CustomIdent),
+}
+
+impl Parse for PageName {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ Ok(match_ignore_ascii_case! { ident,
+ "auto" => PageName::auto(),
+ _ => PageName::PageName(CustomIdent::from_ident(location, ident, &[])?),
+ })
+ }
+}
+
+impl PageName {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ PageName::Auto
+ }
+
+ /// Whether this is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, PageName::Auto)
+ }
+}
diff --git a/servo/components/style/values/specified/percentage.rs b/servo/components/style/values/specified/percentage.rs
new file mode 100644
index 0000000000..ccf16d6463
--- /dev/null
+++ b/servo/components/style/values/specified/percentage.rs
@@ -0,0 +1,225 @@
+/* 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 percentages.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::percentage::Percentage as ComputedPercentage;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::NonNegative;
+use crate::values::specified::calc::CalcNode;
+use crate::values::specified::Number;
+use crate::values::{normalize, serialize_percentage, CSSFloat};
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss};
+
+/// A percentage value.
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Percentage {
+ /// The percentage value as a float.
+ ///
+ /// [0 .. 100%] maps to [0.0 .. 1.0]
+ value: CSSFloat,
+ /// If this percentage came from a calc() expression, this tells how
+ /// clamping should be done on the value.
+ calc_clamping_mode: Option<AllowedNumericType>,
+}
+
+impl ToCss for Percentage {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.calc_clamping_mode.is_some() {
+ dest.write_str("calc(")?;
+ }
+
+ serialize_percentage(self.value, dest)?;
+
+ if self.calc_clamping_mode.is_some() {
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+}
+
+impl Percentage {
+ /// Creates a percentage from a numeric value.
+ pub(super) fn new_with_clamping_mode(
+ value: CSSFloat,
+ calc_clamping_mode: Option<AllowedNumericType>,
+ ) -> Self {
+ Self {
+ value,
+ calc_clamping_mode,
+ }
+ }
+
+ /// Creates a percentage from a numeric value.
+ pub fn new(value: CSSFloat) -> Self {
+ Self::new_with_clamping_mode(value, None)
+ }
+
+ /// `0%`
+ #[inline]
+ pub fn zero() -> Self {
+ Percentage {
+ value: 0.,
+ calc_clamping_mode: None,
+ }
+ }
+
+ /// `100%`
+ #[inline]
+ pub fn hundred() -> Self {
+ Percentage {
+ value: 1.,
+ calc_clamping_mode: None,
+ }
+ }
+
+ /// Gets the underlying value for this float.
+ pub fn get(&self) -> CSSFloat {
+ self.calc_clamping_mode
+ .map_or(self.value, |mode| mode.clamp(self.value))
+ }
+
+ /// Returns this percentage as a number.
+ pub fn to_number(&self) -> Number {
+ Number::new_with_clamping_mode(self.value, self.calc_clamping_mode)
+ }
+
+ /// Returns the calc() clamping mode for this percentage.
+ pub fn calc_clamping_mode(&self) -> Option<AllowedNumericType> {
+ self.calc_clamping_mode
+ }
+
+ /// Reverses this percentage, preserving calc-ness.
+ ///
+ /// For example: If it was 20%, convert it into 80%.
+ pub fn reverse(&mut self) {
+ let new_value = 1. - self.value;
+ self.value = new_value;
+ }
+
+ /// Parses a specific kind of percentage.
+ pub fn parse_with_clamping_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ num_context: AllowedNumericType,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Percentage { unit_value, .. }
+ if num_context.is_ok(context.parsing_mode, unit_value) =>
+ {
+ Ok(Percentage::new(unit_value))
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let value = CalcNode::parse_percentage(context, input, function)?;
+ Ok(Percentage {
+ value,
+ calc_clamping_mode: Some(num_context),
+ })
+ },
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+
+ /// Parses a percentage token, but rejects it if it's negative.
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ }
+
+ /// Parses a percentage token, but rejects it if it's negative or more than
+ /// 100%.
+ pub fn parse_zero_to_a_hundred<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::ZeroToOne)
+ }
+
+ /// Clamp to 100% if the value is over 100%.
+ #[inline]
+ pub fn clamp_to_hundred(self) -> Self {
+ Percentage {
+ value: self.value.min(1.),
+ calc_clamping_mode: self.calc_clamping_mode,
+ }
+ }
+}
+
+impl Parse for Percentage {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::All)
+ }
+}
+
+impl ToComputedValue for Percentage {
+ type ComputedValue = ComputedPercentage;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ ComputedPercentage(normalize(self.get()))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Percentage::new(computed.0)
+ }
+}
+
+impl SpecifiedValueInfo for Percentage {}
+
+/// Turns the percentage into a plain float.
+pub trait ToPercentage {
+ /// Returns whether this percentage used to be a calc().
+ fn is_calc(&self) -> bool {
+ false
+ }
+ /// Turns the percentage into a plain float.
+ fn to_percentage(&self) -> CSSFloat;
+}
+
+impl ToPercentage for Percentage {
+ fn is_calc(&self) -> bool {
+ self.calc_clamping_mode.is_some()
+ }
+
+ fn to_percentage(&self) -> CSSFloat {
+ self.get()
+ }
+}
+
+/// A wrapper of Percentage, whose value must be >= 0.
+pub type NonNegativePercentage = NonNegative<Percentage>;
+
+impl Parse for NonNegativePercentage {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(Percentage::parse_non_negative(context, input)?))
+ }
+}
+
+impl NonNegativePercentage {
+ /// Convert to ComputedPercentage, for FontFaceRule size-adjust getter.
+ #[inline]
+ pub fn compute(&self) -> ComputedPercentage {
+ ComputedPercentage(self.0.get())
+ }
+}
diff --git a/servo/components/style/values/specified/position.rs b/servo/components/style/values/specified/position.rs
new file mode 100644
index 0000000000..bab853d972
--- /dev/null
+++ b/servo/components/style/values/specified/position.rs
@@ -0,0 +1,955 @@
+/* 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/. */
+
+//! CSS handling for the specified value of
+//! [`position`][position]s
+//!
+//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position
+
+use crate::parser::{Parse, ParserContext};
+use crate::selector_map::PrecomputedHashMap;
+use crate::str::HTML_SPACE_CHARACTERS;
+use crate::values::computed::LengthPercentage as ComputedLengthPercentage;
+use crate::values::computed::{Context, Percentage, ToComputedValue};
+use crate::values::generics::position::AspectRatio as GenericAspectRatio;
+use crate::values::generics::position::Position as GenericPosition;
+use crate::values::generics::position::PositionComponent as GenericPositionComponent;
+use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto;
+use crate::values::generics::position::ZIndex as GenericZIndex;
+use crate::values::specified::{AllowQuirks, Integer, LengthPercentage, NonNegativeNumber};
+use crate::{Atom, Zero};
+use cssparser::Parser;
+use selectors::parser::SelectorParseErrorKind;
+use servo_arc::Arc;
+use std::collections::hash_map::Entry;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// The specified value of a CSS `<position>`
+pub type Position = GenericPosition<HorizontalPosition, VerticalPosition>;
+
+/// The specified value of an `auto | <position>`.
+pub type PositionOrAuto = GenericPositionOrAuto<Position>;
+
+/// The specified value of a horizontal position.
+pub type HorizontalPosition = PositionComponent<HorizontalPositionKeyword>;
+
+/// The specified value of a vertical position.
+pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>;
+
+/// The specified value of a component of a CSS `<position>`.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum PositionComponent<S> {
+ /// `center`
+ Center,
+ /// `<length-percentage>`
+ Length(LengthPercentage),
+ /// `<side> <length-percentage>?`
+ Side(S, Option<LengthPercentage>),
+}
+
+/// A keyword for the X direction.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum HorizontalPositionKeyword {
+ Left,
+ Right,
+}
+
+/// A keyword for the Y direction.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum VerticalPositionKeyword {
+ Top,
+ Bottom,
+}
+
+impl Parse for Position {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let position = Self::parse_three_value_quirky(context, input, AllowQuirks::No)?;
+ if position.is_three_value_syntax() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(position)
+ }
+}
+
+impl Position {
+ /// Parses a `<bg-position>`, with quirks.
+ pub fn parse_three_value_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ match input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) {
+ Ok(x_pos @ PositionComponent::Center) => {
+ if let Ok(y_pos) =
+ input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks))
+ {
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ let x_pos = input
+ .try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks))
+ .unwrap_or(x_pos);
+ let y_pos = PositionComponent::Center;
+ return Ok(Self::new(x_pos, y_pos));
+ },
+ Ok(PositionComponent::Side(x_keyword, lp)) => {
+ if input
+ .try_parse(|i| i.expect_ident_matching("center"))
+ .is_ok()
+ {
+ let x_pos = PositionComponent::Side(x_keyword, lp);
+ let y_pos = PositionComponent::Center;
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
+ let y_lp = input
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ .ok();
+ let x_pos = PositionComponent::Side(x_keyword, lp);
+ let y_pos = PositionComponent::Side(y_keyword, y_lp);
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ let x_pos = PositionComponent::Side(x_keyword, None);
+ let y_pos = lp.map_or(PositionComponent::Center, PositionComponent::Length);
+ return Ok(Self::new(x_pos, y_pos));
+ },
+ Ok(x_pos @ PositionComponent::Length(_)) => {
+ if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
+ let y_pos = PositionComponent::Side(y_keyword, None);
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ if let Ok(y_lp) =
+ input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ {
+ let y_pos = PositionComponent::Length(y_lp);
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ let y_pos = PositionComponent::Center;
+ let _ = input.try_parse(|i| i.expect_ident_matching("center"));
+ return Ok(Self::new(x_pos, y_pos));
+ },
+ Err(_) => {},
+ }
+ let y_keyword = VerticalPositionKeyword::parse(input)?;
+ let lp_and_x_pos: Result<_, ParseError> = input.try_parse(|i| {
+ let y_lp = i
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ .ok();
+ if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) {
+ let x_lp = i
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ .ok();
+ let x_pos = PositionComponent::Side(x_keyword, x_lp);
+ return Ok((y_lp, x_pos));
+ };
+ i.expect_ident_matching("center")?;
+ let x_pos = PositionComponent::Center;
+ Ok((y_lp, x_pos))
+ });
+ if let Ok((y_lp, x_pos)) = lp_and_x_pos {
+ let y_pos = PositionComponent::Side(y_keyword, y_lp);
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ let x_pos = PositionComponent::Center;
+ let y_pos = PositionComponent::Side(y_keyword, None);
+ Ok(Self::new(x_pos, y_pos))
+ }
+
+ /// `center center`
+ #[inline]
+ pub fn center() -> Self {
+ Self::new(PositionComponent::Center, PositionComponent::Center)
+ }
+
+ /// Returns true if this uses a 3 value syntax.
+ #[inline]
+ fn is_three_value_syntax(&self) -> bool {
+ self.horizontal.component_count() != self.vertical.component_count()
+ }
+}
+
+impl ToCss for Position {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match (&self.horizontal, &self.vertical) {
+ (
+ x_pos @ &PositionComponent::Side(_, Some(_)),
+ &PositionComponent::Length(ref y_lp),
+ ) => {
+ x_pos.to_css(dest)?;
+ dest.write_str(" top ")?;
+ y_lp.to_css(dest)
+ },
+ (
+ &PositionComponent::Length(ref x_lp),
+ y_pos @ &PositionComponent::Side(_, Some(_)),
+ ) => {
+ dest.write_str("left ")?;
+ x_lp.to_css(dest)?;
+ dest.write_char(' ')?;
+ y_pos.to_css(dest)
+ },
+ (x_pos, y_pos) => {
+ x_pos.to_css(dest)?;
+ dest.write_char(' ')?;
+ y_pos.to_css(dest)
+ },
+ }
+ }
+}
+
+impl<S: Parse> Parse for PositionComponent<S> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl<S: Parse> PositionComponent<S> {
+ /// Parses a component of a CSS position, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("center"))
+ .is_ok()
+ {
+ return Ok(PositionComponent::Center);
+ }
+ if let Ok(lp) =
+ input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ {
+ return Ok(PositionComponent::Length(lp));
+ }
+ let keyword = S::parse(context, input)?;
+ let lp = input
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ .ok();
+ Ok(PositionComponent::Side(keyword, lp))
+ }
+}
+
+impl<S> GenericPositionComponent for PositionComponent<S> {
+ fn is_center(&self) -> bool {
+ match *self {
+ PositionComponent::Center => true,
+ PositionComponent::Length(LengthPercentage::Percentage(ref per)) => per.0 == 0.5,
+ // 50% from any side is still the center.
+ PositionComponent::Side(_, Some(LengthPercentage::Percentage(ref per))) => per.0 == 0.5,
+ _ => false,
+ }
+ }
+}
+
+impl<S> PositionComponent<S> {
+ /// `0%`
+ pub fn zero() -> Self {
+ PositionComponent::Length(LengthPercentage::Percentage(Percentage::zero()))
+ }
+
+ /// Returns the count of this component.
+ fn component_count(&self) -> usize {
+ match *self {
+ PositionComponent::Length(..) | PositionComponent::Center => 1,
+ PositionComponent::Side(_, ref lp) => {
+ if lp.is_some() {
+ 2
+ } else {
+ 1
+ }
+ },
+ }
+ }
+}
+
+impl<S: Side> ToComputedValue for PositionComponent<S> {
+ type ComputedValue = ComputedLengthPercentage;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ PositionComponent::Center => ComputedLengthPercentage::new_percent(Percentage(0.5)),
+ PositionComponent::Side(ref keyword, None) => {
+ let p = Percentage(if keyword.is_start() { 0. } else { 1. });
+ ComputedLengthPercentage::new_percent(p)
+ },
+ PositionComponent::Side(ref keyword, Some(ref length)) if !keyword.is_start() => {
+ let length = length.to_computed_value(context);
+ // We represent `<end-side> <length>` as `calc(100% - <length>)`.
+ ComputedLengthPercentage::hundred_percent_minus(length, AllowedNumericType::All)
+ },
+ PositionComponent::Side(_, Some(ref length)) |
+ PositionComponent::Length(ref length) => length.to_computed_value(context),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ PositionComponent::Length(ToComputedValue::from_computed_value(computed))
+ }
+}
+
+impl<S: Side> PositionComponent<S> {
+ /// The initial specified value of a position component, i.e. the start side.
+ pub fn initial_specified_value() -> Self {
+ PositionComponent::Side(S::start(), None)
+ }
+}
+
+/// Represents a side, either horizontal or vertical, of a CSS position.
+pub trait Side {
+ /// Returns the start side.
+ fn start() -> Self;
+
+ /// Returns whether this side is the start side.
+ fn is_start(&self) -> bool;
+}
+
+impl Side for HorizontalPositionKeyword {
+ #[inline]
+ fn start() -> Self {
+ HorizontalPositionKeyword::Left
+ }
+
+ #[inline]
+ fn is_start(&self) -> bool {
+ *self == Self::start()
+ }
+}
+
+impl Side for VerticalPositionKeyword {
+ #[inline]
+ fn start() -> Self {
+ VerticalPositionKeyword::Top
+ }
+
+ #[inline]
+ fn is_start(&self) -> bool {
+ *self == Self::start()
+ }
+}
+
+/// Controls how the auto-placement algorithm works specifying exactly how auto-placed items
+/// get flowed into the grid.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "row,column,dense")]
+#[repr(C)]
+pub struct GridAutoFlow(u8);
+bitflags! {
+ impl GridAutoFlow: u8 {
+ /// 'row' - mutually exclusive with 'column'
+ const ROW = 1 << 0;
+ /// 'column' - mutually exclusive with 'row'
+ const COLUMN = 1 << 1;
+ /// 'dense'
+ const DENSE = 1 << 2;
+ }
+}
+
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+/// Masonry auto-placement algorithm packing.
+pub enum MasonryPlacement {
+ /// Place the item in the track(s) with the smallest extent so far.
+ Pack,
+ /// Place the item after the last item, from start to end.
+ Next,
+}
+
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+/// Masonry auto-placement algorithm item sorting option.
+pub enum MasonryItemOrder {
+ /// Place all items with a definite placement before auto-placed items.
+ DefiniteFirst,
+ /// Place items in `order-modified document order`.
+ Ordered,
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Controls how the Masonry layout algorithm works
+/// specifying exactly how auto-placed items get flowed in the masonry axis.
+pub struct MasonryAutoFlow {
+ /// Specify how to pick a auto-placement track.
+ #[css(contextual_skip_if = "is_pack_with_non_default_order")]
+ pub placement: MasonryPlacement,
+ /// Specify how to pick an item to place.
+ #[css(skip_if = "is_item_order_definite_first")]
+ pub order: MasonryItemOrder,
+}
+
+#[inline]
+fn is_pack_with_non_default_order(placement: &MasonryPlacement, order: &MasonryItemOrder) -> bool {
+ *placement == MasonryPlacement::Pack && *order != MasonryItemOrder::DefiniteFirst
+}
+
+#[inline]
+fn is_item_order_definite_first(order: &MasonryItemOrder) -> bool {
+ *order == MasonryItemOrder::DefiniteFirst
+}
+
+impl MasonryAutoFlow {
+ #[inline]
+ /// Get initial `masonry-auto-flow` value.
+ pub fn initial() -> MasonryAutoFlow {
+ MasonryAutoFlow {
+ placement: MasonryPlacement::Pack,
+ order: MasonryItemOrder::DefiniteFirst,
+ }
+ }
+}
+
+impl Parse for MasonryAutoFlow {
+ /// [ definite-first | ordered ] || [ pack | next ]
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<MasonryAutoFlow, ParseError<'i>> {
+ let mut value = MasonryAutoFlow::initial();
+ let mut got_placement = false;
+ let mut got_order = false;
+ while !input.is_exhausted() {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ let success = match_ignore_ascii_case! { &ident,
+ "pack" if !got_placement => {
+ got_placement = true;
+ true
+ },
+ "next" if !got_placement => {
+ value.placement = MasonryPlacement::Next;
+ got_placement = true;
+ true
+ },
+ "definite-first" if !got_order => {
+ got_order = true;
+ true
+ },
+ "ordered" if !got_order => {
+ value.order = MasonryItemOrder::Ordered;
+ got_order = true;
+ true
+ },
+ _ => false
+ };
+ if !success {
+ return Err(location
+ .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())));
+ }
+ }
+
+ if got_placement || got_order {
+ Ok(value)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+// TODO: Can be derived with some care.
+impl Parse for GridAutoFlow {
+ /// [ row | column ] || dense
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<GridAutoFlow, ParseError<'i>> {
+ let mut track = None;
+ let mut dense = GridAutoFlow::empty();
+
+ while !input.is_exhausted() {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ let success = match_ignore_ascii_case! { &ident,
+ "row" if track.is_none() => {
+ track = Some(GridAutoFlow::ROW);
+ true
+ },
+ "column" if track.is_none() => {
+ track = Some(GridAutoFlow::COLUMN);
+ true
+ },
+ "dense" if dense.is_empty() => {
+ dense = GridAutoFlow::DENSE;
+ true
+ },
+ _ => false,
+ };
+ if !success {
+ return Err(location
+ .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())));
+ }
+ }
+
+ if track.is_some() || !dense.is_empty() {
+ Ok(track.unwrap_or(GridAutoFlow::ROW) | dense)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+impl ToCss for GridAutoFlow {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if *self == GridAutoFlow::ROW {
+ return dest.write_str("row");
+ }
+
+ if *self == GridAutoFlow::COLUMN {
+ return dest.write_str("column");
+ }
+
+ if *self == GridAutoFlow::ROW | GridAutoFlow::DENSE {
+ return dest.write_str("dense");
+ }
+
+ if *self == GridAutoFlow::COLUMN | GridAutoFlow::DENSE {
+ return dest.write_str("column dense");
+ }
+
+ debug_assert!(false, "Unknown or invalid grid-autoflow value");
+ Ok(())
+ }
+}
+
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// https://drafts.csswg.org/css-grid/#named-grid-area
+pub struct TemplateAreas {
+ /// `named area` containing for each template area
+ #[css(skip)]
+ pub areas: crate::OwnedSlice<NamedArea>,
+ /// The simplified CSS strings for serialization purpose.
+ /// https://drafts.csswg.org/css-grid/#serialize-template
+ // Note: We also use the length of `strings` when computing the explicit grid end line number
+ // (i.e. row number).
+ #[css(iterable)]
+ pub strings: crate::OwnedSlice<crate::OwnedStr>,
+ /// The number of columns of the grid.
+ #[css(skip)]
+ pub width: u32,
+}
+
+/// Parser for grid template areas.
+#[derive(Default)]
+pub struct TemplateAreasParser {
+ areas: Vec<NamedArea>,
+ area_indices: PrecomputedHashMap<Atom, usize>,
+ strings: Vec<crate::OwnedStr>,
+ width: u32,
+ row: u32,
+}
+
+impl TemplateAreasParser {
+ /// Parse a single string.
+ pub fn try_parse_string<'i>(
+ &mut self,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<(), ParseError<'i>> {
+ input.try_parse(|input| {
+ self.parse_string(input.expect_string()?)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ })
+ }
+
+ /// Parse a single string.
+ fn parse_string(&mut self, string: &str) -> Result<(), ()> {
+ self.row += 1;
+ let mut simplified_string = String::new();
+ let mut current_area_index: Option<usize> = None;
+ let mut column = 0u32;
+ for token in TemplateAreasTokenizer(string) {
+ column += 1;
+ if column > 1 {
+ simplified_string.push(' ');
+ }
+ let name = if let Some(token) = token? {
+ simplified_string.push_str(token);
+ Atom::from(token)
+ } else {
+ if let Some(index) = current_area_index.take() {
+ if self.areas[index].columns.end != column {
+ return Err(());
+ }
+ }
+ simplified_string.push('.');
+ continue;
+ };
+ if let Some(index) = current_area_index {
+ if self.areas[index].name == name {
+ if self.areas[index].rows.start == self.row {
+ self.areas[index].columns.end += 1;
+ }
+ continue;
+ }
+ if self.areas[index].columns.end != column {
+ return Err(());
+ }
+ }
+ match self.area_indices.entry(name) {
+ Entry::Occupied(ref e) => {
+ let index = *e.get();
+ if self.areas[index].columns.start != column ||
+ self.areas[index].rows.end != self.row
+ {
+ return Err(());
+ }
+ self.areas[index].rows.end += 1;
+ current_area_index = Some(index);
+ },
+ Entry::Vacant(v) => {
+ let index = self.areas.len();
+ let name = v.key().clone();
+ v.insert(index);
+ self.areas.push(NamedArea {
+ name,
+ columns: UnsignedRange {
+ start: column,
+ end: column + 1,
+ },
+ rows: UnsignedRange {
+ start: self.row,
+ end: self.row + 1,
+ },
+ });
+ current_area_index = Some(index);
+ },
+ }
+ }
+ if column == 0 {
+ // Each string must produce a valid token.
+ // https://github.com/w3c/csswg-drafts/issues/5110
+ return Err(());
+ }
+ if let Some(index) = current_area_index {
+ if self.areas[index].columns.end != column + 1 {
+ debug_assert_ne!(self.areas[index].rows.start, self.row);
+ return Err(());
+ }
+ }
+ if self.row == 1 {
+ self.width = column;
+ } else if self.width != column {
+ return Err(());
+ }
+
+ self.strings.push(simplified_string.into());
+ Ok(())
+ }
+
+ /// Return the parsed template areas.
+ pub fn finish(self) -> Result<TemplateAreas, ()> {
+ if self.strings.is_empty() {
+ return Err(());
+ }
+ Ok(TemplateAreas {
+ areas: self.areas.into(),
+ strings: self.strings.into(),
+ width: self.width,
+ })
+ }
+}
+
+impl TemplateAreas {
+ fn parse_internal(input: &mut Parser) -> Result<Self, ()> {
+ let mut parser = TemplateAreasParser::default();
+ while parser.try_parse_string(input).is_ok() {}
+ parser.finish()
+ }
+}
+
+impl Parse for TemplateAreas {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(input)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+/// Arc type for `Arc<TemplateAreas>`
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct TemplateAreasArc(#[ignore_malloc_size_of = "Arc"] pub Arc<TemplateAreas>);
+
+impl Parse for TemplateAreasArc {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let parsed = TemplateAreas::parse(context, input)?;
+ Ok(TemplateAreasArc(Arc::new(parsed)))
+ }
+}
+
+/// A range of rows or columns. Using this instead of std::ops::Range for FFI
+/// purposes.
+#[repr(C)]
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct UnsignedRange {
+ /// The start of the range.
+ pub start: u32,
+ /// The end of the range.
+ pub end: u32,
+}
+
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Not associated with any particular grid item, but can be referenced from the
+/// grid-placement properties.
+pub struct NamedArea {
+ /// Name of the `named area`
+ pub name: Atom,
+ /// Rows of the `named area`
+ pub rows: UnsignedRange,
+ /// Columns of the `named area`
+ pub columns: UnsignedRange,
+}
+
+/// Tokenize the string into a list of the tokens,
+/// using longest-match semantics
+struct TemplateAreasTokenizer<'a>(&'a str);
+
+impl<'a> Iterator for TemplateAreasTokenizer<'a> {
+ type Item = Result<Option<&'a str>, ()>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let rest = self.0.trim_start_matches(HTML_SPACE_CHARACTERS);
+ if rest.is_empty() {
+ return None;
+ }
+ if rest.starts_with('.') {
+ self.0 = &rest[rest.find(|c| c != '.').unwrap_or(rest.len())..];
+ return Some(Ok(None));
+ }
+ if !rest.starts_with(is_name_code_point) {
+ return Some(Err(()));
+ }
+ let token_len = rest.find(|c| !is_name_code_point(c)).unwrap_or(rest.len());
+ let token = &rest[..token_len];
+ self.0 = &rest[token_len..];
+ Some(Ok(Some(token)))
+ }
+}
+
+fn is_name_code_point(c: char) -> bool {
+ c >= 'A' && c <= 'Z' ||
+ c >= 'a' && c <= 'z' ||
+ c >= '\u{80}' ||
+ c == '_' ||
+ c >= '0' && c <= '9' ||
+ c == '-'
+}
+
+/// This property specifies named grid areas.
+///
+/// The syntax of this property also provides a visualization of the structure
+/// of the grid, making the overall layout of the grid container easier to
+/// understand.
+#[repr(C, u8)]
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum GridTemplateAreas {
+ /// The `none` value.
+ None,
+ /// The actual value.
+ Areas(TemplateAreasArc),
+}
+
+impl GridTemplateAreas {
+ #[inline]
+ /// Get default value as `none`
+ pub fn none() -> GridTemplateAreas {
+ GridTemplateAreas::None
+ }
+}
+
+/// A specified value for the `z-index` property.
+pub type ZIndex = GenericZIndex<Integer>;
+
+/// A specified value for the `aspect-ratio` property.
+pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>;
+
+impl Parse for AspectRatio {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::generics::position::PreferredRatio;
+ use crate::values::specified::Ratio;
+
+ let location = input.current_source_location();
+ let mut auto = input.try_parse(|i| i.expect_ident_matching("auto"));
+ let ratio = input.try_parse(|i| Ratio::parse(context, i));
+ if auto.is_err() {
+ auto = input.try_parse(|i| i.expect_ident_matching("auto"));
+ }
+
+ if auto.is_err() && ratio.is_err() {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(AspectRatio {
+ auto: auto.is_ok(),
+ ratio: match ratio {
+ Ok(ratio) => PreferredRatio::Ratio(ratio),
+ Err(..) => PreferredRatio::None,
+ },
+ })
+ }
+}
+
+impl AspectRatio {
+ /// Returns Self by a valid ratio.
+ pub fn from_mapped_ratio(w: f32, h: f32) -> Self {
+ use crate::values::generics::position::PreferredRatio;
+ use crate::values::generics::ratio::Ratio;
+ AspectRatio {
+ auto: true,
+ ratio: PreferredRatio::Ratio(Ratio(
+ NonNegativeNumber::new(w),
+ NonNegativeNumber::new(h),
+ )),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/ratio.rs b/servo/components/style/values/specified/ratio.rs
new file mode 100644
index 0000000000..4cdddd452e
--- /dev/null
+++ b/servo/components/style/values/specified/ratio.rs
@@ -0,0 +1,32 @@
+/* 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 types for <ratio>.
+//!
+//! [ratio]: https://drafts.csswg.org/css-values/#ratios
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::ratio::Ratio as GenericRatio;
+use crate::values::specified::NonNegativeNumber;
+use crate::One;
+use cssparser::Parser;
+use style_traits::ParseError;
+
+/// A specified <ratio> value.
+pub type Ratio = GenericRatio<NonNegativeNumber>;
+
+impl Parse for Ratio {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let a = NonNegativeNumber::parse(context, input)?;
+ let b = match input.try_parse(|input| input.expect_delim('/')) {
+ Ok(()) => NonNegativeNumber::parse(context, input)?,
+ _ => One::one(),
+ };
+
+ Ok(GenericRatio(a, b))
+ }
+}
diff --git a/servo/components/style/values/specified/rect.rs b/servo/components/style/values/specified/rect.rs
new file mode 100644
index 0000000000..7955ecaa48
--- /dev/null
+++ b/servo/components/style/values/specified/rect.rs
@@ -0,0 +1,11 @@
+/* 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 types for CSS borders.
+
+use crate::values::generics::rect::Rect;
+use crate::values::specified::length::NonNegativeLengthOrNumber;
+
+/// A specified rectangle made of four `<length-or-number>` values.
+pub type NonNegativeLengthOrNumberRect = Rect<NonNegativeLengthOrNumber>;
diff --git a/servo/components/style/values/specified/resolution.rs b/servo/components/style/values/specified/resolution.rs
new file mode 100644
index 0000000000..74f100972a
--- /dev/null
+++ b/servo/components/style/values/specified/resolution.rs
@@ -0,0 +1,141 @@
+/* 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/. */
+
+//! Resolution values:
+//!
+//! https://drafts.csswg.org/css-values/#resolution
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::specified::CalcNode;
+use crate::values::CSSFloat;
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// A specified resolution.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
+pub struct Resolution {
+ value: CSSFloat,
+ unit: ResolutionUnit,
+ was_calc: bool,
+}
+
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+enum ResolutionUnit {
+ /// Dots per inch.
+ Dpi,
+ /// An alias unit for dots per pixel.
+ X,
+ /// Dots per pixel.
+ Dppx,
+ /// Dots per centimeter.
+ Dpcm,
+}
+
+impl ResolutionUnit {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Dpi => "dpi",
+ Self::X => "x",
+ Self::Dppx => "dppx",
+ Self::Dpcm => "dpcm",
+ }
+ }
+}
+
+impl Resolution {
+ /// Returns a resolution value from dppx units.
+ pub fn from_dppx(value: CSSFloat) -> Self {
+ Self {
+ value,
+ unit: ResolutionUnit::Dppx,
+ was_calc: false,
+ }
+ }
+
+ /// Returns a resolution value from dppx units.
+ pub fn from_x(value: CSSFloat) -> Self {
+ Self {
+ value,
+ unit: ResolutionUnit::X,
+ was_calc: false,
+ }
+ }
+
+ /// Returns a resolution value from dppx units.
+ pub fn from_dppx_calc(value: CSSFloat) -> Self {
+ Self {
+ value,
+ unit: ResolutionUnit::Dppx,
+ was_calc: true,
+ }
+ }
+
+ /// Convert this resolution value to dppx units.
+ pub fn dppx(&self) -> CSSFloat {
+ match self.unit {
+ ResolutionUnit::X | ResolutionUnit::Dppx => self.value,
+ _ => self.dpi() / 96.0,
+ }
+ }
+
+ /// Convert this resolution value to dpi units.
+ pub fn dpi(&self) -> CSSFloat {
+ match self.unit {
+ ResolutionUnit::Dpi => self.value,
+ ResolutionUnit::X | ResolutionUnit::Dppx => self.value * 96.0,
+ ResolutionUnit::Dpcm => self.value * 2.54,
+ }
+ }
+
+ /// Parse a resolution given a value and unit.
+ pub fn parse_dimension<'i, 't>(value: CSSFloat, unit: &str) -> Result<Self, ()> {
+ let unit = match_ignore_ascii_case! { &unit,
+ "dpi" => ResolutionUnit::Dpi,
+ "dppx" => ResolutionUnit::Dppx,
+ "dpcm" => ResolutionUnit::Dpcm,
+ "x" => ResolutionUnit::X,
+ _ => return Err(())
+ };
+ Ok(Self {
+ value,
+ unit,
+ was_calc: false,
+ })
+ }
+}
+
+impl ToCss for Resolution {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ crate::values::serialize_specified_dimension(
+ self.value,
+ self.unit.as_str(),
+ self.was_calc,
+ dest,
+ )
+ }
+}
+
+impl Parse for Resolution {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Dimension {
+ value, ref unit, ..
+ } if value >= 0. => Self::parse_dimension(value, unit)
+ .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse_resolution(context, input, function)
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/source_size_list.rs b/servo/components/style/values/specified/source_size_list.rs
new file mode 100644
index 0000000000..ac47461cc4
--- /dev/null
+++ b/servo/components/style/values/specified/source_size_list.rs
@@ -0,0 +1,136 @@
+/* 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/. */
+
+//! https://html.spec.whatwg.org/multipage/#source-size-list
+
+use crate::media_queries::Device;
+use crate::parser::{Parse, ParserContext};
+use crate::queries::{FeatureType, QueryCondition};
+use crate::values::computed::{self, ToComputedValue};
+use crate::values::specified::{Length, NoCalcLength, ViewportPercentageLength};
+use app_units::Au;
+use cssparser::{Delimiter, Parser, Token};
+use selectors::context::QuirksMode;
+use style_traits::ParseError;
+
+/// A value for a `<source-size>`:
+///
+/// https://html.spec.whatwg.org/multipage/#source-size
+#[derive(Debug)]
+pub struct SourceSize {
+ condition: QueryCondition,
+ value: Length,
+}
+
+impl Parse for SourceSize {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let condition = QueryCondition::parse(context, input, FeatureType::Media)?;
+ let value = Length::parse_non_negative(context, input)?;
+ Ok(Self { condition, value })
+ }
+}
+
+/// A value for a `<source-size-list>`:
+///
+/// https://html.spec.whatwg.org/multipage/#source-size-list
+#[derive(Debug)]
+pub struct SourceSizeList {
+ source_sizes: Vec<SourceSize>,
+ value: Option<Length>,
+}
+
+impl SourceSizeList {
+ /// Create an empty `SourceSizeList`, which can be used as a fall-back.
+ pub fn empty() -> Self {
+ Self {
+ source_sizes: vec![],
+ value: None,
+ }
+ }
+
+ /// Evaluate this <source-size-list> to get the final viewport length.
+ pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> Au {
+ computed::Context::for_media_query_evaluation(device, quirks_mode, |context| {
+ let matching_source_size = self.source_sizes.iter().find(|source_size| {
+ source_size
+ .condition
+ .matches(context)
+ .to_bool(/* unknown = */ false)
+ });
+
+ match matching_source_size {
+ Some(source_size) => source_size.value.to_computed_value(context),
+ None => match self.value {
+ Some(ref v) => v.to_computed_value(context),
+ None => Length::NoCalc(NoCalcLength::ViewportPercentage(
+ ViewportPercentageLength::Vw(100.),
+ ))
+ .to_computed_value(context),
+ },
+ }
+ })
+ .into()
+ }
+}
+
+enum SourceSizeOrLength {
+ SourceSize(SourceSize),
+ Length(Length),
+}
+
+impl Parse for SourceSizeOrLength {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(size) = input.try_parse(|input| SourceSize::parse(context, input)) {
+ return Ok(SourceSizeOrLength::SourceSize(size));
+ }
+
+ let length = Length::parse_non_negative(context, input)?;
+ Ok(SourceSizeOrLength::Length(length))
+ }
+}
+
+impl SourceSizeList {
+ /// NOTE(emilio): This doesn't match the grammar in the spec, see:
+ ///
+ /// https://html.spec.whatwg.org/multipage/#parsing-a-sizes-attribute
+ pub fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Self {
+ let mut source_sizes = vec![];
+
+ loop {
+ let result = input.parse_until_before(Delimiter::Comma, |input| {
+ SourceSizeOrLength::parse(context, input)
+ });
+
+ match result {
+ Ok(SourceSizeOrLength::Length(value)) => {
+ return Self {
+ source_sizes,
+ value: Some(value),
+ };
+ },
+ Ok(SourceSizeOrLength::SourceSize(source_size)) => {
+ source_sizes.push(source_size);
+ },
+ Err(..) => {},
+ }
+
+ match input.next() {
+ Ok(&Token::Comma) => {},
+ Err(..) => break,
+ _ => unreachable!(),
+ }
+ }
+
+ SourceSizeList {
+ source_sizes,
+ value: None,
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/svg.rs b/servo/components/style/values/specified/svg.rs
new file mode 100644
index 0000000000..8ab2dbb223
--- /dev/null
+++ b/servo/components/style/values/specified/svg.rs
@@ -0,0 +1,404 @@
+/* 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 types for SVG properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::svg as generic;
+use crate::values::specified::color::Color;
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::AllowQuirks;
+use crate::values::specified::LengthPercentage;
+use crate::values::specified::SVGPathData;
+use crate::values::specified::{NonNegativeLengthPercentage, Opacity};
+use crate::values::CustomIdent;
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::{CommaWithSpace, CssWriter, ParseError, Separator};
+use style_traits::{StyleParseErrorKind, ToCss};
+
+/// Specified SVG Paint value
+pub type SVGPaint = generic::GenericSVGPaint<Color, SpecifiedUrl>;
+
+/// <length> | <percentage> | <number> | context-value
+pub type SVGLength = generic::GenericSVGLength<LengthPercentage>;
+
+/// A non-negative version of SVGLength.
+pub type SVGWidth = generic::GenericSVGLength<NonNegativeLengthPercentage>;
+
+/// [ <length> | <percentage> | <number> ]# | context-value
+pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray<NonNegativeLengthPercentage>;
+
+/// Whether the `context-value` value is enabled.
+#[cfg(feature = "gecko")]
+pub fn is_context_value_enabled() -> bool {
+ static_prefs::pref!("gfx.font_rendering.opentype_svg.enabled")
+}
+
+/// Whether the `context-value` value is enabled.
+#[cfg(not(feature = "gecko"))]
+pub fn is_context_value_enabled() -> bool {
+ false
+}
+
+macro_rules! parse_svg_length {
+ ($ty:ty, $lp:ty) => {
+ impl Parse for $ty {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(lp) =
+ input.try_parse(|i| <$lp>::parse_quirky(context, i, AllowQuirks::Always))
+ {
+ return Ok(generic::SVGLength::LengthPercentage(lp));
+ }
+
+ try_match_ident_ignore_ascii_case! { input,
+ "context-value" if is_context_value_enabled() => {
+ Ok(generic::SVGLength::ContextValue)
+ },
+ }
+ }
+ }
+ };
+}
+
+parse_svg_length!(SVGLength, LengthPercentage);
+parse_svg_length!(SVGWidth, NonNegativeLengthPercentage);
+
+impl Parse for SVGStrokeDashArray {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(values) = input.try_parse(|i| {
+ CommaWithSpace::parse(i, |i| {
+ NonNegativeLengthPercentage::parse_quirky(context, i, AllowQuirks::Always)
+ })
+ }) {
+ return Ok(generic::SVGStrokeDashArray::Values(values.into()));
+ }
+
+ try_match_ident_ignore_ascii_case! { input,
+ "context-value" if is_context_value_enabled() => {
+ Ok(generic::SVGStrokeDashArray::ContextValue)
+ },
+ "none" => Ok(generic::SVGStrokeDashArray::Values(Default::default())),
+ }
+ }
+}
+
+/// <opacity-value> | context-fill-opacity | context-stroke-opacity
+pub type SVGOpacity = generic::SVGOpacity<Opacity>;
+
+/// The specified value for a single CSS paint-order property.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, ToCss)]
+pub enum PaintOrder {
+ /// `normal` variant
+ Normal = 0,
+ /// `fill` variant
+ Fill = 1,
+ /// `stroke` variant
+ Stroke = 2,
+ /// `markers` variant
+ Markers = 3,
+}
+
+/// Number of non-normal components
+pub const PAINT_ORDER_COUNT: u8 = 3;
+
+/// Number of bits for each component
+pub const PAINT_ORDER_SHIFT: u8 = 2;
+
+/// Mask with above bits set
+pub const PAINT_ORDER_MASK: u8 = 0b11;
+
+/// The specified value is tree `PaintOrder` values packed into the
+/// bitfields below, as a six-bit field, of 3 two-bit pairs
+///
+/// Each pair can be set to FILL, STROKE, or MARKERS
+/// Lowest significant bit pairs are highest priority.
+/// `normal` is the empty bitfield. The three pairs are
+/// never zero in any case other than `normal`.
+///
+/// Higher priority values, i.e. the values specified first,
+/// will be painted first (and may be covered by paintings of lower priority)
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct SVGPaintOrder(pub u8);
+
+impl SVGPaintOrder {
+ /// Get default `paint-order` with `0`
+ pub fn normal() -> Self {
+ SVGPaintOrder(0)
+ }
+
+ /// Get variant of `paint-order`
+ pub fn order_at(&self, pos: u8) -> PaintOrder {
+ match (self.0 >> pos * PAINT_ORDER_SHIFT) & PAINT_ORDER_MASK {
+ 0 => PaintOrder::Normal,
+ 1 => PaintOrder::Fill,
+ 2 => PaintOrder::Stroke,
+ 3 => PaintOrder::Markers,
+ _ => unreachable!("this cannot happen"),
+ }
+ }
+}
+
+impl Parse for SVGPaintOrder {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<SVGPaintOrder, ParseError<'i>> {
+ if let Ok(()) = input.try_parse(|i| i.expect_ident_matching("normal")) {
+ return Ok(SVGPaintOrder::normal());
+ }
+
+ let mut value = 0;
+ // bitfield representing what we've seen so far
+ // bit 1 is fill, bit 2 is stroke, bit 3 is markers
+ let mut seen = 0;
+ let mut pos = 0;
+
+ loop {
+ let result: Result<_, ParseError> = input.try_parse(|input| {
+ try_match_ident_ignore_ascii_case! { input,
+ "fill" => Ok(PaintOrder::Fill),
+ "stroke" => Ok(PaintOrder::Stroke),
+ "markers" => Ok(PaintOrder::Markers),
+ }
+ });
+
+ match result {
+ Ok(val) => {
+ if (seen & (1 << val as u8)) != 0 {
+ // don't parse the same ident twice
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ value |= (val as u8) << (pos * PAINT_ORDER_SHIFT);
+ seen |= 1 << (val as u8);
+ pos += 1;
+ },
+ Err(_) => break,
+ }
+ }
+
+ if value == 0 {
+ // Couldn't find any keyword
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ // fill in rest
+ for i in pos..PAINT_ORDER_COUNT {
+ for paint in 1..(PAINT_ORDER_COUNT + 1) {
+ // if not seen, set bit at position, mark as seen
+ if (seen & (1 << paint)) == 0 {
+ seen |= 1 << paint;
+ value |= paint << (i * PAINT_ORDER_SHIFT);
+ break;
+ }
+ }
+ }
+
+ Ok(SVGPaintOrder(value))
+ }
+}
+
+impl ToCss for SVGPaintOrder {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.0 == 0 {
+ return dest.write_str("normal");
+ }
+
+ let mut last_pos_to_serialize = 0;
+ for i in (1..PAINT_ORDER_COUNT).rev() {
+ let component = self.order_at(i);
+ let earlier_component = self.order_at(i - 1);
+ if component < earlier_component {
+ last_pos_to_serialize = i - 1;
+ break;
+ }
+ }
+
+ for pos in 0..last_pos_to_serialize + 1 {
+ if pos != 0 {
+ dest.write_char(' ')?
+ }
+ self.order_at(pos).to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// The context properties we understand.
+#[derive(
+ Clone,
+ Copy,
+ Eq,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct ContextPropertyBits(u8);
+bitflags! {
+ impl ContextPropertyBits: u8 {
+ /// `fill`
+ const FILL = 1 << 0;
+ /// `stroke`
+ const STROKE = 1 << 1;
+ /// `fill-opacity`
+ const FILL_OPACITY = 1 << 2;
+ /// `stroke-opacity`
+ const STROKE_OPACITY = 1 << 3;
+ }
+}
+
+/// Specified MozContextProperties value.
+/// Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-context-properties)
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct MozContextProperties {
+ #[css(iterable, if_empty = "none")]
+ #[ignore_malloc_size_of = "Arc"]
+ idents: crate::ArcSlice<CustomIdent>,
+ #[css(skip)]
+ bits: ContextPropertyBits,
+}
+
+impl Parse for MozContextProperties {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<MozContextProperties, ParseError<'i>> {
+ let mut values = vec![];
+ let mut bits = ContextPropertyBits::empty();
+ loop {
+ {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+
+ if ident.eq_ignore_ascii_case("none") && values.is_empty() {
+ return Ok(Self::default());
+ }
+
+ let ident = CustomIdent::from_ident(location, ident, &["all", "none", "auto"])?;
+
+ if ident.0 == atom!("fill") {
+ bits.insert(ContextPropertyBits::FILL);
+ } else if ident.0 == atom!("stroke") {
+ bits.insert(ContextPropertyBits::STROKE);
+ } else if ident.0 == atom!("fill-opacity") {
+ bits.insert(ContextPropertyBits::FILL_OPACITY);
+ } else if ident.0 == atom!("stroke-opacity") {
+ bits.insert(ContextPropertyBits::STROKE_OPACITY);
+ }
+
+ values.push(ident);
+ }
+
+ let location = input.current_source_location();
+ match input.next() {
+ Ok(&Token::Comma) => continue,
+ Err(..) => break,
+ Ok(other) => return Err(location.new_unexpected_token_error(other.clone())),
+ }
+ }
+
+ if values.is_empty() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(MozContextProperties {
+ idents: crate::ArcSlice::from_iter(values.into_iter()),
+ bits,
+ })
+ }
+}
+
+/// The svg d property type.
+///
+/// https://svgwg.org/svg2-draft/paths.html#TheDProperty
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum DProperty {
+ /// Path value for path(<string>) or just a <string>.
+ #[css(function)]
+ Path(SVGPathData),
+ /// None value.
+ #[animation(error)]
+ None,
+}
+
+impl DProperty {
+ /// return none.
+ #[inline]
+ pub fn none() -> Self {
+ DProperty::None
+ }
+}
+
+impl Parse for DProperty {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Parse none.
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(DProperty::none());
+ }
+
+ // Parse possible functions.
+ input.expect_function_matching("path")?;
+ let path_data = input.parse_nested_block(|i| Parse::parse(context, i))?;
+ Ok(DProperty::Path(path_data))
+ }
+}
diff --git a/servo/components/style/values/specified/svg_path.rs b/servo/components/style/values/specified/svg_path.rs
new file mode 100644
index 0000000000..1eb9866dd1
--- /dev/null
+++ b/servo/components/style/values/specified/svg_path.rs
@@ -0,0 +1,1029 @@
+/* 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 types for SVG Path.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::animated::{lists, Animate, Procedure, ToAnimatedZero};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::CSSFloat;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use std::iter::{Cloned, Peekable};
+use std::slice;
+use style_traits::values::SequenceWriter;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// Whether to allow empty string in the parser.
+#[derive(Clone, Debug, Eq, PartialEq)]
+#[allow(missing_docs)]
+pub enum AllowEmpty {
+ Yes,
+ No,
+}
+
+/// The SVG path data.
+///
+/// https://www.w3.org/TR/SVG11/paths.html#PathData
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct SVGPathData(
+ // TODO(emilio): Should probably measure this somehow only from the
+ // specified values.
+ #[ignore_malloc_size_of = "Arc"] pub crate::ArcSlice<PathCommand>,
+);
+
+impl SVGPathData {
+ /// Get the array of PathCommand.
+ #[inline]
+ pub fn commands(&self) -> &[PathCommand] {
+ &self.0
+ }
+
+ /// Create a normalized copy of this path by converting each relative
+ /// command to an absolute command.
+ pub fn normalize(&self) -> Self {
+ let mut state = PathTraversalState {
+ subpath_start: CoordPair::new(0.0, 0.0),
+ pos: CoordPair::new(0.0, 0.0),
+ };
+ let iter = self.0.iter().map(|seg| seg.normalize(&mut state));
+ SVGPathData(crate::ArcSlice::from_iter(iter))
+ }
+
+ // FIXME: Bug 1714238, we may drop this once we use the same data structure for both SVG and
+ // CSS.
+ /// Decode the svg path raw data from Gecko.
+ #[cfg(feature = "gecko")]
+ pub fn decode_from_f32_array(path: &[f32]) -> Result<Self, ()> {
+ use crate::gecko_bindings::structs::dom::SVGPathSeg_Binding::*;
+
+ let mut result: Vec<PathCommand> = Vec::new();
+ let mut i: usize = 0;
+ while i < path.len() {
+ // See EncodeType() and DecodeType() in SVGPathSegUtils.h.
+ // We are using reinterpret_cast<> to encode and decode between u32 and f32, so here we
+ // use to_bits() to decode the type.
+ let seg_type = path[i].to_bits() as u16;
+ i = i + 1;
+ match seg_type {
+ PATHSEG_CLOSEPATH => result.push(PathCommand::ClosePath),
+ PATHSEG_MOVETO_ABS | PATHSEG_MOVETO_REL => {
+ debug_assert!(i + 1 < path.len());
+ result.push(PathCommand::MoveTo {
+ point: CoordPair::new(path[i], path[i + 1]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_MOVETO_ABS),
+ });
+ i = i + 2;
+ },
+ PATHSEG_LINETO_ABS | PATHSEG_LINETO_REL => {
+ debug_assert!(i + 1 < path.len());
+ result.push(PathCommand::LineTo {
+ point: CoordPair::new(path[i], path[i + 1]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_ABS),
+ });
+ i = i + 2;
+ },
+ PATHSEG_CURVETO_CUBIC_ABS | PATHSEG_CURVETO_CUBIC_REL => {
+ debug_assert!(i + 5 < path.len());
+ result.push(PathCommand::CurveTo {
+ control1: CoordPair::new(path[i], path[i + 1]),
+ control2: CoordPair::new(path[i + 2], path[i + 3]),
+ point: CoordPair::new(path[i + 4], path[i + 5]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_CUBIC_ABS),
+ });
+ i = i + 6;
+ },
+ PATHSEG_CURVETO_QUADRATIC_ABS | PATHSEG_CURVETO_QUADRATIC_REL => {
+ debug_assert!(i + 3 < path.len());
+ result.push(PathCommand::QuadBezierCurveTo {
+ control1: CoordPair::new(path[i], path[i + 1]),
+ point: CoordPair::new(path[i + 2], path[i + 3]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_QUADRATIC_ABS),
+ });
+ i = i + 4;
+ },
+ PATHSEG_ARC_ABS | PATHSEG_ARC_REL => {
+ debug_assert!(i + 6 < path.len());
+ result.push(PathCommand::EllipticalArc {
+ rx: path[i],
+ ry: path[i + 1],
+ angle: path[i + 2],
+ large_arc_flag: ArcFlag(path[i + 3] != 0.0f32),
+ sweep_flag: ArcFlag(path[i + 4] != 0.0f32),
+ point: CoordPair::new(path[i + 5], path[i + 6]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_ARC_ABS),
+ });
+ i = i + 7;
+ },
+ PATHSEG_LINETO_HORIZONTAL_ABS | PATHSEG_LINETO_HORIZONTAL_REL => {
+ debug_assert!(i < path.len());
+ result.push(PathCommand::HorizontalLineTo {
+ x: path[i],
+ absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_HORIZONTAL_ABS),
+ });
+ i = i + 1;
+ },
+ PATHSEG_LINETO_VERTICAL_ABS | PATHSEG_LINETO_VERTICAL_REL => {
+ debug_assert!(i < path.len());
+ result.push(PathCommand::VerticalLineTo {
+ y: path[i],
+ absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_VERTICAL_ABS),
+ });
+ i = i + 1;
+ },
+ PATHSEG_CURVETO_CUBIC_SMOOTH_ABS | PATHSEG_CURVETO_CUBIC_SMOOTH_REL => {
+ debug_assert!(i + 3 < path.len());
+ result.push(PathCommand::SmoothCurveTo {
+ control2: CoordPair::new(path[i], path[i + 1]),
+ point: CoordPair::new(path[i + 2], path[i + 3]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_CUBIC_SMOOTH_ABS),
+ });
+ i = i + 4;
+ },
+ PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS | PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL => {
+ debug_assert!(i + 1 < path.len());
+ result.push(PathCommand::SmoothQuadBezierCurveTo {
+ point: CoordPair::new(path[i], path[i + 1]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS),
+ });
+ i = i + 2;
+ },
+ PATHSEG_UNKNOWN | _ => return Err(()),
+ }
+ }
+
+ Ok(SVGPathData(crate::ArcSlice::from_iter(result.into_iter())))
+ }
+
+ /// Parse this SVG path string with the argument that indicates whether we should allow the
+ /// empty string.
+ // We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make
+ // the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.)
+ // e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident
+ // is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable
+ // str::Char iterator to check each character.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ allow_empty: AllowEmpty,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let path_string = input.expect_string()?.as_ref();
+
+ // Parse the svg path string as multiple sub-paths.
+ let mut path_parser = PathParser::new(path_string);
+ while skip_wsp(&mut path_parser.chars) {
+ if path_parser.parse_subpath().is_err() {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ // The css-shapes-1 says a path data string that does conform but defines an empty path is
+ // invalid and causes the entire path() to be invalid, so we use the argement to decide
+ // whether we should allow the empty string.
+ // https://drafts.csswg.org/css-shapes-1/#typedef-basic-shape
+ if matches!(allow_empty, AllowEmpty::No) && path_parser.path.is_empty() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(SVGPathData(crate::ArcSlice::from_iter(
+ path_parser.path.into_iter(),
+ )))
+ }
+}
+
+impl ToCss for SVGPathData {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_char('"')?;
+ {
+ let mut writer = SequenceWriter::new(dest, " ");
+ for command in self.commands() {
+ writer.item(command)?;
+ }
+ }
+ dest.write_char('"')
+ }
+}
+
+impl Parse for SVGPathData {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Note that the EBNF allows the path data string in the d property to be empty, so we
+ // don't reject empty SVG path data.
+ // https://svgwg.org/svg2-draft/single-page.html#paths-PathDataBNF
+ SVGPathData::parse(input, AllowEmpty::Yes)
+ }
+}
+
+impl Animate for SVGPathData {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.0.len() != other.0.len() {
+ return Err(());
+ }
+
+ // FIXME(emilio): This allocates three copies of the path, that's not
+ // great! Specially, once we're normalized once, we don't need to
+ // re-normalize again.
+ let left = self.normalize();
+ let right = other.normalize();
+
+ let items: Vec<_> = lists::by_computed_value::animate(&left.0, &right.0, procedure)?;
+ Ok(SVGPathData(crate::ArcSlice::from_iter(items.into_iter())))
+ }
+}
+
+impl ComputeSquaredDistance for SVGPathData {
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ if self.0.len() != other.0.len() {
+ return Err(());
+ }
+ let left = self.normalize();
+ let right = other.normalize();
+ lists::by_computed_value::squared_distance(&left.0, &right.0)
+ }
+}
+
+/// The SVG path command.
+/// The fields of these commands are self-explanatory, so we skip the documents.
+/// Note: the index of the control points, e.g. control1, control2, are mapping to the control
+/// points of the Bézier curve in the spec.
+///
+/// https://www.w3.org/TR/SVG11/paths.html#PathData
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(C, u8)]
+pub enum PathCommand {
+ /// The unknown type.
+ /// https://www.w3.org/TR/SVG/paths.html#__svg__SVGPathSeg__PATHSEG_UNKNOWN
+ Unknown,
+ /// The "moveto" command.
+ MoveTo {
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The "lineto" command.
+ LineTo {
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The horizontal "lineto" command.
+ HorizontalLineTo { x: CSSFloat, absolute: IsAbsolute },
+ /// The vertical "lineto" command.
+ VerticalLineTo { y: CSSFloat, absolute: IsAbsolute },
+ /// The cubic Bézier curve command.
+ CurveTo {
+ control1: CoordPair,
+ control2: CoordPair,
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The smooth curve command.
+ SmoothCurveTo {
+ control2: CoordPair,
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The quadratic Bézier curve command.
+ QuadBezierCurveTo {
+ control1: CoordPair,
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The smooth quadratic Bézier curve command.
+ SmoothQuadBezierCurveTo {
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The elliptical arc curve command.
+ EllipticalArc {
+ rx: CSSFloat,
+ ry: CSSFloat,
+ angle: CSSFloat,
+ large_arc_flag: ArcFlag,
+ sweep_flag: ArcFlag,
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The "closepath" command.
+ ClosePath,
+}
+
+/// For internal SVGPath normalization.
+#[allow(missing_docs)]
+struct PathTraversalState {
+ subpath_start: CoordPair,
+ pos: CoordPair,
+}
+
+impl PathCommand {
+ /// Create a normalized copy of this PathCommand. Absolute commands will be copied as-is while
+ /// for relative commands an equivalent absolute command will be returned.
+ ///
+ /// See discussion: https://github.com/w3c/svgwg/issues/321
+ fn normalize(&self, state: &mut PathTraversalState) -> Self {
+ use self::PathCommand::*;
+ match *self {
+ Unknown => Unknown,
+ ClosePath => {
+ state.pos = state.subpath_start;
+ ClosePath
+ },
+ MoveTo {
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ point += state.pos;
+ }
+ state.pos = point;
+ state.subpath_start = point;
+ MoveTo {
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ LineTo {
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ point += state.pos;
+ }
+ state.pos = point;
+ LineTo {
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ HorizontalLineTo { mut x, absolute } => {
+ if !absolute.is_yes() {
+ x += state.pos.x;
+ }
+ state.pos.x = x;
+ HorizontalLineTo {
+ x,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ VerticalLineTo { mut y, absolute } => {
+ if !absolute.is_yes() {
+ y += state.pos.y;
+ }
+ state.pos.y = y;
+ VerticalLineTo {
+ y,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ CurveTo {
+ mut control1,
+ mut control2,
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ control1 += state.pos;
+ control2 += state.pos;
+ point += state.pos;
+ }
+ state.pos = point;
+ CurveTo {
+ control1,
+ control2,
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ SmoothCurveTo {
+ mut control2,
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ control2 += state.pos;
+ point += state.pos;
+ }
+ state.pos = point;
+ SmoothCurveTo {
+ control2,
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ QuadBezierCurveTo {
+ mut control1,
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ control1 += state.pos;
+ point += state.pos;
+ }
+ state.pos = point;
+ QuadBezierCurveTo {
+ control1,
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ SmoothQuadBezierCurveTo {
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ point += state.pos;
+ }
+ state.pos = point;
+ SmoothQuadBezierCurveTo {
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ EllipticalArc {
+ rx,
+ ry,
+ angle,
+ large_arc_flag,
+ sweep_flag,
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ point += state.pos;
+ }
+ state.pos = point;
+ EllipticalArc {
+ rx,
+ ry,
+ angle,
+ large_arc_flag,
+ sweep_flag,
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ }
+ }
+}
+
+impl ToCss for PathCommand {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use self::PathCommand::*;
+ match *self {
+ Unknown => dest.write_char('X'),
+ ClosePath => dest.write_char('Z'),
+ MoveTo { point, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'M' } else { 'm' })?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ LineTo { point, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'L' } else { 'l' })?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ CurveTo {
+ control1,
+ control2,
+ point,
+ absolute,
+ } => {
+ dest.write_char(if absolute.is_yes() { 'C' } else { 'c' })?;
+ dest.write_char(' ')?;
+ control1.to_css(dest)?;
+ dest.write_char(' ')?;
+ control2.to_css(dest)?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ QuadBezierCurveTo {
+ control1,
+ point,
+ absolute,
+ } => {
+ dest.write_char(if absolute.is_yes() { 'Q' } else { 'q' })?;
+ dest.write_char(' ')?;
+ control1.to_css(dest)?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ EllipticalArc {
+ rx,
+ ry,
+ angle,
+ large_arc_flag,
+ sweep_flag,
+ point,
+ absolute,
+ } => {
+ dest.write_char(if absolute.is_yes() { 'A' } else { 'a' })?;
+ dest.write_char(' ')?;
+ rx.to_css(dest)?;
+ dest.write_char(' ')?;
+ ry.to_css(dest)?;
+ dest.write_char(' ')?;
+ angle.to_css(dest)?;
+ dest.write_char(' ')?;
+ large_arc_flag.to_css(dest)?;
+ dest.write_char(' ')?;
+ sweep_flag.to_css(dest)?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ HorizontalLineTo { x, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'H' } else { 'h' })?;
+ dest.write_char(' ')?;
+ x.to_css(dest)
+ },
+ VerticalLineTo { y, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'V' } else { 'v' })?;
+ dest.write_char(' ')?;
+ y.to_css(dest)
+ },
+ SmoothCurveTo {
+ control2,
+ point,
+ absolute,
+ } => {
+ dest.write_char(if absolute.is_yes() { 'S' } else { 's' })?;
+ dest.write_char(' ')?;
+ control2.to_css(dest)?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ SmoothQuadBezierCurveTo { point, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'T' } else { 't' })?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ }
+ }
+}
+
+/// The path command absolute type.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum IsAbsolute {
+ Yes,
+ No,
+}
+
+impl IsAbsolute {
+ /// Return true if this is IsAbsolute::Yes.
+ #[inline]
+ pub fn is_yes(&self) -> bool {
+ *self == IsAbsolute::Yes
+ }
+
+ /// Return Yes if value is true. Otherwise, return No.
+ #[inline]
+ fn new(value: bool) -> Self {
+ if value {
+ IsAbsolute::Yes
+ } else {
+ IsAbsolute::No
+ }
+ }
+}
+
+/// The path coord type.
+#[allow(missing_docs)]
+#[derive(
+ AddAssign,
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct CoordPair {
+ x: CSSFloat,
+ y: CSSFloat,
+}
+
+impl CoordPair {
+ /// Create a CoordPair.
+ #[inline]
+ pub fn new(x: CSSFloat, y: CSSFloat) -> Self {
+ CoordPair { x, y }
+ }
+}
+
+/// The EllipticalArc flag type.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct ArcFlag(bool);
+
+impl ToCss for ArcFlag {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ (self.0 as i32).to_css(dest)
+ }
+}
+
+impl Animate for ArcFlag {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ (self.0 as i32)
+ .animate(&(other.0 as i32), procedure)
+ .map(|v| ArcFlag(v > 0))
+ }
+}
+
+impl ComputeSquaredDistance for ArcFlag {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ (self.0 as i32).compute_squared_distance(&(other.0 as i32))
+ }
+}
+
+impl ToAnimatedZero for ArcFlag {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ // The 2 ArcFlags in EllipticalArc determine which one of the 4 different arcs will be
+ // used. (i.e. From 4 combinations). In other words, if we change the flag, we get a
+ // different arc. Therefore, we return *self.
+ // https://svgwg.org/svg2-draft/paths.html#PathDataEllipticalArcCommands
+ Ok(*self)
+ }
+}
+
+/// SVG Path parser.
+struct PathParser<'a> {
+ chars: Peekable<Cloned<slice::Iter<'a, u8>>>,
+ path: Vec<PathCommand>,
+}
+
+macro_rules! parse_arguments {
+ (
+ $parser:ident,
+ $abs:ident,
+ $enum:ident,
+ [ $para:ident => $func:ident $(, $other_para:ident => $other_func:ident)* ]
+ ) => {
+ {
+ loop {
+ let $para = $func(&mut $parser.chars)?;
+ $(
+ skip_comma_wsp(&mut $parser.chars);
+ let $other_para = $other_func(&mut $parser.chars)?;
+ )*
+ $parser.path.push(PathCommand::$enum { $para $(, $other_para)*, $abs });
+
+ // End of string or the next character is a possible new command.
+ if !skip_wsp(&mut $parser.chars) ||
+ $parser.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) {
+ break;
+ }
+ skip_comma_wsp(&mut $parser.chars);
+ }
+ Ok(())
+ }
+ }
+}
+
+impl<'a> PathParser<'a> {
+ /// Return a PathParser.
+ #[inline]
+ fn new(string: &'a str) -> Self {
+ PathParser {
+ chars: string.as_bytes().iter().cloned().peekable(),
+ path: Vec::new(),
+ }
+ }
+
+ /// Parse a sub-path.
+ fn parse_subpath(&mut self) -> Result<(), ()> {
+ // Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path
+ // (i.e. not a valid moveto-drawto-command-group).
+ self.parse_moveto()?;
+
+ // Handle other commands.
+ loop {
+ skip_wsp(&mut self.chars);
+ if self.chars.peek().map_or(true, |&m| m == b'M' || m == b'm') {
+ break;
+ }
+
+ let command = self.chars.next().unwrap();
+ let abs = if command.is_ascii_uppercase() {
+ IsAbsolute::Yes
+ } else {
+ IsAbsolute::No
+ };
+
+ skip_wsp(&mut self.chars);
+ match command {
+ b'Z' | b'z' => self.parse_closepath(),
+ b'L' | b'l' => self.parse_lineto(abs),
+ b'H' | b'h' => self.parse_h_lineto(abs),
+ b'V' | b'v' => self.parse_v_lineto(abs),
+ b'C' | b'c' => self.parse_curveto(abs),
+ b'S' | b's' => self.parse_smooth_curveto(abs),
+ b'Q' | b'q' => self.parse_quadratic_bezier_curveto(abs),
+ b'T' | b't' => self.parse_smooth_quadratic_bezier_curveto(abs),
+ b'A' | b'a' => self.parse_elliptical_arc(abs),
+ _ => return Err(()),
+ }?;
+ }
+ Ok(())
+ }
+
+ /// Parse "moveto" command.
+ fn parse_moveto(&mut self) -> Result<(), ()> {
+ let command = match self.chars.next() {
+ Some(c) if c == b'M' || c == b'm' => c,
+ _ => return Err(()),
+ };
+
+ skip_wsp(&mut self.chars);
+ let point = parse_coord(&mut self.chars)?;
+ let absolute = if command == b'M' {
+ IsAbsolute::Yes
+ } else {
+ IsAbsolute::No
+ };
+ self.path.push(PathCommand::MoveTo { point, absolute });
+
+ // End of string or the next character is a possible new command.
+ if !skip_wsp(&mut self.chars) || self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic())
+ {
+ return Ok(());
+ }
+ skip_comma_wsp(&mut self.chars);
+
+ // If a moveto is followed by multiple pairs of coordinates, the subsequent
+ // pairs are treated as implicit lineto commands.
+ self.parse_lineto(absolute)
+ }
+
+ /// Parse "closepath" command.
+ fn parse_closepath(&mut self) -> Result<(), ()> {
+ self.path.push(PathCommand::ClosePath);
+ Ok(())
+ }
+
+ /// Parse "lineto" command.
+ fn parse_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, LineTo, [ point => parse_coord ])
+ }
+
+ /// Parse horizontal "lineto" command.
+ fn parse_h_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, HorizontalLineTo, [ x => parse_number ])
+ }
+
+ /// Parse vertical "lineto" command.
+ fn parse_v_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, VerticalLineTo, [ y => parse_number ])
+ }
+
+ /// Parse cubic Bézier curve command.
+ fn parse_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, CurveTo, [
+ control1 => parse_coord, control2 => parse_coord, point => parse_coord
+ ])
+ }
+
+ /// Parse smooth "curveto" command.
+ fn parse_smooth_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, SmoothCurveTo, [
+ control2 => parse_coord, point => parse_coord
+ ])
+ }
+
+ /// Parse quadratic Bézier curve command.
+ fn parse_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, QuadBezierCurveTo, [
+ control1 => parse_coord, point => parse_coord
+ ])
+ }
+
+ /// Parse smooth quadratic Bézier curveto command.
+ fn parse_smooth_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, SmoothQuadBezierCurveTo, [ point => parse_coord ])
+ }
+
+ /// Parse elliptical arc curve command.
+ fn parse_elliptical_arc(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ // Parse a flag whose value is '0' or '1'; otherwise, return Err(()).
+ let parse_flag = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() {
+ Some(c) if c == b'0' || c == b'1' => Ok(ArcFlag(c == b'1')),
+ _ => Err(()),
+ };
+ parse_arguments!(self, absolute, EllipticalArc, [
+ rx => parse_number,
+ ry => parse_number,
+ angle => parse_number,
+ large_arc_flag => parse_flag,
+ sweep_flag => parse_flag,
+ point => parse_coord
+ ])
+ }
+}
+
+/// Parse a pair of numbers into CoordPair.
+fn parse_coord(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CoordPair, ()> {
+ let x = parse_number(iter)?;
+ skip_comma_wsp(iter);
+ let y = parse_number(iter)?;
+ Ok(CoordPair::new(x, y))
+}
+
+/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed
+/// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating
+/// point number. In other words, the logic here is similar with that of
+/// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the
+/// input is a Peekable and we only accept an integer of a floating point number.
+///
+/// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF
+fn parse_number(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CSSFloat, ()> {
+ // 1. Check optional sign.
+ let sign = if iter
+ .peek()
+ .map_or(false, |&sign| sign == b'+' || sign == b'-')
+ {
+ if iter.next().unwrap() == b'-' {
+ -1.
+ } else {
+ 1.
+ }
+ } else {
+ 1.
+ };
+
+ // 2. Check integer part.
+ let mut integral_part: f64 = 0.;
+ let got_dot = if !iter.peek().map_or(false, |&n| n == b'.') {
+ // If the first digit in integer part is neither a dot nor a digit, this is not a number.
+ if iter.peek().map_or(true, |n| !n.is_ascii_digit()) {
+ return Err(());
+ }
+
+ while iter.peek().map_or(false, |n| n.is_ascii_digit()) {
+ integral_part = integral_part * 10. + (iter.next().unwrap() - b'0') as f64;
+ }
+
+ iter.peek().map_or(false, |&n| n == b'.')
+ } else {
+ true
+ };
+
+ // 3. Check fractional part.
+ let mut fractional_part: f64 = 0.;
+ if got_dot {
+ // Consume '.'.
+ iter.next();
+ // If the first digit in fractional part is not a digit, this is not a number.
+ if iter.peek().map_or(true, |n| !n.is_ascii_digit()) {
+ return Err(());
+ }
+
+ let mut factor = 0.1;
+ while iter.peek().map_or(false, |n| n.is_ascii_digit()) {
+ fractional_part += (iter.next().unwrap() - b'0') as f64 * factor;
+ factor *= 0.1;
+ }
+ }
+
+ let mut value = sign * (integral_part + fractional_part);
+
+ // 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to
+ // treat the numbers after 'E' or 'e' are in the exponential part.
+ if iter.peek().map_or(false, |&exp| exp == b'E' || exp == b'e') {
+ // Consume 'E' or 'e'.
+ iter.next();
+ let exp_sign = if iter
+ .peek()
+ .map_or(false, |&sign| sign == b'+' || sign == b'-')
+ {
+ if iter.next().unwrap() == b'-' {
+ -1.
+ } else {
+ 1.
+ }
+ } else {
+ 1.
+ };
+
+ let mut exp: f64 = 0.;
+ while iter.peek().map_or(false, |n| n.is_ascii_digit()) {
+ exp = exp * 10. + (iter.next().unwrap() - b'0') as f64;
+ }
+
+ value *= f64::powf(10., exp * exp_sign);
+ }
+
+ if value.is_finite() {
+ Ok(value.min(f32::MAX as f64).max(f32::MIN as f64) as CSSFloat)
+ } else {
+ Err(())
+ }
+}
+
+/// Skip all svg whitespaces, and return true if |iter| hasn't finished.
+#[inline]
+fn skip_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool {
+ // Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}.
+ // However, SVG 2 has one extra whitespace: \u{C}.
+ // Therefore, we follow the newest spec for the definition of whitespace,
+ // i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}.
+ while iter.peek().map_or(false, |c| c.is_ascii_whitespace()) {
+ iter.next();
+ }
+ iter.peek().is_some()
+}
+
+/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished.
+#[inline]
+fn skip_comma_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool {
+ if !skip_wsp(iter) {
+ return false;
+ }
+
+ if *iter.peek().unwrap() != b',' {
+ return true;
+ }
+ iter.next();
+
+ skip_wsp(iter)
+}
diff --git a/servo/components/style/values/specified/table.rs b/servo/components/style/values/specified/table.rs
new file mode 100644
index 0000000000..88f917ac78
--- /dev/null
+++ b/servo/components/style/values/specified/table.rs
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS values related to tables.
+
+/// Specified values for the `caption-side` property.
+///
+/// Note that despite having "physical" names, these are actually interpreted
+/// according to the table's writing-mode: Top and Bottom are treated as
+/// block-start and -end respectively.
+///
+/// https://drafts.csswg.org/css-tables/#propdef-caption-side
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ MallocSizeOf,
+ Ord,
+ Parse,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum CaptionSide {
+ Top,
+ Bottom,
+}
diff --git a/servo/components/style/values/specified/text.rs b/servo/components/style/values/specified/text.rs
new file mode 100644
index 0000000000..0e70bd26ac
--- /dev/null
+++ b/servo/components/style/values/specified/text.rs
@@ -0,0 +1,1193 @@
+/* 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 types for text properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode;
+use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle;
+use crate::values::computed::text::TextOverflow as ComputedTextOverflow;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::text::InitialLetter as GenericInitialLetter;
+use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing};
+use crate::values::specified::length::{Length, LengthPercentage};
+use crate::values::specified::{AllowQuirks, Integer, Number};
+use cssparser::{Parser, Token};
+use icu_segmenter::GraphemeClusterSegmenter;
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Write};
+use style_traits::values::SequenceWriter;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+use style_traits::{KeywordsCollectFn, SpecifiedValueInfo};
+
+/// A specified type for the `initial-letter` property.
+pub type InitialLetter = GenericInitialLetter<Number, Integer>;
+
+/// A specified value for the `letter-spacing` property.
+pub type LetterSpacing = Spacing<Length>;
+
+/// A specified value for the `word-spacing` property.
+pub type WordSpacing = Spacing<LengthPercentage>;
+
+/// A value for the `hyphenate-character` property.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum HyphenateCharacter {
+ /// `auto`
+ Auto,
+ /// `<string>`
+ String(crate::OwnedStr),
+}
+
+impl Parse for InitialLetter {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(GenericInitialLetter::Normal);
+ }
+ let size = Number::parse_at_least_one(context, input)?;
+ let sink = input
+ .try_parse(|i| Integer::parse_positive(context, i))
+ .ok();
+ Ok(GenericInitialLetter::Specified(size, sink))
+ }
+}
+
+impl Parse for LetterSpacing {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Spacing::parse_with(context, input, |c, i| {
+ Length::parse_quirky(c, i, AllowQuirks::Yes)
+ })
+ }
+}
+
+impl Parse for WordSpacing {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Spacing::parse_with(context, input, |c, i| {
+ LengthPercentage::parse_quirky(c, i, AllowQuirks::Yes)
+ })
+ }
+}
+
+/// A generic value for the `text-overflow` property.
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum TextOverflowSide {
+ /// Clip inline content.
+ Clip,
+ /// Render ellipsis to represent clipped inline content.
+ Ellipsis,
+ /// Render a given string to represent clipped inline content.
+ String(crate::OwnedStr),
+}
+
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+/// text-overflow. Specifies rendering when inline content overflows its line box edge.
+pub struct TextOverflow {
+ /// First value. Applies to end line box edge if no second is supplied; line-left edge otherwise.
+ pub first: TextOverflowSide,
+ /// Second value. Applies to the line-right edge if supplied.
+ pub second: Option<TextOverflowSide>,
+}
+
+impl Parse for TextOverflow {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<TextOverflow, ParseError<'i>> {
+ let first = TextOverflowSide::parse(context, input)?;
+ let second = input
+ .try_parse(|input| TextOverflowSide::parse(context, input))
+ .ok();
+ Ok(TextOverflow { first, second })
+ }
+}
+
+impl ToComputedValue for TextOverflow {
+ type ComputedValue = ComputedTextOverflow;
+
+ #[inline]
+ fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
+ if let Some(ref second) = self.second {
+ Self::ComputedValue {
+ first: self.first.clone(),
+ second: second.clone(),
+ sides_are_logical: false,
+ }
+ } else {
+ Self::ComputedValue {
+ first: TextOverflowSide::Clip,
+ second: self.first.clone(),
+ sides_are_logical: true,
+ }
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ if computed.sides_are_logical {
+ assert_eq!(computed.first, TextOverflowSide::Clip);
+ TextOverflow {
+ first: computed.second.clone(),
+ second: None,
+ }
+ } else {
+ TextOverflow {
+ first: computed.first.clone(),
+ second: Some(computed.second.clone()),
+ }
+ }
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ Serialize,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(bitflags(single = "none", mixed = "underline,overline,line-through,blink"))]
+#[repr(C)]
+/// Specified keyword values for the text-decoration-line property.
+pub struct TextDecorationLine(u8);
+bitflags! {
+ impl TextDecorationLine: u8 {
+ /// No text decoration line is specified.
+ const NONE = 0;
+ /// underline
+ const UNDERLINE = 1 << 0;
+ /// overline
+ const OVERLINE = 1 << 1;
+ /// line-through
+ const LINE_THROUGH = 1 << 2;
+ /// blink
+ const BLINK = 1 << 3;
+ /// Only set by presentation attributes
+ ///
+ /// Setting this will mean that text-decorations use the color
+ /// specified by `color` in quirks mode.
+ ///
+ /// For example, this gives <a href=foo><font color="red">text</font></a>
+ /// a red text decoration
+ #[cfg(feature = "gecko")]
+ const COLOR_OVERRIDE = 0x10;
+ }
+}
+
+impl Default for TextDecorationLine {
+ fn default() -> Self {
+ TextDecorationLine::NONE
+ }
+}
+
+impl TextDecorationLine {
+ #[inline]
+ /// Returns the initial value of text-decoration-line
+ pub fn none() -> Self {
+ TextDecorationLine::NONE
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Specified value of the text-transform property, stored in two parts:
+/// the case-related transforms (mutually exclusive, only one may be in effect), and others (non-exclusive).
+pub struct TextTransform {
+ /// Case transform, if any.
+ pub case_: TextTransformCase,
+ /// Non-case transforms.
+ pub other_: TextTransformOther,
+}
+
+impl TextTransform {
+ #[inline]
+ /// Returns the initial value of text-transform
+ pub fn none() -> Self {
+ TextTransform {
+ case_: TextTransformCase::None,
+ other_: TextTransformOther::empty(),
+ }
+ }
+ #[inline]
+ /// Returns whether the value is 'none'
+ pub fn is_none(&self) -> bool {
+ self.case_ == TextTransformCase::None && self.other_.is_empty()
+ }
+}
+
+// TODO: This can be simplified by deriving it.
+impl Parse for TextTransform {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut result = TextTransform::none();
+
+ // Case keywords are mutually exclusive; other transforms may co-occur.
+ loop {
+ let location = input.current_source_location();
+ let ident = match input.next() {
+ Ok(&Token::Ident(ref ident)) => ident,
+ Ok(other) => return Err(location.new_unexpected_token_error(other.clone())),
+ Err(..) => break,
+ };
+
+ match_ignore_ascii_case! { ident,
+ "none" if result.is_none() => {
+ return Ok(result);
+ },
+ "uppercase" if result.case_ == TextTransformCase::None => {
+ result.case_ = TextTransformCase::Uppercase
+ },
+ "lowercase" if result.case_ == TextTransformCase::None => {
+ result.case_ = TextTransformCase::Lowercase
+ },
+ "capitalize" if result.case_ == TextTransformCase::None => {
+ result.case_ = TextTransformCase::Capitalize
+ },
+ "math-auto" if result.case_ == TextTransformCase::None &&
+ result.other_.is_empty() => {
+ result.case_ = TextTransformCase::MathAuto;
+ return Ok(result);
+ },
+ "full-width" if !result.other_.intersects(TextTransformOther::FULL_WIDTH) => {
+ result.other_.insert(TextTransformOther::FULL_WIDTH)
+ },
+ "full-size-kana" if !result.other_.intersects(TextTransformOther::FULL_SIZE_KANA) => {
+ result.other_.insert(TextTransformOther::FULL_SIZE_KANA)
+ },
+ _ => return Err(location.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+ )),
+ }
+ }
+
+ if result.is_none() {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(result)
+ }
+ }
+}
+
+impl ToCss for TextTransform {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_none() {
+ return dest.write_str("none");
+ }
+
+ if self.case_ != TextTransformCase::None {
+ self.case_.to_css(dest)?;
+ if !self.other_.is_empty() {
+ dest.write_char(' ')?;
+ }
+ }
+
+ self.other_.to_css(dest)
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Specified keyword values for case transforms in the text-transform property. (These are exclusive.)
+pub enum TextTransformCase {
+ /// No case transform.
+ None,
+ /// All uppercase.
+ Uppercase,
+ /// All lowercase.
+ Lowercase,
+ /// Capitalize each word.
+ Capitalize,
+ /// Automatic italicization of math variables.
+ MathAuto,
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ Serialize,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(bitflags(mixed = "full-width,full-size-kana"))]
+#[repr(C)]
+/// Specified keyword values for non-case transforms in the text-transform property. (Non-exclusive.)
+pub struct TextTransformOther(u8);
+bitflags! {
+ impl TextTransformOther: u8 {
+ /// full-width
+ const FULL_WIDTH = 1 << 0;
+ /// full-size-kana
+ const FULL_SIZE_KANA = 1 << 1;
+ }
+}
+
+/// Specified and computed value of text-align-last.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum TextAlignLast {
+ Auto,
+ Start,
+ End,
+ Left,
+ Right,
+ Center,
+ Justify,
+}
+
+/// Specified value of text-align keyword value.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum TextAlignKeyword {
+ Start,
+ Left,
+ Right,
+ Center,
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ Justify,
+ #[css(skip)]
+ #[cfg(feature = "gecko")]
+ Char,
+ End,
+ #[cfg(feature = "gecko")]
+ MozCenter,
+ #[cfg(feature = "gecko")]
+ MozLeft,
+ #[cfg(feature = "gecko")]
+ MozRight,
+ #[cfg(feature = "servo-layout-2013")]
+ ServoCenter,
+ #[cfg(feature = "servo-layout-2013")]
+ ServoLeft,
+ #[cfg(feature = "servo-layout-2013")]
+ ServoRight,
+}
+
+/// Specified value of text-align property.
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub enum TextAlign {
+ /// Keyword value of text-align property.
+ Keyword(TextAlignKeyword),
+ /// `match-parent` value of text-align property. It has a different handling
+ /// unlike other keywords.
+ #[cfg(feature = "gecko")]
+ MatchParent,
+ /// This is how we implement the following HTML behavior from
+ /// https://html.spec.whatwg.org/#tables-2:
+ ///
+ /// User agents are expected to have a rule in their user agent style sheet
+ /// that matches th elements that have a parent node whose computed value
+ /// for the 'text-align' property is its initial value, whose declaration
+ /// block consists of just a single declaration that sets the 'text-align'
+ /// property to the value 'center'.
+ ///
+ /// Since selectors can't depend on the ancestor styles, we implement it with a
+ /// magic value that computes to the right thing. Since this is an
+ /// implementation detail, it shouldn't be exposed to web content.
+ #[cfg(feature = "gecko")]
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozCenterOrInherit,
+}
+
+impl ToComputedValue for TextAlign {
+ type ComputedValue = TextAlignKeyword;
+
+ #[inline]
+ fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
+ match *self {
+ TextAlign::Keyword(key) => key,
+ #[cfg(feature = "gecko")]
+ TextAlign::MatchParent => {
+ // on the root <html> element we should still respect the dir
+ // but the parent dir of that element is LTR even if it's <html dir=rtl>
+ // and will only be RTL if certain prefs have been set.
+ // In that case, the default behavior here will set it to left,
+ // but we want to set it to right -- instead set it to the default (`start`),
+ // which will do the right thing in this case (but not the general case)
+ if _context.builder.is_root_element {
+ return TextAlignKeyword::Start;
+ }
+ let parent = _context
+ .builder
+ .get_parent_inherited_text()
+ .clone_text_align();
+ let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr();
+ match (parent, ltr) {
+ (TextAlignKeyword::Start, true) => TextAlignKeyword::Left,
+ (TextAlignKeyword::Start, false) => TextAlignKeyword::Right,
+ (TextAlignKeyword::End, true) => TextAlignKeyword::Right,
+ (TextAlignKeyword::End, false) => TextAlignKeyword::Left,
+ _ => parent,
+ }
+ },
+ #[cfg(feature = "gecko")]
+ TextAlign::MozCenterOrInherit => {
+ let parent = _context
+ .builder
+ .get_parent_inherited_text()
+ .clone_text_align();
+ if parent == TextAlignKeyword::Start {
+ TextAlignKeyword::Center
+ } else {
+ parent
+ }
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ TextAlign::Keyword(*computed)
+ }
+}
+
+fn fill_mode_is_default_and_shape_exists(
+ fill: &TextEmphasisFillMode,
+ shape: &Option<TextEmphasisShapeKeyword>,
+) -> bool {
+ shape.is_some() && fill.is_filled()
+}
+
+/// Specified value of text-emphasis-style property.
+///
+/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+#[allow(missing_docs)]
+pub enum TextEmphasisStyle {
+ /// [ <fill> || <shape> ]
+ Keyword {
+ #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")]
+ fill: TextEmphasisFillMode,
+ shape: Option<TextEmphasisShapeKeyword>,
+ },
+ /// `none`
+ None,
+ /// `<string>` (of which only the first grapheme cluster will be used).
+ String(crate::OwnedStr),
+}
+
+/// Fill mode for the text-emphasis-style property
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum TextEmphasisFillMode {
+ /// `filled`
+ Filled,
+ /// `open`
+ Open,
+}
+
+impl TextEmphasisFillMode {
+ /// Whether the value is `filled`.
+ #[inline]
+ pub fn is_filled(&self) -> bool {
+ matches!(*self, TextEmphasisFillMode::Filled)
+ }
+}
+
+/// Shape keyword for the text-emphasis-style property
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum TextEmphasisShapeKeyword {
+ /// `dot`
+ Dot,
+ /// `circle`
+ Circle,
+ /// `double-circle`
+ DoubleCircle,
+ /// `triangle`
+ Triangle,
+ /// `sesame`
+ Sesame,
+}
+
+impl ToComputedValue for TextEmphasisStyle {
+ type ComputedValue = ComputedTextEmphasisStyle;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ TextEmphasisStyle::Keyword { fill, shape } => {
+ let shape = shape.unwrap_or_else(|| {
+ // FIXME(emilio, bug 1572958): This should set the
+ // rule_cache_conditions properly.
+ //
+ // Also should probably use WritingMode::is_vertical rather
+ // than the computed value of the `writing-mode` property.
+ if context.style().get_inherited_box().clone_writing_mode() ==
+ SpecifiedWritingMode::HorizontalTb
+ {
+ TextEmphasisShapeKeyword::Circle
+ } else {
+ TextEmphasisShapeKeyword::Sesame
+ }
+ });
+ ComputedTextEmphasisStyle::Keyword { fill, shape }
+ },
+ TextEmphasisStyle::None => ComputedTextEmphasisStyle::None,
+ TextEmphasisStyle::String(ref s) => {
+ // FIXME(emilio): Doing this at computed value time seems wrong.
+ // The spec doesn't say that this should be a computed-value
+ // time operation. This is observable from getComputedStyle().
+ //
+ // Note that the first grapheme cluster boundary should always be the start of the string.
+ let first_grapheme_end = GraphemeClusterSegmenter::new().segment_str(s).nth(1).unwrap_or(0);
+ ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into())
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match *computed {
+ ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword {
+ fill,
+ shape: Some(shape),
+ },
+ ComputedTextEmphasisStyle::None => TextEmphasisStyle::None,
+ ComputedTextEmphasisStyle::String(ref string) => {
+ TextEmphasisStyle::String(string.clone())
+ },
+ }
+ }
+}
+
+impl Parse for TextEmphasisStyle {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(TextEmphasisStyle::None);
+ }
+
+ if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) {
+ // Handle <string>
+ return Ok(TextEmphasisStyle::String(s.into()));
+ }
+
+ // Handle a pair of keywords
+ let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
+ let fill = input.try_parse(TextEmphasisFillMode::parse).ok();
+ if shape.is_none() {
+ shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
+ }
+
+ if shape.is_none() && fill.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ // If a shape keyword is specified but neither filled nor open is
+ // specified, filled is assumed.
+ let fill = fill.unwrap_or(TextEmphasisFillMode::Filled);
+
+ // We cannot do the same because the default `<shape>` depends on the
+ // computed writing-mode.
+ Ok(TextEmphasisStyle::Keyword { fill, shape })
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ Serialize,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[css(bitflags(
+ mixed = "over,under,left,right",
+ validate_mixed = "Self::validate_and_simplify"
+))]
+/// Values for text-emphasis-position:
+/// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property>
+pub struct TextEmphasisPosition(u8);
+bitflags! {
+ impl TextEmphasisPosition: u8 {
+ /// Draws marks to the right of the text in vertical writing mode.
+ const OVER = 1 << 0;
+ /// Draw marks under the text in horizontal writing mode.
+ const UNDER = 1 << 1;
+ /// Draw marks to the left of the text in vertical writing mode.
+ const LEFT = 1 << 2;
+ /// Draws marks to the right of the text in vertical writing mode.
+ const RIGHT = 1 << 3;
+ }
+}
+
+impl TextEmphasisPosition {
+ fn validate_and_simplify(&mut self) -> bool {
+ if self.intersects(Self::OVER) == self.intersects(Self::UNDER) {
+ return false;
+ }
+
+ if self.intersects(Self::LEFT) {
+ return !self.intersects(Self::RIGHT);
+ }
+
+ self.remove(Self::RIGHT); // Right is the default
+ true
+ }
+}
+
+/// Values for the `word-break` property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum WordBreak {
+ Normal,
+ BreakAll,
+ KeepAll,
+ /// The break-word value, needed for compat.
+ ///
+ /// Specifying `word-break: break-word` makes `overflow-wrap` behave as
+ /// `anywhere`, and `word-break` behave like `normal`.
+ #[cfg(feature = "gecko")]
+ BreakWord,
+}
+
+/// Values for the `text-justify` CSS property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum TextJustify {
+ Auto,
+ None,
+ InterWord,
+ // See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute
+ // and https://github.com/w3c/csswg-drafts/issues/6156 for the alias.
+ #[parse(aliases = "distribute")]
+ InterCharacter,
+}
+
+/// Values for the `-moz-control-character-visibility` CSS property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum MozControlCharacterVisibility {
+ Hidden,
+ Visible,
+}
+
+impl Default for MozControlCharacterVisibility {
+ fn default() -> Self {
+ if static_prefs::pref!("layout.css.control-characters.visible") {
+ Self::Visible
+ } else {
+ Self::Hidden
+ }
+ }
+}
+
+/// Values for the `line-break` property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum LineBreak {
+ Auto,
+ Loose,
+ Normal,
+ Strict,
+ Anywhere,
+}
+
+/// Values for the `overflow-wrap` property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum OverflowWrap {
+ Normal,
+ BreakWord,
+ Anywhere,
+}
+
+/// A specified value for the `text-indent` property
+/// which takes the grammar of [<length-percentage>] && hanging? && each-line?
+///
+/// https://drafts.csswg.org/css-text/#propdef-text-indent
+pub type TextIndent = GenericTextIndent<LengthPercentage>;
+
+impl Parse for TextIndent {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut length = None;
+ let mut hanging = false;
+ let mut each_line = false;
+
+ // The length-percentage and the two possible keywords can occur in any order.
+ while !input.is_exhausted() {
+ // If we haven't seen a length yet, try to parse one.
+ if length.is_none() {
+ if let Ok(len) = input
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes))
+ {
+ length = Some(len);
+ continue;
+ }
+ }
+
+ if static_prefs::pref!("layout.css.text-indent-keywords.enabled") {
+ // Check for the keywords (boolean flags).
+ try_match_ident_ignore_ascii_case! { input,
+ "hanging" if !hanging => hanging = true,
+ "each-line" if !each_line => each_line = true,
+ }
+ continue;
+ }
+
+ // If we reach here, there must be something that we failed to parse;
+ // just break and let the caller deal with it.
+ break;
+ }
+
+ // The length-percentage value is required for the declaration to be valid.
+ if let Some(length) = length {
+ Ok(Self {
+ length,
+ hanging,
+ each_line,
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+/// Implements text-decoration-skip-ink which takes the keywords auto | none | all
+///
+/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property
+#[repr(u8)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum TextDecorationSkipInk {
+ Auto,
+ None,
+ All,
+}
+
+/// Implements type for `text-decoration-thickness` property
+pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>;
+
+impl TextDecorationLength {
+ /// `Auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ GenericTextDecorationLength::Auto
+ }
+
+ /// Whether this is the `Auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, GenericTextDecorationLength::Auto)
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "auto,from-font,under,left,right")]
+#[repr(C)]
+/// Specified keyword values for the text-underline-position property.
+/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives
+/// `auto | [ from-font | under ] || [ left | right ]`.)
+/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
+pub struct TextUnderlinePosition(u8);
+bitflags! {
+ impl TextUnderlinePosition: u8 {
+ /// Use automatic positioning below the alphabetic baseline.
+ const AUTO = 0;
+ /// Use underline position from the first available font.
+ const FROM_FONT = 1 << 0;
+ /// Below the glyph box.
+ const UNDER = 1 << 1;
+ /// In vertical mode, place to the left of the text.
+ const LEFT = 1 << 2;
+ /// In vertical mode, place to the right of the text.
+ const RIGHT = 1 << 3;
+ }
+}
+
+// TODO: This can be derived with some care.
+impl Parse for TextUnderlinePosition {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<TextUnderlinePosition, ParseError<'i>> {
+ let mut result = TextUnderlinePosition::empty();
+
+ loop {
+ let location = input.current_source_location();
+ let ident = match input.next() {
+ Ok(&Token::Ident(ref ident)) => ident,
+ Ok(other) => return Err(location.new_unexpected_token_error(other.clone())),
+ Err(..) => break,
+ };
+
+ match_ignore_ascii_case! { ident,
+ "auto" if result.is_empty() => {
+ return Ok(result);
+ },
+ "from-font" if !result.intersects(TextUnderlinePosition::FROM_FONT |
+ TextUnderlinePosition::UNDER) => {
+ result.insert(TextUnderlinePosition::FROM_FONT);
+ },
+ "under" if !result.intersects(TextUnderlinePosition::FROM_FONT |
+ TextUnderlinePosition::UNDER) => {
+ result.insert(TextUnderlinePosition::UNDER);
+ },
+ "left" if !result.intersects(TextUnderlinePosition::LEFT |
+ TextUnderlinePosition::RIGHT) => {
+ result.insert(TextUnderlinePosition::LEFT);
+ },
+ "right" if !result.intersects(TextUnderlinePosition::LEFT |
+ TextUnderlinePosition::RIGHT) => {
+ result.insert(TextUnderlinePosition::RIGHT);
+ },
+ _ => return Err(location.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+ )),
+ }
+ }
+
+ if !result.is_empty() {
+ Ok(result)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+impl ToCss for TextUnderlinePosition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_empty() {
+ return dest.write_str("auto");
+ }
+
+ let mut writer = SequenceWriter::new(dest, " ");
+ let mut any = false;
+
+ macro_rules! maybe_write {
+ ($ident:ident => $str:expr) => {
+ if self.contains(TextUnderlinePosition::$ident) {
+ any = true;
+ writer.raw_item($str)?;
+ }
+ };
+ }
+
+ maybe_write!(FROM_FONT => "from-font");
+ maybe_write!(UNDER => "under");
+ maybe_write!(LEFT => "left");
+ maybe_write!(RIGHT => "right");
+
+ debug_assert!(any);
+
+ Ok(())
+ }
+}
+
+/// Values for `ruby-position` property
+#[repr(u8)]
+#[derive(
+ Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum RubyPosition {
+ AlternateOver,
+ AlternateUnder,
+ Over,
+ Under,
+}
+
+impl Parse for RubyPosition {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<RubyPosition, ParseError<'i>> {
+ // Parse alternate before
+ let alternate = input
+ .try_parse(|i| i.expect_ident_matching("alternate"))
+ .is_ok();
+ if alternate && input.is_exhausted() {
+ return Ok(RubyPosition::AlternateOver);
+ }
+ // Parse over / under
+ let over = try_match_ident_ignore_ascii_case! { input,
+ "over" => true,
+ "under" => false,
+ };
+ // Parse alternate after
+ let alternate = alternate ||
+ input
+ .try_parse(|i| i.expect_ident_matching("alternate"))
+ .is_ok();
+
+ Ok(match (over, alternate) {
+ (true, true) => RubyPosition::AlternateOver,
+ (false, true) => RubyPosition::AlternateUnder,
+ (true, false) => RubyPosition::Over,
+ (false, false) => RubyPosition::Under,
+ })
+ }
+}
+
+impl ToCss for RubyPosition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(match self {
+ RubyPosition::AlternateOver => "alternate",
+ RubyPosition::AlternateUnder => "alternate under",
+ RubyPosition::Over => "over",
+ RubyPosition::Under => "under",
+ })
+ }
+}
+
+impl SpecifiedValueInfo for RubyPosition {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["alternate", "over", "under"])
+ }
+}
diff --git a/servo/components/style/values/specified/time.rs b/servo/components/style/values/specified/time.rs
new file mode 100644
index 0000000000..3061ebddcc
--- /dev/null
+++ b/servo/components/style/values/specified/time.rs
@@ -0,0 +1,183 @@
+/* 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 time values.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::time::Time as ComputedTime;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::specified::calc::CalcNode;
+use crate::values::CSSFloat;
+use crate::Zero;
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+/// A time value according to CSS-VALUES § 6.2.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Time {
+ seconds: CSSFloat,
+ unit: TimeUnit,
+ calc_clamping_mode: Option<AllowedNumericType>,
+}
+
+/// A time unit.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub enum TimeUnit {
+ /// `s`
+ Second,
+ /// `ms`
+ Millisecond,
+}
+
+impl Time {
+ /// Returns a time value that represents `seconds` seconds.
+ pub fn from_seconds_with_calc_clamping_mode(
+ seconds: CSSFloat,
+ calc_clamping_mode: Option<AllowedNumericType>,
+ ) -> Self {
+ Time {
+ seconds,
+ unit: TimeUnit::Second,
+ calc_clamping_mode,
+ }
+ }
+
+ /// Returns a time value that represents `seconds` seconds.
+ pub fn from_seconds(seconds: CSSFloat) -> Self {
+ Self::from_seconds_with_calc_clamping_mode(seconds, None)
+ }
+
+ /// Returns the time in fractional seconds.
+ pub fn seconds(self) -> CSSFloat {
+ self.seconds
+ }
+
+ /// Returns the unit of the time.
+ #[inline]
+ pub fn unit(&self) -> &'static str {
+ match self.unit {
+ TimeUnit::Second => "s",
+ TimeUnit::Millisecond => "ms",
+ }
+ }
+
+ #[inline]
+ fn unitless_value(&self) -> CSSFloat {
+ match self.unit {
+ TimeUnit::Second => self.seconds,
+ TimeUnit::Millisecond => self.seconds * 1000.,
+ }
+ }
+
+ /// Parses a time according to CSS-VALUES § 6.2.
+ pub fn parse_dimension(value: CSSFloat, unit: &str) -> Result<Time, ()> {
+ let (seconds, unit) = match_ignore_ascii_case! { unit,
+ "s" => (value, TimeUnit::Second),
+ "ms" => (value / 1000.0, TimeUnit::Millisecond),
+ _ => return Err(())
+ };
+
+ Ok(Time {
+ seconds,
+ unit,
+ calc_clamping_mode: None,
+ })
+ }
+
+ fn parse_with_clamping_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ ) -> Result<Self, ParseError<'i>> {
+ use style_traits::ParsingMode;
+
+ let location = input.current_source_location();
+ match *input.next()? {
+ // Note that we generally pass ParserContext to is_ok() to check
+ // that the ParserMode of the ParserContext allows all numeric
+ // values for SMIL regardless of clamping_mode, but in this Time
+ // value case, the value does not animate for SMIL at all, so we use
+ // ParsingMode::DEFAULT directly.
+ Token::Dimension {
+ value, ref unit, ..
+ } if clamping_mode.is_ok(ParsingMode::DEFAULT, value) => {
+ Time::parse_dimension(value, unit)
+ .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse_time(context, input, clamping_mode, function)
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+
+ /// Parses a non-negative time value.
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ }
+}
+
+impl Zero for Time {
+ #[inline]
+ fn zero() -> Self {
+ Self::from_seconds(0.0)
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ // The unit doesn't matter, i.e. `s` and `ms` are the same for zero.
+ self.seconds == 0.0 && self.calc_clamping_mode.is_none()
+ }
+}
+
+impl ToComputedValue for Time {
+ type ComputedValue = ComputedTime;
+
+ fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
+ let seconds = self
+ .calc_clamping_mode
+ .map_or(self.seconds(), |mode| mode.clamp(self.seconds()));
+
+ ComputedTime::from_seconds(crate::values::normalize(seconds))
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Time {
+ seconds: computed.seconds(),
+ unit: TimeUnit::Second,
+ calc_clamping_mode: None,
+ }
+ }
+}
+
+impl Parse for Time {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::All)
+ }
+}
+
+impl ToCss for Time {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ crate::values::serialize_specified_dimension(
+ self.unitless_value(),
+ self.unit(),
+ self.calc_clamping_mode.is_some(),
+ dest,
+ )
+ }
+}
+
+impl SpecifiedValueInfo for Time {}
diff --git a/servo/components/style/values/specified/transform.rs b/servo/components/style/values/specified/transform.rs
new file mode 100644
index 0000000000..ec5e286bc2
--- /dev/null
+++ b/servo/components/style/values/specified/transform.rs
@@ -0,0 +1,530 @@
+/* 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 types for CSS values that are related to transformations.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{Context, LengthPercentage as ComputedLengthPercentage};
+use crate::values::computed::{Percentage as ComputedPercentage, ToComputedValue};
+use crate::values::generics::transform as generic;
+use crate::values::generics::transform::{Matrix, Matrix3D};
+use crate::values::specified::position::{
+ HorizontalPositionKeyword, Side, VerticalPositionKeyword,
+};
+use crate::values::specified::{
+ self, Angle, Integer, Length, LengthPercentage, Number, NumberOrPercentage,
+};
+use crate::Zero;
+use cssparser::Parser;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+pub use crate::values::generics::transform::TransformStyle;
+
+/// A single operation in a specified CSS `transform`
+pub type TransformOperation =
+ generic::TransformOperation<Angle, Number, Length, Integer, LengthPercentage>;
+
+/// A specified CSS `transform`
+pub type Transform = generic::Transform<TransformOperation>;
+
+/// The specified value of a CSS `<transform-origin>`
+pub type TransformOrigin = generic::TransformOrigin<
+ OriginComponent<HorizontalPositionKeyword>,
+ OriginComponent<VerticalPositionKeyword>,
+ Length,
+>;
+
+#[cfg(feature = "gecko")]
+fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool {
+ static_prefs::pref!("layout.css.transform-box-content-stroke.enabled")
+}
+
+#[cfg(feature = "servo")]
+fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool {
+ false
+}
+
+/// The specified value of `transform-box`.
+/// https://drafts.csswg.org/css-transforms-1/#transform-box
+// Note: Once we ship everything, we can drop this and just use single_keyword for tranform-box.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum TransformBox {
+ #[parse(condition = "all_transform_boxes_are_enabled")]
+ ContentBox,
+ BorderBox,
+ FillBox,
+ #[parse(condition = "all_transform_boxes_are_enabled")]
+ StrokeBox,
+ ViewBox,
+}
+
+impl TransformOrigin {
+ /// Returns the initial specified value for `transform-origin`.
+ #[inline]
+ pub fn initial_value() -> Self {
+ Self::new(
+ OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))),
+ OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))),
+ Length::zero(),
+ )
+ }
+
+ /// Returns the `0 0` value.
+ pub fn zero_zero() -> Self {
+ Self::new(
+ OriginComponent::Length(LengthPercentage::zero()),
+ OriginComponent::Length(LengthPercentage::zero()),
+ Length::zero(),
+ )
+ }
+}
+
+impl Transform {
+ /// Internal parse function for deciding if we wish to accept prefixed values or not
+ ///
+ /// `transform` allows unitless zero angles as an exception, see:
+ /// https://github.com/w3c/csswg-drafts/issues/1162
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use style_traits::{Separator, Space};
+
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(generic::Transform::none());
+ }
+
+ Ok(generic::Transform(
+ Space::parse(input, |input| {
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(|input| {
+ let location = input.current_source_location();
+ let result = match_ignore_ascii_case! { &function,
+ "matrix" => {
+ let a = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let b = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let c = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let d = Number::parse(context, input)?;
+ input.expect_comma()?;
+ // Standard matrix parsing.
+ let e = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let f = Number::parse(context, input)?;
+ Ok(generic::TransformOperation::Matrix(Matrix { a, b, c, d, e, f }))
+ },
+ "matrix3d" => {
+ let m11 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m12 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m13 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m14 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m21 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m22 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m23 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m24 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m31 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m32 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m33 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m34 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ // Standard matrix3d parsing.
+ let m41 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m42 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m43 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m44 = Number::parse(context, input)?;
+ Ok(generic::TransformOperation::Matrix3D(Matrix3D {
+ m11, m12, m13, m14,
+ m21, m22, m23, m24,
+ m31, m32, m33, m34,
+ m41, m42, m43, m44,
+ }))
+ },
+ "translate" => {
+ let sx = specified::LengthPercentage::parse(context, input)?;
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ let sy = specified::LengthPercentage::parse(context, input)?;
+ Ok(generic::TransformOperation::Translate(sx, sy))
+ } else {
+ Ok(generic::TransformOperation::Translate(sx, Zero::zero()))
+ }
+ },
+ "translatex" => {
+ let tx = specified::LengthPercentage::parse(context, input)?;
+ Ok(generic::TransformOperation::TranslateX(tx))
+ },
+ "translatey" => {
+ let ty = specified::LengthPercentage::parse(context, input)?;
+ Ok(generic::TransformOperation::TranslateY(ty))
+ },
+ "translatez" => {
+ let tz = specified::Length::parse(context, input)?;
+ Ok(generic::TransformOperation::TranslateZ(tz))
+ },
+ "translate3d" => {
+ let tx = specified::LengthPercentage::parse(context, input)?;
+ input.expect_comma()?;
+ let ty = specified::LengthPercentage::parse(context, input)?;
+ input.expect_comma()?;
+ let tz = specified::Length::parse(context, input)?;
+ Ok(generic::TransformOperation::Translate3D(tx, ty, tz))
+ },
+ "scale" => {
+ let sx = NumberOrPercentage::parse(context, input)?.to_number();
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ let sy = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::Scale(sx, sy))
+ } else {
+ Ok(generic::TransformOperation::Scale(sx, sx))
+ }
+ },
+ "scalex" => {
+ let sx = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::ScaleX(sx))
+ },
+ "scaley" => {
+ let sy = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::ScaleY(sy))
+ },
+ "scalez" => {
+ let sz = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::ScaleZ(sz))
+ },
+ "scale3d" => {
+ let sx = NumberOrPercentage::parse(context, input)?.to_number();
+ input.expect_comma()?;
+ let sy = NumberOrPercentage::parse(context, input)?.to_number();
+ input.expect_comma()?;
+ let sz = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::Scale3D(sx, sy, sz))
+ },
+ "rotate" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::Rotate(theta))
+ },
+ "rotatex" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::RotateX(theta))
+ },
+ "rotatey" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::RotateY(theta))
+ },
+ "rotatez" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::RotateZ(theta))
+ },
+ "rotate3d" => {
+ let ax = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let ay = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let az = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ // TODO(gw): Check that the axis can be normalized.
+ Ok(generic::TransformOperation::Rotate3D(ax, ay, az, theta))
+ },
+ "skew" => {
+ let ax = specified::Angle::parse_with_unitless(context, input)?;
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ let ay = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::Skew(ax, ay))
+ } else {
+ Ok(generic::TransformOperation::Skew(ax, Zero::zero()))
+ }
+ },
+ "skewx" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::SkewX(theta))
+ },
+ "skewy" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::SkewY(theta))
+ },
+ "perspective" => {
+ let p = match input.try_parse(|input| specified::Length::parse_non_negative(context, input)) {
+ Ok(p) => generic::PerspectiveFunction::Length(p),
+ Err(..) => {
+ input.expect_ident_matching("none")?;
+ generic::PerspectiveFunction::None
+ }
+ };
+ Ok(generic::TransformOperation::Perspective(p))
+ },
+ _ => Err(()),
+ };
+ result.map_err(|()| {
+ location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(
+ function.clone(),
+ ))
+ })
+ })
+ })?
+ .into(),
+ ))
+ }
+}
+
+impl Parse for Transform {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Transform::parse_internal(context, input)
+ }
+}
+
+/// The specified value of a component of a CSS `<transform-origin>`.
+#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum OriginComponent<S> {
+ /// `center`
+ Center,
+ /// `<length-percentage>`
+ Length(LengthPercentage),
+ /// `<side>`
+ Side(S),
+}
+
+impl Parse for TransformOrigin {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let parse_depth = |input: &mut Parser| {
+ input
+ .try_parse(|i| Length::parse(context, i))
+ .unwrap_or(Length::zero())
+ };
+ match input.try_parse(|i| OriginComponent::parse(context, i)) {
+ Ok(x_origin @ OriginComponent::Center) => {
+ if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ let y_origin = OriginComponent::Center;
+ if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
+ let x_origin = OriginComponent::Side(x_keyword);
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ let depth = Length::from_px(0.);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ },
+ Ok(x_origin) => {
+ if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ let y_origin = OriginComponent::Center;
+ let depth = Length::from_px(0.);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ },
+ Err(_) => {},
+ }
+ let y_keyword = VerticalPositionKeyword::parse(input)?;
+ let y_origin = OriginComponent::Side(y_keyword);
+ if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
+ let x_origin = OriginComponent::Side(x_keyword);
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ if input
+ .try_parse(|i| i.expect_ident_matching("center"))
+ .is_ok()
+ {
+ let x_origin = OriginComponent::Center;
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ let x_origin = OriginComponent::Center;
+ let depth = Length::from_px(0.);
+ Ok(Self::new(x_origin, y_origin, depth))
+ }
+}
+
+impl<S> ToComputedValue for OriginComponent<S>
+where
+ S: Side,
+{
+ type ComputedValue = ComputedLengthPercentage;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ OriginComponent::Center => {
+ ComputedLengthPercentage::new_percent(ComputedPercentage(0.5))
+ },
+ OriginComponent::Length(ref length) => length.to_computed_value(context),
+ OriginComponent::Side(ref keyword) => {
+ let p = ComputedPercentage(if keyword.is_start() { 0. } else { 1. });
+ ComputedLengthPercentage::new_percent(p)
+ },
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ OriginComponent::Length(ToComputedValue::from_computed_value(computed))
+ }
+}
+
+impl<S> OriginComponent<S> {
+ /// `0%`
+ pub fn zero() -> Self {
+ OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage::zero()))
+ }
+}
+
+/// A specified CSS `rotate`
+pub type Rotate = generic::Rotate<Number, Angle>;
+
+impl Parse for Rotate {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(generic::Rotate::None);
+ }
+
+ // Parse <angle> or [ x | y | z | <number>{3} ] && <angle>.
+ //
+ // The rotate axis and angle could be in any order, so we parse angle twice to cover
+ // two cases. i.e. `<number>{3} <angle>` or `<angle> <number>{3}`
+ let angle = input
+ .try_parse(|i| specified::Angle::parse(context, i))
+ .ok();
+ let axis = input
+ .try_parse(|i| {
+ Ok(try_match_ident_ignore_ascii_case! { i,
+ "x" => (Number::new(1.), Number::new(0.), Number::new(0.)),
+ "y" => (Number::new(0.), Number::new(1.), Number::new(0.)),
+ "z" => (Number::new(0.), Number::new(0.), Number::new(1.)),
+ })
+ })
+ .or_else(|_: ParseError| -> Result<_, ParseError> {
+ input.try_parse(|i| {
+ Ok((
+ Number::parse(context, i)?,
+ Number::parse(context, i)?,
+ Number::parse(context, i)?,
+ ))
+ })
+ })
+ .ok();
+ let angle = match angle {
+ Some(a) => a,
+ None => specified::Angle::parse(context, input)?,
+ };
+
+ Ok(match axis {
+ Some((x, y, z)) => generic::Rotate::Rotate3D(x, y, z, angle),
+ None => generic::Rotate::Rotate(angle),
+ })
+ }
+}
+
+/// A specified CSS `translate`
+pub type Translate = generic::Translate<LengthPercentage, Length>;
+
+impl Parse for Translate {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(generic::Translate::None);
+ }
+
+ let tx = specified::LengthPercentage::parse(context, input)?;
+ if let Ok(ty) = input.try_parse(|i| specified::LengthPercentage::parse(context, i)) {
+ if let Ok(tz) = input.try_parse(|i| specified::Length::parse(context, i)) {
+ // 'translate: <length-percentage> <length-percentage> <length>'
+ return Ok(generic::Translate::Translate(tx, ty, tz));
+ }
+
+ // translate: <length-percentage> <length-percentage>'
+ return Ok(generic::Translate::Translate(
+ tx,
+ ty,
+ specified::Length::zero(),
+ ));
+ }
+
+ // 'translate: <length-percentage> '
+ Ok(generic::Translate::Translate(
+ tx,
+ specified::LengthPercentage::zero(),
+ specified::Length::zero(),
+ ))
+ }
+}
+
+/// A specified CSS `scale`
+pub type Scale = generic::Scale<Number>;
+
+impl Parse for Scale {
+ /// Scale accepts <number> | <percentage>, so we parse it as NumberOrPercentage,
+ /// and then convert into an Number if it's a Percentage.
+ /// https://github.com/w3c/csswg-drafts/pull/4396
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(generic::Scale::None);
+ }
+
+ let sx = NumberOrPercentage::parse(context, input)?.to_number();
+ if let Ok(sy) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) {
+ let sy = sy.to_number();
+ if let Ok(sz) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) {
+ // 'scale: <number> <number> <number>'
+ return Ok(generic::Scale::Scale(sx, sy, sz.to_number()));
+ }
+
+ // 'scale: <number> <number>'
+ return Ok(generic::Scale::Scale(sx, sy, Number::new(1.0)));
+ }
+
+ // 'scale: <number>'
+ Ok(generic::Scale::Scale(sx, sx, Number::new(1.0)))
+ }
+}
diff --git a/servo/components/style/values/specified/ui.rs b/servo/components/style/values/specified/ui.rs
new file mode 100644
index 0000000000..2237335ec4
--- /dev/null
+++ b/servo/components/style/values/specified/ui.rs
@@ -0,0 +1,257 @@
+/* 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 types for UI properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::ui as generics;
+use crate::values::specified::color::Color;
+use crate::values::specified::image::Image;
+use crate::values::specified::Number;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{
+ CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss,
+};
+
+/// A specified value for the `cursor` property.
+pub type Cursor = generics::GenericCursor<CursorImage>;
+
+/// A specified value for item of `image cursors`.
+pub type CursorImage = generics::GenericCursorImage<Image, Number>;
+
+impl Parse for Cursor {
+ /// cursor: [<url> [<number> <number>]?]# [auto | default | ...]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut images = vec![];
+ loop {
+ match input.try_parse(|input| CursorImage::parse(context, input)) {
+ Ok(image) => images.push(image),
+ Err(_) => break,
+ }
+ input.expect_comma()?;
+ }
+ Ok(Self {
+ images: images.into(),
+ keyword: CursorKind::parse(input)?,
+ })
+ }
+}
+
+impl Parse for CursorImage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::Zero;
+
+ let image = Image::parse_only_url(context, input)?;
+ let mut has_hotspot = false;
+ let mut hotspot_x = Number::zero();
+ let mut hotspot_y = Number::zero();
+
+ if let Ok(x) = input.try_parse(|input| Number::parse(context, input)) {
+ has_hotspot = true;
+ hotspot_x = x;
+ hotspot_y = Number::parse(context, input)?;
+ }
+
+ Ok(Self {
+ image,
+ has_hotspot,
+ hotspot_x,
+ hotspot_y,
+ })
+ }
+}
+
+// This trait is manually implemented because we don't support the whole <image>
+// syntax for cursors
+impl SpecifiedValueInfo for CursorImage {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["url", "image-set"]);
+ }
+}
+/// Specified value of `-moz-force-broken-image-icon`
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct BoolInteger(pub bool);
+
+impl BoolInteger {
+ /// Returns 0
+ #[inline]
+ pub fn zero() -> Self {
+ Self(false)
+ }
+}
+
+impl Parse for BoolInteger {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // We intentionally don't support calc values here.
+ match input.expect_integer()? {
+ 0 => Ok(Self(false)),
+ 1 => Ok(Self(true)),
+ _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+}
+
+impl ToCss for BoolInteger {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(if self.0 { "1" } else { "0" })
+ }
+}
+
+/// A specified value for `scrollbar-color` property
+pub type ScrollbarColor = generics::ScrollbarColor<Color>;
+
+impl Parse for ScrollbarColor {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ return Ok(generics::ScrollbarColor::Auto);
+ }
+ Ok(generics::ScrollbarColor::Colors {
+ thumb: Color::parse(context, input)?,
+ track: Color::parse(context, input)?,
+ })
+ }
+}
+
+/// The specified value for the `user-select` property.
+///
+/// https://drafts.csswg.org/css-ui-4/#propdef-user-select
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum UserSelect {
+ Auto,
+ Text,
+ #[parse(aliases = "-moz-none")]
+ None,
+ /// Force selection of all children.
+ All,
+}
+
+/// The keywords allowed in the Cursor property.
+///
+/// https://drafts.csswg.org/css-ui-4/#propdef-cursor
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum CursorKind {
+ None,
+ Default,
+ Pointer,
+ ContextMenu,
+ Help,
+ Progress,
+ Wait,
+ Cell,
+ Crosshair,
+ Text,
+ VerticalText,
+ Alias,
+ Copy,
+ Move,
+ NoDrop,
+ NotAllowed,
+ #[parse(aliases = "-moz-grab")]
+ Grab,
+ #[parse(aliases = "-moz-grabbing")]
+ Grabbing,
+ EResize,
+ NResize,
+ NeResize,
+ NwResize,
+ SResize,
+ SeResize,
+ SwResize,
+ WResize,
+ EwResize,
+ NsResize,
+ NeswResize,
+ NwseResize,
+ ColResize,
+ RowResize,
+ AllScroll,
+ #[parse(aliases = "-moz-zoom-in")]
+ ZoomIn,
+ #[parse(aliases = "-moz-zoom-out")]
+ ZoomOut,
+ Auto,
+}
+
+/// The keywords allowed in the -moz-theme property.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum MozTheme {
+ /// Choose the default (maybe native) rendering.
+ Auto,
+ /// Choose the non-native rendering.
+ NonNative,
+}
diff --git a/servo/components/style/values/specified/url.rs b/servo/components/style/values/specified/url.rs
new file mode 100644
index 0000000000..17ecbe0d5e
--- /dev/null
+++ b/servo/components/style/values/specified/url.rs
@@ -0,0 +1,15 @@
+/* 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/. */
+
+//! Common handling for the specified value CSS url() values.
+
+use crate::values::generics::url::GenericUrlOrNone;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::url::{SpecifiedImageUrl, SpecifiedUrl};
+#[cfg(feature = "servo")]
+pub use crate::servo::url::{SpecifiedImageUrl, SpecifiedUrl};
+
+/// Specified <url> | <none>
+pub type UrlOrNone = GenericUrlOrNone<SpecifiedUrl>;
diff --git a/servo/components/style_derive/Cargo.toml b/servo/components/style_derive/Cargo.toml
new file mode 100644
index 0000000000..35f36b04ef
--- /dev/null
+++ b/servo/components/style_derive/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "style_derive"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+publish = false
+
+[lib]
+path = "lib.rs"
+proc-macro = true
+
+[dependencies]
+darling = { version = "0.20", default-features = false }
+derive_common = { path = "../derive_common" }
+proc-macro2 = "1"
+quote = "1"
+syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] }
+synstructure = "0.13"
diff --git a/servo/components/style_derive/animate.rs b/servo/components/style_derive/animate.rs
new file mode 100644
index 0000000000..9549100ad0
--- /dev/null
+++ b/servo/components/style_derive/animate.rs
@@ -0,0 +1,135 @@
+/* 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/. */
+
+use darling::util::PathList;
+use derive_common::cg;
+use proc_macro2::TokenStream;
+use quote::TokenStreamExt;
+use syn::{DeriveInput, WhereClause};
+use synstructure::{Structure, VariantInfo};
+
+pub fn derive(mut input: DeriveInput) -> TokenStream {
+ let animation_input_attrs = cg::parse_input_attrs::<AnimationInputAttrs>(&input);
+
+ let no_bound = animation_input_attrs.no_bound.unwrap_or_default();
+ let mut where_clause = input.generics.where_clause.take();
+ for param in input.generics.type_params() {
+ if !no_bound.iter().any(|name| name.is_ident(&param.ident)) {
+ cg::add_predicate(
+ &mut where_clause,
+ parse_quote!(#param: crate::values::animated::Animate),
+ );
+ }
+ }
+ let (mut match_body, needs_catchall_branch) = {
+ let s = Structure::new(&input);
+ let needs_catchall_branch = s.variants().len() > 1;
+ let match_body = s.variants().iter().fold(quote!(), |body, variant| {
+ let arm = derive_variant_arm(variant, &mut where_clause);
+ quote! { #body #arm }
+ });
+ (match_body, needs_catchall_branch)
+ };
+
+ input.generics.where_clause = where_clause;
+
+ if needs_catchall_branch {
+ // This ideally shouldn't be needed, but see
+ // https://github.com/rust-lang/rust/issues/68867
+ match_body.append_all(quote! { _ => unsafe { debug_unreachable!() } });
+ }
+
+ let name = &input.ident;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ quote! {
+ impl #impl_generics crate::values::animated::Animate for #name #ty_generics #where_clause {
+ #[allow(unused_variables, unused_imports)]
+ #[inline]
+ fn animate(
+ &self,
+ other: &Self,
+ procedure: crate::values::animated::Procedure,
+ ) -> Result<Self, ()> {
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+ match (self, other) {
+ #match_body
+ }
+ }
+ }
+ }
+}
+
+fn derive_variant_arm(
+ variant: &VariantInfo,
+ where_clause: &mut Option<WhereClause>,
+) -> TokenStream {
+ let variant_attrs = cg::parse_variant_attrs_from_ast::<AnimationVariantAttrs>(&variant.ast());
+ let (this_pattern, this_info) = cg::ref_pattern(&variant, "this");
+ let (other_pattern, other_info) = cg::ref_pattern(&variant, "other");
+
+ if variant_attrs.error {
+ return quote! {
+ (&#this_pattern, &#other_pattern) => Err(()),
+ };
+ }
+
+ let (result_value, result_info) = cg::value(&variant, "result");
+ let mut computations = quote!();
+ let iter = result_info.iter().zip(this_info.iter().zip(&other_info));
+ computations.append_all(iter.map(|(result, (this, other))| {
+ let field_attrs = cg::parse_field_attrs::<AnimationFieldAttrs>(&result.ast());
+ if field_attrs.field_bound {
+ let ty = &this.ast().ty;
+ cg::add_predicate(
+ where_clause,
+ parse_quote!(#ty: crate::values::animated::Animate),
+ );
+ }
+ if field_attrs.constant {
+ quote! {
+ if #this != #other {
+ return Err(());
+ }
+ let #result = std::clone::Clone::clone(#this);
+ }
+ } else {
+ quote! {
+ let #result =
+ crate::values::animated::Animate::animate(#this, #other, procedure)?;
+ }
+ }
+ }));
+
+ quote! {
+ (&#this_pattern, &#other_pattern) => {
+ #computations
+ Ok(#result_value)
+ }
+ }
+}
+
+#[derive(Default, FromDeriveInput)]
+#[darling(attributes(animation), default)]
+pub struct AnimationInputAttrs {
+ pub no_bound: Option<PathList>,
+}
+
+#[derive(Default, FromVariant)]
+#[darling(attributes(animation), default)]
+pub struct AnimationVariantAttrs {
+ pub error: bool,
+ // Only here because of structs, where the struct definition acts as a
+ // variant itself.
+ pub no_bound: Option<PathList>,
+}
+
+#[derive(Default, FromField)]
+#[darling(attributes(animation), default)]
+pub struct AnimationFieldAttrs {
+ pub constant: bool,
+ pub field_bound: bool,
+}
diff --git a/servo/components/style_derive/compute_squared_distance.rs b/servo/components/style_derive/compute_squared_distance.rs
new file mode 100644
index 0000000000..022ab115ee
--- /dev/null
+++ b/servo/components/style_derive/compute_squared_distance.rs
@@ -0,0 +1,125 @@
+/* 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/. */
+
+use crate::animate::{AnimationFieldAttrs, AnimationInputAttrs, AnimationVariantAttrs};
+use derive_common::cg;
+use proc_macro2::TokenStream;
+use quote::TokenStreamExt;
+use syn::{DeriveInput, WhereClause};
+use synstructure;
+
+pub fn derive(mut input: DeriveInput) -> TokenStream {
+ let animation_input_attrs = cg::parse_input_attrs::<AnimationInputAttrs>(&input);
+ let no_bound = animation_input_attrs.no_bound.unwrap_or_default();
+ let mut where_clause = input.generics.where_clause.take();
+ for param in input.generics.type_params() {
+ if !no_bound.iter().any(|name| name.is_ident(&param.ident)) {
+ cg::add_predicate(
+ &mut where_clause,
+ parse_quote!(#param: crate::values::distance::ComputeSquaredDistance),
+ );
+ }
+ }
+
+ let (mut match_body, needs_catchall_branch) = {
+ let s = synstructure::Structure::new(&input);
+ let needs_catchall_branch = s.variants().len() > 1;
+
+ let match_body = s.variants().iter().fold(quote!(), |body, variant| {
+ let arm = derive_variant_arm(variant, &mut where_clause);
+ quote! { #body #arm }
+ });
+
+ (match_body, needs_catchall_branch)
+ };
+
+ input.generics.where_clause = where_clause;
+
+ if needs_catchall_branch {
+ // This ideally shouldn't be needed, but see:
+ // https://github.com/rust-lang/rust/issues/68867
+ match_body.append_all(quote! { _ => unsafe { debug_unreachable!() } });
+ }
+
+ let name = &input.ident;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ quote! {
+ impl #impl_generics crate::values::distance::ComputeSquaredDistance for #name #ty_generics #where_clause {
+ #[allow(unused_variables, unused_imports)]
+ #[inline]
+ fn compute_squared_distance(
+ &self,
+ other: &Self,
+ ) -> Result<crate::values::distance::SquaredDistance, ()> {
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+ match (self, other) {
+ #match_body
+ }
+ }
+ }
+ }
+}
+
+fn derive_variant_arm(
+ variant: &synstructure::VariantInfo,
+ mut where_clause: &mut Option<WhereClause>,
+) -> TokenStream {
+ let variant_attrs = cg::parse_variant_attrs_from_ast::<AnimationVariantAttrs>(&variant.ast());
+ let (this_pattern, this_info) = cg::ref_pattern(&variant, "this");
+ let (other_pattern, other_info) = cg::ref_pattern(&variant, "other");
+
+ if variant_attrs.error {
+ return quote! {
+ (&#this_pattern, &#other_pattern) => Err(()),
+ };
+ }
+
+ let sum = if this_info.is_empty() {
+ quote! { crate::values::distance::SquaredDistance::from_sqrt(0.) }
+ } else {
+ let mut sum = quote!();
+ sum.append_separated(this_info.iter().zip(&other_info).map(|(this, other)| {
+ let field_attrs = cg::parse_field_attrs::<DistanceFieldAttrs>(&this.ast());
+ if field_attrs.field_bound {
+ let ty = &this.ast().ty;
+ cg::add_predicate(
+ &mut where_clause,
+ parse_quote!(#ty: crate::values::distance::ComputeSquaredDistance),
+ );
+ }
+
+ let animation_field_attrs =
+ cg::parse_field_attrs::<AnimationFieldAttrs>(&this.ast());
+
+ if animation_field_attrs.constant {
+ quote! {
+ {
+ if #this != #other {
+ return Err(());
+ }
+ crate::values::distance::SquaredDistance::from_sqrt(0.)
+ }
+ }
+ } else {
+ quote! {
+ crate::values::distance::ComputeSquaredDistance::compute_squared_distance(#this, #other)?
+ }
+ }
+ }), quote!(+));
+ sum
+ };
+
+ return quote! {
+ (&#this_pattern, &#other_pattern) => Ok(#sum),
+ };
+}
+
+#[derive(Default, FromField)]
+#[darling(attributes(distance), default)]
+struct DistanceFieldAttrs {
+ field_bound: bool,
+}
diff --git a/servo/components/style_derive/lib.rs b/servo/components/style_derive/lib.rs
new file mode 100644
index 0000000000..079db00c5a
--- /dev/null
+++ b/servo/components/style_derive/lib.rs
@@ -0,0 +1,82 @@
+/* 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/. */
+
+#![recursion_limit = "128"]
+
+#[macro_use]
+extern crate darling;
+extern crate derive_common;
+extern crate proc_macro;
+extern crate proc_macro2;
+#[macro_use]
+extern crate quote;
+#[macro_use]
+extern crate syn;
+extern crate synstructure;
+
+use proc_macro::TokenStream;
+
+mod animate;
+mod compute_squared_distance;
+mod parse;
+mod specified_value_info;
+mod to_animated_value;
+mod to_animated_zero;
+mod to_computed_value;
+mod to_css;
+mod to_resolved_value;
+
+#[proc_macro_derive(Animate, attributes(animate, animation))]
+pub fn derive_animate(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ animate::derive(input).into()
+}
+
+#[proc_macro_derive(ComputeSquaredDistance, attributes(animation, distance))]
+pub fn derive_compute_squared_distance(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ compute_squared_distance::derive(input).into()
+}
+
+#[proc_macro_derive(ToAnimatedValue)]
+pub fn derive_to_animated_value(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ to_animated_value::derive(input).into()
+}
+
+#[proc_macro_derive(Parse, attributes(css, parse))]
+pub fn derive_parse(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ parse::derive(input).into()
+}
+
+#[proc_macro_derive(ToAnimatedZero, attributes(animation, zero))]
+pub fn derive_to_animated_zero(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ to_animated_zero::derive(input).into()
+}
+
+#[proc_macro_derive(ToComputedValue, attributes(compute))]
+pub fn derive_to_computed_value(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ to_computed_value::derive(input).into()
+}
+
+#[proc_macro_derive(ToResolvedValue, attributes(resolve))]
+pub fn derive_to_resolved_value(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ to_resolved_value::derive(input).into()
+}
+
+#[proc_macro_derive(ToCss, attributes(css))]
+pub fn derive_to_css(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ to_css::derive(input).into()
+}
+
+#[proc_macro_derive(SpecifiedValueInfo, attributes(css, parse, value_info))]
+pub fn derive_specified_value_info(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ specified_value_info::derive(input).into()
+}
diff --git a/servo/components/style_derive/parse.rs b/servo/components/style_derive/parse.rs
new file mode 100644
index 0000000000..b1a1213435
--- /dev/null
+++ b/servo/components/style_derive/parse.rs
@@ -0,0 +1,323 @@
+/* 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/. */
+
+use crate::to_css::{CssBitflagAttrs, CssVariantAttrs};
+use derive_common::cg;
+use proc_macro2::{Span, TokenStream};
+use quote::TokenStreamExt;
+use syn::{self, DeriveInput, Ident, Path};
+use synstructure::{Structure, VariantInfo};
+
+#[derive(Default, FromVariant)]
+#[darling(attributes(parse), default)]
+pub struct ParseVariantAttrs {
+ pub aliases: Option<String>,
+ pub condition: Option<Path>,
+}
+
+#[derive(Default, FromField)]
+#[darling(attributes(parse), default)]
+pub struct ParseFieldAttrs {
+ field_bound: bool,
+}
+
+fn parse_bitflags(bitflags: &CssBitflagAttrs) -> TokenStream {
+ let mut match_arms = TokenStream::new();
+ for (rust_name, css_name) in bitflags.single_flags() {
+ let rust_ident = Ident::new(&rust_name, Span::call_site());
+ match_arms.append_all(quote! {
+ #css_name if result.is_empty() => {
+ single_flag = true;
+ Self::#rust_ident
+ },
+ });
+ }
+
+ for (rust_name, css_name) in bitflags.mixed_flags() {
+ let rust_ident = Ident::new(&rust_name, Span::call_site());
+ match_arms.append_all(quote! {
+ #css_name => Self::#rust_ident,
+ });
+ }
+
+ let mut validate_condition = quote! { !result.is_empty() };
+ if let Some(ref function) = bitflags.validate_mixed {
+ validate_condition.append_all(quote! {
+ && #function(&mut result)
+ });
+ }
+
+ // NOTE(emilio): this loop has this weird structure because we run this code
+ // to parse stuff like text-decoration-line in the text-decoration
+ // shorthand, so we need to be a bit careful that we don't error if we don't
+ // consume the whole thing because we find an invalid identifier or other
+ // kind of token. Instead, we should leave it unconsumed.
+ quote! {
+ let mut result = Self::empty();
+ loop {
+ let mut single_flag = false;
+ let flag: Result<_, style_traits::ParseError<'i>> = input.try_parse(|input| {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ #match_arms
+ })
+ });
+
+ let flag = match flag {
+ Ok(flag) => flag,
+ Err(..) => break,
+ };
+
+ if single_flag {
+ return Ok(flag);
+ }
+
+ if result.intersects(flag) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ result.insert(flag);
+ }
+ if #validate_condition {
+ Ok(result)
+ } else {
+ Err(input.new_custom_error(style_traits::StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+fn parse_non_keyword_variant(
+ where_clause: &mut Option<syn::WhereClause>,
+ name: &syn::Ident,
+ variant: &VariantInfo,
+ variant_attrs: &CssVariantAttrs,
+ parse_attrs: &ParseVariantAttrs,
+ skip_try: bool,
+) -> TokenStream {
+ let bindings = variant.bindings();
+ assert!(parse_attrs.aliases.is_none());
+ assert!(variant_attrs.function.is_none());
+ assert!(variant_attrs.keyword.is_none());
+ assert_eq!(
+ bindings.len(),
+ 1,
+ "We only support deriving parse for simple variants"
+ );
+ let variant_name = &variant.ast().ident;
+ let binding_ast = &bindings[0].ast();
+ let ty = &binding_ast.ty;
+
+ if let Some(ref bitflags) = variant_attrs.bitflags {
+ assert!(skip_try, "Should be the only variant");
+ assert!(
+ parse_attrs.condition.is_none(),
+ "Should be the only variant"
+ );
+ assert!(where_clause.is_none(), "Generic bitflags?");
+ return parse_bitflags(bitflags);
+ }
+
+ let field_attrs = cg::parse_field_attrs::<ParseFieldAttrs>(binding_ast);
+ if field_attrs.field_bound {
+ cg::add_predicate(where_clause, parse_quote!(#ty: crate::parser::Parse));
+ }
+
+ let mut parse = if skip_try {
+ quote! {
+ let v = <#ty as crate::parser::Parse>::parse(context, input)?;
+ return Ok(#name::#variant_name(v));
+ }
+ } else {
+ quote! {
+ if let Ok(v) = input.try(|i| <#ty as crate::parser::Parse>::parse(context, i)) {
+ return Ok(#name::#variant_name(v));
+ }
+ }
+ };
+
+ if let Some(ref condition) = parse_attrs.condition {
+ parse = quote! {
+ if #condition(context) {
+ #parse
+ }
+ };
+
+ if skip_try {
+ // We're the last variant and we can fail to parse due to the
+ // condition clause. If that happens, we need to return an error.
+ parse = quote! {
+ #parse
+ Err(input.new_custom_error(style_traits::StyleParseErrorKind::UnspecifiedError))
+ };
+ }
+ }
+
+ parse
+}
+
+pub fn derive(mut input: DeriveInput) -> TokenStream {
+ let mut where_clause = input.generics.where_clause.take();
+ for param in input.generics.type_params() {
+ cg::add_predicate(
+ &mut where_clause,
+ parse_quote!(#param: crate::parser::Parse),
+ );
+ }
+
+ let name = &input.ident;
+ let s = Structure::new(&input);
+
+ let mut saw_condition = false;
+ let mut match_keywords = quote! {};
+ let mut non_keywords = vec![];
+
+ let mut effective_variants = 0;
+ for variant in s.variants().iter() {
+ let css_variant_attrs = cg::parse_variant_attrs_from_ast::<CssVariantAttrs>(&variant.ast());
+ if css_variant_attrs.skip {
+ continue;
+ }
+ effective_variants += 1;
+
+ let parse_attrs = cg::parse_variant_attrs_from_ast::<ParseVariantAttrs>(&variant.ast());
+
+ saw_condition |= parse_attrs.condition.is_some();
+
+ if !variant.bindings().is_empty() {
+ non_keywords.push((variant, css_variant_attrs, parse_attrs));
+ continue;
+ }
+
+ let identifier = cg::to_css_identifier(
+ &css_variant_attrs
+ .keyword
+ .unwrap_or_else(|| variant.ast().ident.to_string()),
+ );
+ let ident = &variant.ast().ident;
+
+ let condition = match parse_attrs.condition {
+ Some(ref p) => quote! { if #p(context) },
+ None => quote! {},
+ };
+
+ match_keywords.extend(quote! {
+ #identifier #condition => Ok(#name::#ident),
+ });
+
+ let aliases = match parse_attrs.aliases {
+ Some(aliases) => aliases,
+ None => continue,
+ };
+
+ for alias in aliases.split(',') {
+ match_keywords.extend(quote! {
+ #alias #condition => Ok(#name::#ident),
+ });
+ }
+ }
+
+ let needs_context = saw_condition || !non_keywords.is_empty();
+
+ let context_ident = if needs_context {
+ quote! { context }
+ } else {
+ quote! { _ }
+ };
+
+ let has_keywords = non_keywords.len() != effective_variants;
+
+ let mut parse_non_keywords = quote! {};
+ for (i, (variant, css_attrs, parse_attrs)) in non_keywords.iter().enumerate() {
+ let skip_try = !has_keywords && i == non_keywords.len() - 1;
+ let parse_variant = parse_non_keyword_variant(
+ &mut where_clause,
+ name,
+ variant,
+ css_attrs,
+ parse_attrs,
+ skip_try,
+ );
+ parse_non_keywords.extend(parse_variant);
+ }
+
+ let parse_body = if needs_context {
+ let parse_keywords = if has_keywords {
+ quote! {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ match_ignore_ascii_case! { &ident,
+ #match_keywords
+ _ => Err(location.new_unexpected_token_error(
+ cssparser::Token::Ident(ident.clone())
+ ))
+ }
+ }
+ } else {
+ quote! {}
+ };
+
+ quote! {
+ #parse_non_keywords
+ #parse_keywords
+ }
+ } else {
+ quote! { Self::parse(input) }
+ };
+
+ let has_non_keywords = !non_keywords.is_empty();
+
+ input.generics.where_clause = where_clause;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ let parse_trait_impl = quote! {
+ impl #impl_generics crate::parser::Parse for #name #ty_generics #where_clause {
+ #[inline]
+ fn parse<'i, 't>(
+ #context_ident: &crate::parser::ParserContext,
+ input: &mut cssparser::Parser<'i, 't>,
+ ) -> Result<Self, style_traits::ParseError<'i>> {
+ #parse_body
+ }
+ }
+ };
+
+ if needs_context {
+ return parse_trait_impl;
+ }
+
+ assert!(!has_non_keywords);
+
+ // TODO(emilio): It'd be nice to get rid of these, but that makes the
+ // conversion harder...
+ let methods_impl = quote! {
+ impl #name {
+ /// Parse this keyword.
+ #[inline]
+ pub fn parse<'i, 't>(
+ input: &mut cssparser::Parser<'i, 't>,
+ ) -> Result<Self, style_traits::ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ Self::from_ident(ident.as_ref()).map_err(|()| {
+ location.new_unexpected_token_error(
+ cssparser::Token::Ident(ident.clone())
+ )
+ })
+ }
+
+ /// Parse this keyword from a string slice.
+ #[inline]
+ pub fn from_ident(ident: &str) -> Result<Self, ()> {
+ match_ignore_ascii_case! { ident,
+ #match_keywords
+ _ => Err(()),
+ }
+ }
+ }
+ };
+
+ quote! {
+ #parse_trait_impl
+ #methods_impl
+ }
+}
diff --git a/servo/components/style_derive/specified_value_info.rs b/servo/components/style_derive/specified_value_info.rs
new file mode 100644
index 0000000000..9a07ab49a6
--- /dev/null
+++ b/servo/components/style_derive/specified_value_info.rs
@@ -0,0 +1,195 @@
+/* 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/. */
+
+use crate::parse::ParseVariantAttrs;
+use crate::to_css::{CssFieldAttrs, CssInputAttrs, CssVariantAttrs};
+use derive_common::cg;
+use proc_macro2::TokenStream;
+use quote::TokenStreamExt;
+use syn::{Data, DeriveInput, Fields, Ident, Type};
+
+pub fn derive(mut input: DeriveInput) -> TokenStream {
+ let css_attrs = cg::parse_input_attrs::<CssInputAttrs>(&input);
+ let mut types = vec![];
+ let mut values = vec![];
+
+ let input_ident = &input.ident;
+ let input_name = || cg::to_css_identifier(&input_ident.to_string());
+ if let Some(function) = css_attrs.function {
+ values.push(function.explicit().unwrap_or_else(input_name));
+ // If the whole value is wrapped in a function, value types of
+ // its fields should not be propagated.
+ } else {
+ let mut where_clause = input.generics.where_clause.take();
+ for param in input.generics.type_params() {
+ cg::add_predicate(
+ &mut where_clause,
+ parse_quote!(#param: style_traits::SpecifiedValueInfo),
+ );
+ }
+ input.generics.where_clause = where_clause;
+
+ match input.data {
+ Data::Enum(ref e) => {
+ for v in e.variants.iter() {
+ let css_attrs = cg::parse_variant_attrs::<CssVariantAttrs>(&v);
+ let info_attrs = cg::parse_variant_attrs::<ValueInfoVariantAttrs>(&v);
+ let parse_attrs = cg::parse_variant_attrs::<ParseVariantAttrs>(&v);
+ if css_attrs.skip {
+ continue;
+ }
+ if let Some(aliases) = parse_attrs.aliases {
+ for alias in aliases.split(',') {
+ values.push(alias.to_string());
+ }
+ }
+ if let Some(other_values) = info_attrs.other_values {
+ for value in other_values.split(',') {
+ values.push(value.to_string());
+ }
+ }
+ let ident = &v.ident;
+ let variant_name = || cg::to_css_identifier(&ident.to_string());
+ if info_attrs.starts_with_keyword {
+ values.push(variant_name());
+ continue;
+ }
+ if let Some(keyword) = css_attrs.keyword {
+ values.push(keyword);
+ continue;
+ }
+ if let Some(function) = css_attrs.function {
+ values.push(function.explicit().unwrap_or_else(variant_name));
+ } else if !derive_struct_fields(&v.fields, &mut types, &mut values) {
+ values.push(variant_name());
+ }
+ }
+ },
+ Data::Struct(ref s) => {
+ if let Some(ref bitflags) = css_attrs.bitflags {
+ for (_rust_name, css_name) in bitflags.single_flags() {
+ values.push(css_name)
+ }
+ for (_rust_name, css_name) in bitflags.mixed_flags() {
+ values.push(css_name)
+ }
+ } else if !derive_struct_fields(&s.fields, &mut types, &mut values) {
+ values.push(input_name());
+ }
+ },
+ Data::Union(_) => unreachable!("union is not supported"),
+ }
+ }
+
+ let info_attrs = cg::parse_input_attrs::<ValueInfoInputAttrs>(&input);
+ if let Some(other_values) = info_attrs.other_values {
+ for value in other_values.split(',') {
+ values.push(value.to_string());
+ }
+ }
+
+ let mut types_value = quote!(0);
+ types_value.append_all(types.iter().map(|ty| {
+ quote! {
+ | <#ty as style_traits::SpecifiedValueInfo>::SUPPORTED_TYPES
+ }
+ }));
+
+ let mut nested_collects = quote!();
+ nested_collects.append_all(types.iter().map(|ty| {
+ quote! {
+ <#ty as style_traits::SpecifiedValueInfo>::collect_completion_keywords(_f);
+ }
+ }));
+
+ if let Some(ty) = info_attrs.ty {
+ types_value.append_all(quote! {
+ | style_traits::CssType::#ty
+ });
+ }
+
+ let append_values = if values.is_empty() {
+ quote!()
+ } else {
+ let mut value_list = quote!();
+ value_list.append_separated(values.iter(), quote! { , });
+ quote! { _f(&[#value_list]); }
+ };
+
+ let name = &input.ident;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ quote! {
+ impl #impl_generics style_traits::SpecifiedValueInfo for #name #ty_generics
+ #where_clause
+ {
+ const SUPPORTED_TYPES: u8 = #types_value;
+
+ fn collect_completion_keywords(_f: &mut FnMut(&[&'static str])) {
+ #nested_collects
+ #append_values
+ }
+ }
+ }
+}
+
+/// Derive from the given fields. Return false if the fields is a Unit,
+/// true otherwise.
+fn derive_struct_fields<'a>(
+ fields: &'a Fields,
+ types: &mut Vec<&'a Type>,
+ values: &mut Vec<String>,
+) -> bool {
+ let fields = match *fields {
+ Fields::Unit => return false,
+ Fields::Named(ref fields) => fields.named.iter(),
+ Fields::Unnamed(ref fields) => fields.unnamed.iter(),
+ };
+ types.extend(fields.filter_map(|field| {
+ let info_attrs = cg::parse_field_attrs::<ValueInfoFieldAttrs>(field);
+ if let Some(other_values) = info_attrs.other_values {
+ for value in other_values.split(',') {
+ values.push(value.to_string());
+ }
+ }
+ let css_attrs = cg::parse_field_attrs::<CssFieldAttrs>(field);
+ if css_attrs.represents_keyword {
+ let ident = field
+ .ident
+ .as_ref()
+ .expect("only named field should use represents_keyword");
+ values.push(cg::to_css_identifier(&ident.to_string()).replace("_", "-"));
+ return None;
+ }
+ if let Some(if_empty) = css_attrs.if_empty {
+ values.push(if_empty);
+ }
+ if !css_attrs.skip {
+ Some(&field.ty)
+ } else {
+ None
+ }
+ }));
+ true
+}
+
+#[derive(Default, FromDeriveInput)]
+#[darling(attributes(value_info), default)]
+struct ValueInfoInputAttrs {
+ ty: Option<Ident>,
+ other_values: Option<String>,
+}
+
+#[derive(Default, FromVariant)]
+#[darling(attributes(value_info), default)]
+struct ValueInfoVariantAttrs {
+ starts_with_keyword: bool,
+ other_values: Option<String>,
+}
+
+#[derive(Default, FromField)]
+#[darling(attributes(value_info), default)]
+struct ValueInfoFieldAttrs {
+ other_values: Option<String>,
+}
diff --git a/servo/components/style_derive/to_animated_value.rs b/servo/components/style_derive/to_animated_value.rs
new file mode 100644
index 0000000000..45282f0c44
--- /dev/null
+++ b/servo/components/style_derive/to_animated_value.rs
@@ -0,0 +1,35 @@
+/* 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/. */
+
+use proc_macro2::TokenStream;
+use syn::DeriveInput;
+use synstructure::BindStyle;
+use to_computed_value;
+
+pub fn derive(input: DeriveInput) -> TokenStream {
+ let trait_impl = |from_body, to_body| {
+ quote! {
+ #[inline]
+ fn from_animated_value(from: Self::AnimatedValue) -> Self {
+ #from_body
+ }
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ #to_body
+ }
+ }
+ };
+
+ to_computed_value::derive_to_value(
+ input,
+ parse_quote!(crate::values::animated::ToAnimatedValue),
+ parse_quote!(AnimatedValue),
+ BindStyle::Move,
+ |_| Default::default(),
+ |binding| quote!(crate::values::animated::ToAnimatedValue::from_animated_value(#binding)),
+ |binding| quote!(crate::values::animated::ToAnimatedValue::to_animated_value(#binding)),
+ trait_impl,
+ )
+}
diff --git a/servo/components/style_derive/to_animated_zero.rs b/servo/components/style_derive/to_animated_zero.rs
new file mode 100644
index 0000000000..008e94cbcf
--- /dev/null
+++ b/servo/components/style_derive/to_animated_zero.rs
@@ -0,0 +1,65 @@
+/* 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/. */
+
+use crate::animate::{AnimationFieldAttrs, AnimationInputAttrs, AnimationVariantAttrs};
+use derive_common::cg;
+use proc_macro2::TokenStream;
+use quote::TokenStreamExt;
+use syn;
+use synstructure;
+
+pub fn derive(mut input: syn::DeriveInput) -> TokenStream {
+ let animation_input_attrs = cg::parse_input_attrs::<AnimationInputAttrs>(&input);
+ let no_bound = animation_input_attrs.no_bound.unwrap_or_default();
+ let mut where_clause = input.generics.where_clause.take();
+ for param in input.generics.type_params() {
+ if !no_bound.iter().any(|name| name.is_ident(&param.ident)) {
+ cg::add_predicate(
+ &mut where_clause,
+ parse_quote!(#param: crate::values::animated::ToAnimatedZero),
+ );
+ }
+ }
+
+ let to_body = synstructure::Structure::new(&input).each_variant(|variant| {
+ let attrs = cg::parse_variant_attrs_from_ast::<AnimationVariantAttrs>(&variant.ast());
+ if attrs.error {
+ return Some(quote! { Err(()) });
+ }
+ let (mapped, mapped_bindings) = cg::value(variant, "mapped");
+ let bindings_pairs = variant.bindings().iter().zip(mapped_bindings);
+ let mut computations = quote!();
+ computations.append_all(bindings_pairs.map(|(binding, mapped_binding)| {
+ let field_attrs = cg::parse_field_attrs::<AnimationFieldAttrs>(&binding.ast());
+ if field_attrs.constant {
+ quote! {
+ let #mapped_binding = std::clone::Clone::clone(#binding);
+ }
+ } else {
+ quote! {
+ let #mapped_binding =
+ crate::values::animated::ToAnimatedZero::to_animated_zero(#binding)?;
+ }
+ }
+ }));
+ computations.append_all(quote! { Ok(#mapped) });
+ Some(computations)
+ });
+ input.generics.where_clause = where_clause;
+
+ let name = &input.ident;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ quote! {
+ impl #impl_generics crate::values::animated::ToAnimatedZero for #name #ty_generics #where_clause {
+ #[allow(unused_variables)]
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ match *self {
+ #to_body
+ }
+ }
+ }
+ }
+}
diff --git a/servo/components/style_derive/to_computed_value.rs b/servo/components/style_derive/to_computed_value.rs
new file mode 100644
index 0000000000..5e0f595c6b
--- /dev/null
+++ b/servo/components/style_derive/to_computed_value.rs
@@ -0,0 +1,205 @@
+/* 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/. */
+
+use derive_common::cg;
+use proc_macro2::TokenStream;
+use syn::{DeriveInput, Ident, Path};
+use synstructure::{BindStyle, BindingInfo};
+
+pub fn derive_to_value(
+ mut input: DeriveInput,
+ trait_path: Path,
+ output_type_name: Ident,
+ bind_style: BindStyle,
+ // Returns whether to apply the field bound for a given item.
+ mut binding_attrs: impl FnMut(&BindingInfo) -> ToValueAttrs,
+ // Returns a token stream of the form: trait_path::from_foo(#binding)
+ mut call_from: impl FnMut(&BindingInfo) -> TokenStream,
+ mut call_to: impl FnMut(&BindingInfo) -> TokenStream,
+ // Returns a tokenstream of the form:
+ // fn from_function_syntax(foobar) -> Baz {
+ // #first_arg
+ // }
+ //
+ // fn to_function_syntax(foobar) -> Baz {
+ // #second_arg
+ // }
+ mut trait_impl: impl FnMut(TokenStream, TokenStream) -> TokenStream,
+) -> TokenStream {
+ let name = &input.ident;
+
+ let mut where_clause = input.generics.where_clause.take();
+ cg::propagate_clauses_to_output_type(
+ &mut where_clause,
+ &input.generics,
+ &trait_path,
+ &output_type_name,
+ );
+
+ let moves = match bind_style {
+ BindStyle::Move | BindStyle::MoveMut => true,
+ BindStyle::Ref | BindStyle::RefMut => false,
+ };
+
+ let params = input.generics.type_params().collect::<Vec<_>>();
+ for param in &params {
+ cg::add_predicate(&mut where_clause, parse_quote!(#param: #trait_path));
+ }
+
+ let computed_value_type = cg::fmap_trait_output(&input, &trait_path, &output_type_name);
+
+ let mut add_field_bound = |binding: &BindingInfo| {
+ let ty = &binding.ast().ty;
+
+ let output_type = cg::map_type_params(
+ ty,
+ &params,
+ &computed_value_type,
+ &mut |ident| parse_quote!(<#ident as #trait_path>::#output_type_name),
+ );
+
+ cg::add_predicate(
+ &mut where_clause,
+ parse_quote!(
+ #ty: #trait_path<#output_type_name = #output_type>
+ ),
+ );
+ };
+
+ let (to_body, from_body) = if params.is_empty() {
+ let mut s = synstructure::Structure::new(&input);
+ s.variants_mut().iter_mut().for_each(|v| {
+ v.bind_with(|_| bind_style);
+ });
+
+ for variant in s.variants() {
+ for binding in variant.bindings() {
+ let attrs = binding_attrs(&binding);
+ assert!(
+ !attrs.field_bound,
+ "It is default on a non-generic implementation",
+ );
+ if !attrs.no_field_bound {
+ // Add field bounds to all bindings except the manually
+ // excluded. This ensures the correctness of the clone() /
+ // move based implementation.
+ add_field_bound(binding);
+ }
+ }
+ }
+
+ let to_body = if moves {
+ quote! { self }
+ } else {
+ quote! { std::clone::Clone::clone(self) }
+ };
+
+ let from_body = if moves {
+ quote! { from }
+ } else {
+ quote! { std::clone::Clone::clone(from) }
+ };
+
+ (to_body, from_body)
+ } else {
+ let to_body = cg::fmap_match(&input, bind_style, |binding| {
+ let attrs = binding_attrs(&binding);
+ assert!(
+ !attrs.no_field_bound,
+ "It doesn't make sense on a generic implementation"
+ );
+ if attrs.field_bound {
+ add_field_bound(&binding);
+ }
+ call_to(&binding)
+ });
+
+ let from_body = cg::fmap_match(&input, bind_style, |binding| call_from(&binding));
+
+ let self_ = if moves {
+ quote! { self }
+ } else {
+ quote! { *self }
+ };
+ let from_ = if moves {
+ quote! { from }
+ } else {
+ quote! { *from }
+ };
+
+ let to_body = quote! {
+ match #self_ {
+ #to_body
+ }
+ };
+
+ let from_body = quote! {
+ match #from_ {
+ #from_body
+ }
+ };
+
+ (to_body, from_body)
+ };
+
+ input.generics.where_clause = where_clause;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ let impl_ = trait_impl(from_body, to_body);
+
+ quote! {
+ impl #impl_generics #trait_path for #name #ty_generics #where_clause {
+ type #output_type_name = #computed_value_type;
+
+ #impl_
+ }
+ }
+}
+
+pub fn derive(input: DeriveInput) -> TokenStream {
+ let trait_impl = |from_body, to_body| {
+ quote! {
+ #[inline]
+ fn from_computed_value(from: &Self::ComputedValue) -> Self {
+ #from_body
+ }
+
+ #[allow(unused_variables)]
+ #[inline]
+ fn to_computed_value(&self, context: &crate::values::computed::Context) -> Self::ComputedValue {
+ #to_body
+ }
+ }
+ };
+
+ derive_to_value(
+ input,
+ parse_quote!(crate::values::computed::ToComputedValue),
+ parse_quote!(ComputedValue),
+ BindStyle::Ref,
+ |binding| {
+ let attrs = cg::parse_field_attrs::<ComputedValueAttrs>(&binding.ast());
+ ToValueAttrs {
+ field_bound: attrs.field_bound,
+ no_field_bound: attrs.no_field_bound,
+ }
+ },
+ |binding| quote!(crate::values::computed::ToComputedValue::from_computed_value(#binding)),
+ |binding| quote!(crate::values::computed::ToComputedValue::to_computed_value(#binding, context)),
+ trait_impl,
+ )
+}
+
+#[derive(Default)]
+pub struct ToValueAttrs {
+ pub field_bound: bool,
+ pub no_field_bound: bool,
+}
+
+#[derive(Default, FromField)]
+#[darling(attributes(compute), default)]
+struct ComputedValueAttrs {
+ field_bound: bool,
+ no_field_bound: bool,
+}
diff --git a/servo/components/style_derive/to_css.rs b/servo/components/style_derive/to_css.rs
new file mode 100644
index 0000000000..aa33536648
--- /dev/null
+++ b/servo/components/style_derive/to_css.rs
@@ -0,0 +1,396 @@
+/* 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/. */
+
+use darling::util::Override;
+use derive_common::cg;
+use proc_macro2::{Span, TokenStream};
+use quote::{ToTokens, TokenStreamExt};
+use syn::{self, Data, Ident, Path, WhereClause};
+use synstructure::{BindingInfo, Structure, VariantInfo};
+
+fn derive_bitflags(input: &syn::DeriveInput, bitflags: &CssBitflagAttrs) -> TokenStream {
+ let name = &input.ident;
+ let mut body = TokenStream::new();
+ for (rust_name, css_name) in bitflags.single_flags() {
+ let rust_ident = Ident::new(&rust_name, Span::call_site());
+ body.append_all(quote! {
+ if *self == Self::#rust_ident {
+ return dest.write_str(#css_name);
+ }
+ });
+ }
+
+ body.append_all(quote! {
+ let mut has_any = false;
+ });
+
+ if bitflags.overlapping_bits {
+ body.append_all(quote! {
+ let mut serialized = Self::empty();
+ });
+ }
+
+ for (rust_name, css_name) in bitflags.mixed_flags() {
+ let rust_ident = Ident::new(&rust_name, Span::call_site());
+ let serialize = quote! {
+ if has_any {
+ dest.write_char(' ')?;
+ }
+ has_any = true;
+ dest.write_str(#css_name)?;
+ };
+ if bitflags.overlapping_bits {
+ body.append_all(quote! {
+ if self.contains(Self::#rust_ident) && !serialized.intersects(Self::#rust_ident) {
+ #serialize
+ serialized.insert(Self::#rust_ident);
+ }
+ });
+ } else {
+ body.append_all(quote! {
+ if self.intersects(Self::#rust_ident) {
+ #serialize
+ }
+ });
+ }
+ }
+
+ body.append_all(quote! {
+ Ok(())
+ });
+
+ quote! {
+ impl style_traits::ToCss for #name {
+ #[allow(unused_variables)]
+ #[inline]
+ fn to_css<W>(
+ &self,
+ dest: &mut style_traits::CssWriter<W>,
+ ) -> std::fmt::Result
+ where
+ W: std::fmt::Write,
+ {
+ #body
+ }
+ }
+ }
+}
+
+pub fn derive(mut input: syn::DeriveInput) -> TokenStream {
+ let mut where_clause = input.generics.where_clause.take();
+ for param in input.generics.type_params() {
+ cg::add_predicate(&mut where_clause, parse_quote!(#param: style_traits::ToCss));
+ }
+
+ let input_attrs = cg::parse_input_attrs::<CssInputAttrs>(&input);
+ if matches!(input.data, Data::Enum(..)) || input_attrs.bitflags.is_some() {
+ assert!(
+ input_attrs.function.is_none(),
+ "#[css(function)] is not allowed on enums or bitflags"
+ );
+ assert!(
+ !input_attrs.comma,
+ "#[css(comma)] is not allowed on enums or bitflags"
+ );
+ }
+
+ if let Some(ref bitflags) = input_attrs.bitflags {
+ assert!(
+ !input_attrs.derive_debug,
+ "Bitflags can derive debug on their own"
+ );
+ assert!(where_clause.is_none(), "Generic bitflags?");
+ return derive_bitflags(&input, bitflags);
+ }
+
+ let match_body = {
+ let s = Structure::new(&input);
+ s.each_variant(|variant| derive_variant_arm(variant, &mut where_clause))
+ };
+ input.generics.where_clause = where_clause;
+
+ let name = &input.ident;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ let mut impls = quote! {
+ impl #impl_generics style_traits::ToCss for #name #ty_generics #where_clause {
+ #[allow(unused_variables)]
+ #[inline]
+ fn to_css<W>(
+ &self,
+ dest: &mut style_traits::CssWriter<W>,
+ ) -> std::fmt::Result
+ where
+ W: std::fmt::Write,
+ {
+ match *self {
+ #match_body
+ }
+ }
+ }
+ };
+
+ if input_attrs.derive_debug {
+ impls.append_all(quote! {
+ impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ style_traits::ToCss::to_css(
+ self,
+ &mut style_traits::CssWriter::new(f),
+ )
+ }
+ }
+ });
+ }
+
+ impls
+}
+
+fn derive_variant_arm(variant: &VariantInfo, generics: &mut Option<WhereClause>) -> TokenStream {
+ let bindings = variant.bindings();
+ let identifier = cg::to_css_identifier(&variant.ast().ident.to_string());
+ let ast = variant.ast();
+ let variant_attrs = cg::parse_variant_attrs_from_ast::<CssVariantAttrs>(&ast);
+ let separator = if variant_attrs.comma { ", " } else { " " };
+
+ if variant_attrs.skip {
+ return quote!(Ok(()));
+ }
+ if variant_attrs.dimension {
+ assert_eq!(bindings.len(), 1);
+ assert!(
+ variant_attrs.function.is_none() && variant_attrs.keyword.is_none(),
+ "That makes no sense"
+ );
+ }
+
+ let mut expr = if let Some(keyword) = variant_attrs.keyword {
+ assert!(bindings.is_empty());
+ quote! {
+ std::fmt::Write::write_str(dest, #keyword)
+ }
+ } else if !bindings.is_empty() {
+ derive_variant_fields_expr(bindings, generics, separator)
+ } else {
+ quote! {
+ std::fmt::Write::write_str(dest, #identifier)
+ }
+ };
+
+ if variant_attrs.dimension {
+ expr = quote! {
+ #expr?;
+ std::fmt::Write::write_str(dest, #identifier)
+ }
+ } else if let Some(function) = variant_attrs.function {
+ let mut identifier = function.explicit().map_or(identifier, |name| name);
+ identifier.push('(');
+ expr = quote! {
+ std::fmt::Write::write_str(dest, #identifier)?;
+ #expr?;
+ std::fmt::Write::write_str(dest, ")")
+ }
+ }
+ expr
+}
+
+fn derive_variant_fields_expr(
+ bindings: &[BindingInfo],
+ where_clause: &mut Option<WhereClause>,
+ separator: &str,
+) -> TokenStream {
+ let mut iter = bindings
+ .iter()
+ .filter_map(|binding| {
+ let attrs = cg::parse_field_attrs::<CssFieldAttrs>(&binding.ast());
+ if attrs.skip {
+ return None;
+ }
+ Some((binding, attrs))
+ })
+ .peekable();
+
+ let (first, attrs) = match iter.next() {
+ Some(pair) => pair,
+ None => return quote! { Ok(()) },
+ };
+ if attrs.field_bound {
+ let ty = &first.ast().ty;
+ // TODO(emilio): IntoIterator might not be enough for every type of
+ // iterable thing (like ArcSlice<> or what not). We might want to expose
+ // an `item = "T"` attribute to handle that in the future.
+ let predicate = if attrs.iterable {
+ parse_quote!(<#ty as IntoIterator>::Item: style_traits::ToCss)
+ } else {
+ parse_quote!(#ty: style_traits::ToCss)
+ };
+ cg::add_predicate(where_clause, predicate);
+ }
+ if !attrs.iterable && iter.peek().is_none() {
+ let mut expr = quote! { style_traits::ToCss::to_css(#first, dest) };
+ if let Some(condition) = attrs.skip_if {
+ expr = quote! {
+ if !#condition(#first) {
+ #expr
+ }
+ }
+ }
+
+ if let Some(condition) = attrs.contextual_skip_if {
+ expr = quote! {
+ if !#condition(#(#bindings), *) {
+ #expr
+ }
+ }
+ }
+ return expr;
+ }
+
+ let mut expr = derive_single_field_expr(first, attrs, where_clause, bindings);
+ for (binding, attrs) in iter {
+ derive_single_field_expr(binding, attrs, where_clause, bindings).to_tokens(&mut expr)
+ }
+
+ quote! {{
+ let mut writer = style_traits::values::SequenceWriter::new(dest, #separator);
+ #expr
+ Ok(())
+ }}
+}
+
+fn derive_single_field_expr(
+ field: &BindingInfo,
+ attrs: CssFieldAttrs,
+ where_clause: &mut Option<WhereClause>,
+ bindings: &[BindingInfo],
+) -> TokenStream {
+ let mut expr = if attrs.iterable {
+ if let Some(if_empty) = attrs.if_empty {
+ return quote! {
+ {
+ let mut iter = #field.iter().peekable();
+ if iter.peek().is_none() {
+ writer.raw_item(#if_empty)?;
+ } else {
+ for item in iter {
+ writer.item(&item)?;
+ }
+ }
+ }
+ };
+ }
+ quote! {
+ for item in #field.iter() {
+ writer.item(&item)?;
+ }
+ }
+ } else if attrs.represents_keyword {
+ let ident = field
+ .ast()
+ .ident
+ .as_ref()
+ .expect("Unnamed field with represents_keyword?");
+ let ident = cg::to_css_identifier(&ident.to_string()).replace("_", "-");
+ quote! {
+ if *#field {
+ writer.raw_item(#ident)?;
+ }
+ }
+ } else {
+ if attrs.field_bound {
+ let ty = &field.ast().ty;
+ cg::add_predicate(where_clause, parse_quote!(#ty: style_traits::ToCss));
+ }
+ quote! { writer.item(#field)?; }
+ };
+
+ if let Some(condition) = attrs.skip_if {
+ expr = quote! {
+ if !#condition(#field) {
+ #expr
+ }
+ }
+ }
+
+ if let Some(condition) = attrs.contextual_skip_if {
+ expr = quote! {
+ if !#condition(#(#bindings), *) {
+ #expr
+ }
+ }
+ }
+
+ expr
+}
+
+#[derive(Default, FromMeta)]
+#[darling(default)]
+pub struct CssBitflagAttrs {
+ /// Flags that can only go on their own, comma-separated.
+ pub single: Option<String>,
+ /// Flags that can go mixed with each other, comma-separated.
+ pub mixed: Option<String>,
+ /// Extra validation of the resulting mixed flags.
+ pub validate_mixed: Option<Path>,
+ /// Whether there are overlapping bits we need to take care of when
+ /// serializing.
+ pub overlapping_bits: bool,
+}
+
+impl CssBitflagAttrs {
+ /// Returns a vector of (rust_name, css_name) of a given flag list.
+ fn names(s: &Option<String>) -> Vec<(String, String)> {
+ let s = match s {
+ Some(s) => s,
+ None => return vec![],
+ };
+ s.split(',')
+ .map(|css_name| (cg::to_scream_case(css_name), css_name.to_owned()))
+ .collect()
+ }
+
+ pub fn single_flags(&self) -> Vec<(String, String)> {
+ Self::names(&self.single)
+ }
+
+ pub fn mixed_flags(&self) -> Vec<(String, String)> {
+ Self::names(&self.mixed)
+ }
+}
+
+#[derive(Default, FromDeriveInput)]
+#[darling(attributes(css), default)]
+pub struct CssInputAttrs {
+ pub derive_debug: bool,
+ // Here because structs variants are also their whole type definition.
+ pub function: Option<Override<String>>,
+ // Here because structs variants are also their whole type definition.
+ pub comma: bool,
+ pub bitflags: Option<CssBitflagAttrs>,
+}
+
+#[derive(Default, FromVariant)]
+#[darling(attributes(css), default)]
+pub struct CssVariantAttrs {
+ pub function: Option<Override<String>>,
+ // Here because structs variants are also their whole type definition.
+ pub derive_debug: bool,
+ pub comma: bool,
+ pub bitflags: Option<CssBitflagAttrs>,
+ pub dimension: bool,
+ pub keyword: Option<String>,
+ pub skip: bool,
+}
+
+#[derive(Default, FromField)]
+#[darling(attributes(css), default)]
+pub struct CssFieldAttrs {
+ pub if_empty: Option<String>,
+ pub field_bound: bool,
+ pub iterable: bool,
+ pub skip: bool,
+ pub represents_keyword: bool,
+ pub contextual_skip_if: Option<Path>,
+ pub skip_if: Option<Path>,
+}
diff --git a/servo/components/style_derive/to_resolved_value.rs b/servo/components/style_derive/to_resolved_value.rs
new file mode 100644
index 0000000000..e049f91152
--- /dev/null
+++ b/servo/components/style_derive/to_resolved_value.rs
@@ -0,0 +1,52 @@
+/* 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/. */
+
+use derive_common::cg;
+use proc_macro2::TokenStream;
+use syn::DeriveInput;
+use synstructure::BindStyle;
+use to_computed_value;
+
+pub fn derive(input: DeriveInput) -> TokenStream {
+ let trait_impl = |from_body, to_body| {
+ quote! {
+ #[inline]
+ fn from_resolved_value(from: Self::ResolvedValue) -> Self {
+ #from_body
+ }
+
+ #[inline]
+ fn to_resolved_value(
+ self,
+ context: &crate::values::resolved::Context,
+ ) -> Self::ResolvedValue {
+ #to_body
+ }
+ }
+ };
+
+ to_computed_value::derive_to_value(
+ input,
+ parse_quote!(crate::values::resolved::ToResolvedValue),
+ parse_quote!(ResolvedValue),
+ BindStyle::Move,
+ |binding| {
+ let attrs = cg::parse_field_attrs::<ResolvedValueAttrs>(&binding.ast());
+ to_computed_value::ToValueAttrs {
+ field_bound: attrs.field_bound,
+ no_field_bound: attrs.no_field_bound,
+ }
+ },
+ |binding| quote!(crate::values::resolved::ToResolvedValue::from_resolved_value(#binding)),
+ |binding| quote!(crate::values::resolved::ToResolvedValue::to_resolved_value(#binding, context)),
+ trait_impl,
+ )
+}
+
+#[derive(Default, FromField)]
+#[darling(attributes(resolve), default)]
+struct ResolvedValueAttrs {
+ field_bound: bool,
+ no_field_bound: bool,
+}
diff --git a/servo/components/style_traits/Cargo.toml b/servo/components/style_traits/Cargo.toml
new file mode 100644
index 0000000000..81d6e2bdf4
--- /dev/null
+++ b/servo/components/style_traits/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "style_traits"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+publish = false
+
+[lib]
+name = "style_traits"
+path = "lib.rs"
+
+[features]
+servo = ["servo_atoms", "cssparser/serde", "webrender_api", "servo_url", "euclid/serde"]
+gecko = ["nsstring"]
+
+[dependencies]
+app_units = "0.7"
+bitflags = "2"
+cssparser = "0.33"
+euclid = "0.22"
+lazy_static = "1"
+malloc_size_of = { path = "../malloc_size_of" }
+malloc_size_of_derive = { path = "../../../xpcom/rust/malloc_size_of_derive" }
+nsstring = {path = "../../../xpcom/rust/nsstring/", optional = true}
+selectors = { path = "../selectors" }
+serde = "1.0"
+servo_arc = { path = "../servo_arc" }
+servo_atoms = { path = "../atoms", optional = true }
+servo_url = { path = "../url", optional = true }
+to_shmem = { path = "../to_shmem" }
+to_shmem_derive = { path = "../to_shmem_derive" }
+webrender_api = { git = "https://github.com/servo/webrender", optional = true }
diff --git a/servo/components/style_traits/arc_slice.rs b/servo/components/style_traits/arc_slice.rs
new file mode 100644
index 0000000000..1721a33f48
--- /dev/null
+++ b/servo/components/style_traits/arc_slice.rs
@@ -0,0 +1,162 @@
+/* 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/. */
+
+//! A thin atomically-reference-counted slice.
+
+use serde::de::{Deserialize, Deserializer};
+use serde::ser::{Serialize, Serializer};
+use servo_arc::ThinArc;
+use std::ops::Deref;
+use std::ptr::NonNull;
+use std::{iter, mem};
+
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalSizeOf};
+
+/// A canary that we stash in ArcSlices.
+///
+/// Given we cannot use a zero-sized-type for the header, since well, C++
+/// doesn't have zsts, and we want to use cbindgen for this type, we may as well
+/// assert some sanity at runtime.
+///
+/// We use an u64, to guarantee that we can use a single singleton for every
+/// empty slice, even if the types they hold are aligned differently.
+const ARC_SLICE_CANARY: u64 = 0xf3f3f3f3f3f3f3f3;
+
+/// A wrapper type for a refcounted slice using ThinArc.
+#[repr(C)]
+#[derive(Debug, Eq, PartialEq, ToShmem)]
+pub struct ArcSlice<T>(#[shmem(field_bound)] ThinArc<u64, T>);
+
+impl<T> Deref for ArcSlice<T> {
+ type Target = [T];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ debug_assert_eq!(self.0.header, ARC_SLICE_CANARY);
+ self.0.slice()
+ }
+}
+
+impl<T> Clone for ArcSlice<T> {
+ fn clone(&self) -> Self {
+ ArcSlice(self.0.clone())
+ }
+}
+
+lazy_static! {
+ // ThinArc doesn't support alignments greater than align_of::<u64>.
+ static ref EMPTY_ARC_SLICE: ArcSlice<u64> = {
+ ArcSlice::from_iter_leaked(iter::empty())
+ };
+}
+
+impl<T> Default for ArcSlice<T> {
+ #[allow(unsafe_code)]
+ fn default() -> Self {
+ debug_assert!(
+ mem::align_of::<T>() <= mem::align_of::<u64>(),
+ "Need to increase the alignment of EMPTY_ARC_SLICE"
+ );
+ unsafe {
+ let empty: ArcSlice<_> = EMPTY_ARC_SLICE.clone();
+ let empty: Self = mem::transmute(empty);
+ debug_assert_eq!(empty.len(), 0);
+ empty
+ }
+ }
+}
+
+impl<T: Serialize> Serialize for ArcSlice<T> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ self.deref().serialize(serializer)
+ }
+}
+
+impl<'de, T: Deserialize<'de>> Deserialize<'de> for ArcSlice<T> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let r = Vec::deserialize(deserializer)?;
+ Ok(ArcSlice::from_iter(r.into_iter()))
+ }
+}
+
+impl<T> ArcSlice<T> {
+ /// Creates an Arc for a slice using the given iterator to generate the
+ /// slice.
+ #[inline]
+ pub fn from_iter<I>(items: I) -> Self
+ where
+ I: Iterator<Item = T> + ExactSizeIterator,
+ {
+ if items.len() == 0 {
+ return Self::default();
+ }
+ ArcSlice(ThinArc::from_header_and_iter(ARC_SLICE_CANARY, items))
+ }
+
+ /// Creates an Arc for a slice using the given iterator to generate the
+ /// slice, and marks the arc as intentionally leaked from the refcount
+ /// logging point of view.
+ #[inline]
+ pub fn from_iter_leaked<I>(items: I) -> Self
+ where
+ I: Iterator<Item = T> + ExactSizeIterator,
+ {
+ let arc = ThinArc::from_header_and_iter(ARC_SLICE_CANARY, items);
+ arc.mark_as_intentionally_leaked();
+ ArcSlice(arc)
+ }
+
+ /// Creates a value that can be passed via FFI, and forgets this value
+ /// altogether.
+ #[inline]
+ #[allow(unsafe_code)]
+ pub fn forget(self) -> ForgottenArcSlicePtr<T> {
+ let ret = unsafe {
+ ForgottenArcSlicePtr(NonNull::new_unchecked(
+ self.0.raw_ptr() as *const _ as *mut _
+ ))
+ };
+ mem::forget(self);
+ ret
+ }
+
+ /// Leaks an empty arc slice pointer, and returns it. Only to be used to
+ /// construct ArcSlices from FFI.
+ #[inline]
+ pub fn leaked_empty_ptr() -> *mut std::os::raw::c_void {
+ let empty: ArcSlice<_> = EMPTY_ARC_SLICE.clone();
+ let ptr = empty.0.raw_ptr();
+ std::mem::forget(empty);
+ ptr as *mut _
+ }
+
+ /// Returns whether there's only one reference to this ArcSlice.
+ pub fn is_unique(&self) -> bool {
+ self.0.is_unique()
+ }
+}
+
+impl<T: MallocSizeOf> MallocUnconditionalSizeOf for ArcSlice<T> {
+ #[allow(unsafe_code)]
+ fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ let mut size = unsafe { ops.malloc_size_of(self.0.heap_ptr()) };
+ for el in self.iter() {
+ size += el.size_of(ops);
+ }
+ size
+ }
+}
+
+/// The inner pointer of an ArcSlice<T>, to be sent via FFI.
+/// The type of the pointer is a bit of a lie, we just want to preserve the type
+/// but these pointers cannot be constructed outside of this crate, so we're
+/// good.
+#[repr(C)]
+pub struct ForgottenArcSlicePtr<T>(NonNull<T>);
diff --git a/servo/components/style_traits/dom.rs b/servo/components/style_traits/dom.rs
new file mode 100644
index 0000000000..03d5264abf
--- /dev/null
+++ b/servo/components/style_traits/dom.rs
@@ -0,0 +1,26 @@
+/* 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/. */
+
+//! Types used to access the DOM from style calculation.
+
+/// An opaque handle to a node, which, unlike UnsafeNode, cannot be transformed
+/// back into a non-opaque representation. The only safe operation that can be
+/// performed on this node is to compare it to another opaque handle or to another
+/// OpaqueNode.
+///
+/// Layout and Graphics use this to safely represent nodes for comparison purposes.
+/// Because the script task's GC does not trace layout, node data cannot be safely stored in layout
+/// data structures. Also, layout code tends to be faster when the DOM is not being accessed, for
+/// locality reasons. Using `OpaqueNode` enforces this invariant.
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+pub struct OpaqueNode(pub usize);
+
+impl OpaqueNode {
+ /// Returns the address of this node, for debugging purposes.
+ #[inline]
+ pub fn id(&self) -> usize {
+ self.0
+ }
+}
diff --git a/servo/components/style_traits/lib.rs b/servo/components/style_traits/lib.rs
new file mode 100644
index 0000000000..9bb2b3c655
--- /dev/null
+++ b/servo/components/style_traits/lib.rs
@@ -0,0 +1,295 @@
+/* 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/. */
+
+//! This module contains shared types and messages for use by devtools/script.
+//! The traits are here instead of in script so that the devtools crate can be
+//! modified independently of the rest of Servo.
+
+#![crate_name = "style_traits"]
+#![crate_type = "rlib"]
+#![deny(unsafe_code, missing_docs)]
+
+extern crate app_units;
+#[macro_use]
+extern crate bitflags;
+extern crate cssparser;
+extern crate euclid;
+#[macro_use]
+extern crate lazy_static;
+extern crate malloc_size_of;
+#[macro_use]
+extern crate malloc_size_of_derive;
+extern crate nsstring;
+extern crate selectors;
+#[macro_use]
+extern crate serde;
+extern crate servo_arc;
+#[cfg(feature = "servo")]
+extern crate servo_atoms;
+#[cfg(feature = "servo")]
+extern crate servo_url;
+extern crate to_shmem;
+#[macro_use]
+extern crate to_shmem_derive;
+#[cfg(feature = "servo")]
+extern crate webrender_api;
+#[cfg(feature = "servo")]
+pub use webrender_api::units::DevicePixel;
+
+use cssparser::{CowRcStr, Token};
+use selectors::parser::SelectorParseErrorKind;
+#[cfg(feature = "servo")]
+use servo_atoms::Atom;
+
+/// One hardware pixel.
+///
+/// This unit corresponds to the smallest addressable element of the display hardware.
+#[cfg(not(feature = "servo"))]
+#[derive(Clone, Copy, Debug)]
+pub enum DevicePixel {}
+
+/// Represents a mobile style pinch zoom factor.
+/// TODO(gw): Once WR supports pinch zoom, use a type directly from webrender_api.
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize, MallocSizeOf))]
+pub struct PinchZoomFactor(f32);
+
+impl PinchZoomFactor {
+ /// Construct a new pinch zoom factor.
+ pub fn new(scale: f32) -> PinchZoomFactor {
+ PinchZoomFactor(scale)
+ }
+
+ /// Get the pinch zoom factor as an untyped float.
+ pub fn get(&self) -> f32 {
+ self.0
+ }
+}
+
+/// One CSS "px" in the coordinate system of the "initial viewport":
+/// <http://www.w3.org/TR/css-device-adapt/#initial-viewport>
+///
+/// `CSSPixel` is equal to `DeviceIndependentPixel` times a "page zoom" factor controlled by the user. This is
+/// the desktop-style "full page" zoom that enlarges content but then reflows the layout viewport
+/// so it still exactly fits the visible area.
+///
+/// At the default zoom level of 100%, one `CSSPixel` is equal to one `DeviceIndependentPixel`. However, if the
+/// document is zoomed in or out then this scale may be larger or smaller.
+#[derive(Clone, Copy, Debug)]
+pub enum CSSPixel {}
+
+// In summary, the hierarchy of pixel units and the factors to convert from one to the next:
+//
+// DevicePixel
+// / hidpi_ratio => DeviceIndependentPixel
+// / desktop_zoom => CSSPixel
+
+pub mod arc_slice;
+pub mod dom;
+pub mod specified_value_info;
+#[macro_use]
+pub mod values;
+pub mod owned_slice;
+pub mod owned_str;
+
+pub use crate::specified_value_info::{CssType, KeywordsCollectFn, SpecifiedValueInfo};
+pub use crate::values::{
+ Comma, CommaWithSpace, CssWriter, OneOrMoreSeparated, Separator, Space, ToCss,
+};
+
+/// The error type for all CSS parsing routines.
+pub type ParseError<'i> = cssparser::ParseError<'i, StyleParseErrorKind<'i>>;
+
+/// Error in property value parsing
+pub type ValueParseError<'i> = cssparser::ParseError<'i, ValueParseErrorKind<'i>>;
+
+#[derive(Clone, Debug, PartialEq)]
+/// Errors that can be encountered while parsing CSS values.
+pub enum StyleParseErrorKind<'i> {
+ /// A bad URL token in a DVB.
+ BadUrlInDeclarationValueBlock(CowRcStr<'i>),
+ /// A bad string token in a DVB.
+ BadStringInDeclarationValueBlock(CowRcStr<'i>),
+ /// Unexpected closing parenthesis in a DVB.
+ UnbalancedCloseParenthesisInDeclarationValueBlock,
+ /// Unexpected closing bracket in a DVB.
+ UnbalancedCloseSquareBracketInDeclarationValueBlock,
+ /// Unexpected closing curly bracket in a DVB.
+ UnbalancedCloseCurlyBracketInDeclarationValueBlock,
+ /// A property declaration value had input remaining after successfully parsing.
+ PropertyDeclarationValueNotExhausted,
+ /// An unexpected dimension token was encountered.
+ UnexpectedDimension(CowRcStr<'i>),
+ /// Missing or invalid media feature name.
+ MediaQueryExpectedFeatureName(CowRcStr<'i>),
+ /// Missing or invalid media feature value.
+ MediaQueryExpectedFeatureValue,
+ /// A media feature range operator was not expected.
+ MediaQueryUnexpectedOperator,
+ /// min- or max- properties must have a value.
+ RangedExpressionWithNoValue,
+ /// A function was encountered that was not expected.
+ UnexpectedFunction(CowRcStr<'i>),
+ /// Error encountered parsing a @property's `syntax` descriptor
+ PropertySyntaxField(PropertySyntaxParseError),
+ /// @namespace must be before any rule but @charset and @import
+ UnexpectedNamespaceRule,
+ /// @import must be before any rule but @charset
+ UnexpectedImportRule,
+ /// @import rules are disallowed in the parser.
+ DisallowedImportRule,
+ /// Unexpected @charset rule encountered.
+ UnexpectedCharsetRule,
+ /// The @property `<custom-property-name>` must start with `--`
+ UnexpectedIdent(CowRcStr<'i>),
+ /// A placeholder for many sources of errors that require more specific variants.
+ UnspecifiedError,
+ /// An unexpected token was found within a namespace rule.
+ UnexpectedTokenWithinNamespace(Token<'i>),
+ /// An error was encountered while parsing a property value.
+ ValueError(ValueParseErrorKind<'i>),
+ /// An error was encountered while parsing a selector
+ SelectorError(SelectorParseErrorKind<'i>),
+ /// The property declaration was for an unknown property.
+ UnknownProperty(CowRcStr<'i>),
+ /// The property declaration was for a disabled experimental property.
+ ExperimentalProperty,
+ /// The property declaration contained an invalid color value.
+ InvalidColor(CowRcStr<'i>, Token<'i>),
+ /// The property declaration contained an invalid filter value.
+ InvalidFilter(CowRcStr<'i>, Token<'i>),
+ /// The property declaration contained an invalid value.
+ OtherInvalidValue(CowRcStr<'i>),
+ /// The declaration contained an animation property, and we were parsing
+ /// this as a keyframe block (so that property should be ignored).
+ ///
+ /// See: https://drafts.csswg.org/css-animations/#keyframes
+ AnimationPropertyInKeyframeBlock,
+ /// The property is not allowed within a page rule.
+ NotAllowedInPageRule,
+}
+
+impl<'i> From<ValueParseErrorKind<'i>> for StyleParseErrorKind<'i> {
+ fn from(this: ValueParseErrorKind<'i>) -> Self {
+ StyleParseErrorKind::ValueError(this)
+ }
+}
+
+impl<'i> From<SelectorParseErrorKind<'i>> for StyleParseErrorKind<'i> {
+ fn from(this: SelectorParseErrorKind<'i>) -> Self {
+ StyleParseErrorKind::SelectorError(this)
+ }
+}
+
+/// Specific errors that can be encountered while parsing property values.
+#[derive(Clone, Debug, PartialEq)]
+pub enum ValueParseErrorKind<'i> {
+ /// An invalid token was encountered while parsing a color value.
+ InvalidColor(Token<'i>),
+ /// An invalid filter value was encountered.
+ InvalidFilter(Token<'i>),
+}
+
+impl<'i> StyleParseErrorKind<'i> {
+ /// Create an InvalidValue parse error
+ pub fn new_invalid<S>(name: S, value_error: ParseError<'i>) -> ParseError<'i>
+ where
+ S: Into<CowRcStr<'i>>,
+ {
+ let name = name.into();
+ let variant = match value_error.kind {
+ cssparser::ParseErrorKind::Custom(StyleParseErrorKind::ValueError(e)) => match e {
+ ValueParseErrorKind::InvalidColor(token) => {
+ StyleParseErrorKind::InvalidColor(name, token)
+ },
+ ValueParseErrorKind::InvalidFilter(token) => {
+ StyleParseErrorKind::InvalidFilter(name, token)
+ },
+ },
+ _ => StyleParseErrorKind::OtherInvalidValue(name),
+ };
+ cssparser::ParseError {
+ kind: cssparser::ParseErrorKind::Custom(variant),
+ location: value_error.location,
+ }
+ }
+}
+
+/// Errors that can be encountered while parsing the @property rule's syntax descriptor.
+#[derive(Clone, Debug, PartialEq)]
+pub enum PropertySyntaxParseError {
+ /// The string's length was 0.
+ EmptyInput,
+ /// A non-whitespace, non-pipe character was fount after parsing a component.
+ ExpectedPipeBetweenComponents,
+ /// The start of an identifier was expected but not found.
+ ///
+ /// <https://drafts.csswg.org/css-syntax-3/#name-start-code-point>
+ InvalidNameStart,
+ /// The name is not a valid `<ident>`.
+ InvalidName,
+ /// The data type name was not closed.
+ ///
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-data-type-name>
+ UnclosedDataTypeName,
+ /// The next byte was expected while parsing, but EOF was found instead.
+ UnexpectedEOF,
+ /// The data type is not a supported syntax component name.
+ ///
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#supported-names>
+ UnknownDataTypeName,
+}
+
+bitflags! {
+ /// The mode to use when parsing values.
+ #[derive(Clone, Copy, Eq, PartialEq)]
+ #[repr(C)]
+ pub struct ParsingMode: u8 {
+ /// In CSS; lengths must have units, except for zero values, where the unit can be omitted.
+ /// <https://www.w3.org/TR/css3-values/#lengths>
+ const DEFAULT = 0;
+ /// In SVG; a coordinate or length value without a unit identifier (e.g., "25") is assumed
+ /// to be in user units (px).
+ /// <https://www.w3.org/TR/SVG/coords.html#Units>
+ const ALLOW_UNITLESS_LENGTH = 1;
+ /// In SVG; out-of-range values are not treated as an error in parsing.
+ /// <https://www.w3.org/TR/SVG/implnote.html#RangeClamping>
+ const ALLOW_ALL_NUMERIC_VALUES = 1 << 1;
+ /// In CSS Properties and Values, the initial value must be computationally
+ /// independent.
+ /// <https://drafts.css-houdini.org/css-properties-values-api-1/#ref-for-computationally-independent%E2%91%A0>
+ const DISALLOW_FONT_RELATIVE = 1 << 2;
+ }
+}
+
+impl ParsingMode {
+ /// Whether the parsing mode allows unitless lengths for non-zero values to be intpreted as px.
+ #[inline]
+ pub fn allows_unitless_lengths(&self) -> bool {
+ self.intersects(ParsingMode::ALLOW_UNITLESS_LENGTH)
+ }
+
+ /// Whether the parsing mode allows all numeric values.
+ #[inline]
+ pub fn allows_all_numeric_values(&self) -> bool {
+ self.intersects(ParsingMode::ALLOW_ALL_NUMERIC_VALUES)
+ }
+
+ /// Whether the parsing mode allows font-relative units.
+ #[inline]
+ pub fn allows_font_relative_lengths(&self) -> bool {
+ !self.intersects(ParsingMode::DISALLOW_FONT_RELATIVE)
+ }
+}
+
+#[cfg(feature = "servo")]
+/// Speculatively execute paint code in the worklet thread pool.
+pub trait SpeculativePainter: Send + Sync {
+ /// <https://drafts.css-houdini.org/css-paint-api/#draw-a-paint-image>
+ fn speculatively_draw_a_paint_image(
+ &self,
+ properties: Vec<(Atom, String)>,
+ arguments: Vec<String>,
+ );
+}
diff --git a/servo/components/style_traits/owned_slice.rs b/servo/components/style_traits/owned_slice.rs
new file mode 100644
index 0000000000..36ba3162e5
--- /dev/null
+++ b/servo/components/style_traits/owned_slice.rs
@@ -0,0 +1,198 @@
+/* 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/. */
+
+#![allow(unsafe_code)]
+
+//! A replacement for `Box<[T]>` that cbindgen can understand.
+
+use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps};
+use serde::de::{Deserialize, Deserializer};
+use serde::ser::{Serialize, Serializer};
+use std::marker::PhantomData;
+use std::ops::{Deref, DerefMut};
+use std::ptr::NonNull;
+use std::{fmt, iter, mem, slice};
+use to_shmem::{self, SharedMemoryBuilder, ToShmem};
+
+/// A struct that basically replaces a `Box<[T]>`, but which cbindgen can
+/// understand.
+///
+/// We could rely on the struct layout of `Box<[T]>` per:
+///
+/// https://github.com/rust-lang/unsafe-code-guidelines/blob/master/reference/src/layout/pointers.md
+///
+/// But handling fat pointers with cbindgen both in structs and argument
+/// positions more generally is a bit tricky.
+///
+/// cbindgen:derive-eq=false
+/// cbindgen:derive-neq=false
+#[repr(C)]
+pub struct OwnedSlice<T: Sized> {
+ ptr: NonNull<T>,
+ len: usize,
+ _phantom: PhantomData<T>,
+}
+
+impl<T: Sized> Default for OwnedSlice<T> {
+ #[inline]
+ fn default() -> Self {
+ Self {
+ len: 0,
+ ptr: NonNull::dangling(),
+ _phantom: PhantomData,
+ }
+ }
+}
+
+impl<T: Sized> Drop for OwnedSlice<T> {
+ #[inline]
+ fn drop(&mut self) {
+ if self.len != 0 {
+ let _ = mem::replace(self, Self::default()).into_vec();
+ }
+ }
+}
+
+unsafe impl<T: Sized + Send> Send for OwnedSlice<T> {}
+unsafe impl<T: Sized + Sync> Sync for OwnedSlice<T> {}
+
+impl<T: Clone> Clone for OwnedSlice<T> {
+ #[inline]
+ fn clone(&self) -> Self {
+ Self::from_slice(&**self)
+ }
+}
+
+impl<T: fmt::Debug> fmt::Debug for OwnedSlice<T> {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ self.deref().fmt(formatter)
+ }
+}
+
+impl<T: PartialEq> PartialEq for OwnedSlice<T> {
+ fn eq(&self, other: &Self) -> bool {
+ self.deref().eq(other.deref())
+ }
+}
+
+impl<T: Eq> Eq for OwnedSlice<T> {}
+
+impl<T: Sized> OwnedSlice<T> {
+ /// Convert the OwnedSlice into a boxed slice.
+ #[inline]
+ pub fn into_box(self) -> Box<[T]> {
+ self.into_vec().into_boxed_slice()
+ }
+
+ /// Convert the OwnedSlice into a Vec.
+ #[inline]
+ pub fn into_vec(self) -> Vec<T> {
+ let ret = unsafe { Vec::from_raw_parts(self.ptr.as_ptr(), self.len, self.len) };
+ mem::forget(self);
+ ret
+ }
+
+ /// Convert the regular slice into an owned slice.
+ #[inline]
+ pub fn from_slice(s: &[T]) -> Self
+ where
+ T: Clone,
+ {
+ Self::from(s.to_vec())
+ }
+}
+
+impl<T> IntoIterator for OwnedSlice<T> {
+ type Item = T;
+ type IntoIter = <Vec<T> as IntoIterator>::IntoIter;
+
+ #[inline]
+ fn into_iter(self) -> Self::IntoIter {
+ self.into_vec().into_iter()
+ }
+}
+
+impl<T> Deref for OwnedSlice<T> {
+ type Target = [T];
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
+ }
+}
+
+impl<T> DerefMut for OwnedSlice<T> {
+ #[inline(always)]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
+ }
+}
+
+impl<T> From<Box<[T]>> for OwnedSlice<T> {
+ #[inline]
+ fn from(mut b: Box<[T]>) -> Self {
+ let len = b.len();
+ let ptr = unsafe { NonNull::new_unchecked(b.as_mut_ptr()) };
+ mem::forget(b);
+ Self {
+ len,
+ ptr,
+ _phantom: PhantomData,
+ }
+ }
+}
+
+impl<T> From<Vec<T>> for OwnedSlice<T> {
+ #[inline]
+ fn from(b: Vec<T>) -> Self {
+ Self::from(b.into_boxed_slice())
+ }
+}
+
+impl<T: Sized> MallocShallowSizeOf for OwnedSlice<T> {
+ fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ unsafe { ops.malloc_size_of(self.ptr.as_ptr()) }
+ }
+}
+
+impl<T: MallocSizeOf + Sized> MallocSizeOf for OwnedSlice<T> {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ self.shallow_size_of(ops) + (**self).size_of(ops)
+ }
+}
+
+impl<T: ToShmem + Sized> ToShmem for OwnedSlice<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ unsafe {
+ let dest = to_shmem::to_shmem_slice(self.iter(), builder)?;
+ Ok(mem::ManuallyDrop::new(Self::from(Box::from_raw(dest))))
+ }
+ }
+}
+
+impl<T> iter::FromIterator<T> for OwnedSlice<T> {
+ #[inline]
+ fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
+ Vec::from_iter(iter).into()
+ }
+}
+
+impl<T: Serialize> Serialize for OwnedSlice<T> {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ self.deref().serialize(serializer)
+ }
+}
+
+impl<'de, T: Deserialize<'de>> Deserialize<'de> for OwnedSlice<T> {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let r = Box::<[T]>::deserialize(deserializer)?;
+ Ok(r.into())
+ }
+}
diff --git a/servo/components/style_traits/owned_str.rs b/servo/components/style_traits/owned_str.rs
new file mode 100644
index 0000000000..ebfdcd5e06
--- /dev/null
+++ b/servo/components/style_traits/owned_str.rs
@@ -0,0 +1,81 @@
+/* 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/. */
+
+#![allow(unsafe_code)]
+
+//! A replacement for `Box<str>` that has a defined layout for FFI.
+
+use crate::owned_slice::OwnedSlice;
+use std::fmt;
+use std::ops::{Deref, DerefMut};
+
+/// A struct that basically replaces a Box<str>, but with a defined layout,
+/// suitable for FFI.
+#[repr(C)]
+#[derive(Clone, Default, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub struct OwnedStr(OwnedSlice<u8>);
+
+impl fmt::Debug for OwnedStr {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ self.deref().fmt(formatter)
+ }
+}
+
+impl Deref for OwnedStr {
+ type Target = str;
+
+ #[inline(always)]
+ fn deref(&self) -> &Self::Target {
+ unsafe { std::str::from_utf8_unchecked(&*self.0) }
+ }
+}
+
+impl DerefMut for OwnedStr {
+ #[inline(always)]
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ unsafe { std::str::from_utf8_unchecked_mut(&mut *self.0) }
+ }
+}
+
+impl OwnedStr {
+ /// Convert the OwnedStr into a boxed str.
+ #[inline]
+ pub fn into_box(self) -> Box<str> {
+ self.into_string().into_boxed_str()
+ }
+
+ /// Convert the OwnedStr into a `String`.
+ #[inline]
+ pub fn into_string(self) -> String {
+ unsafe { String::from_utf8_unchecked(self.0.into_vec()) }
+ }
+}
+
+impl From<OwnedStr> for String {
+ #[inline]
+ fn from(b: OwnedStr) -> Self {
+ b.into_string()
+ }
+}
+
+impl From<OwnedStr> for Box<str> {
+ #[inline]
+ fn from(b: OwnedStr) -> Self {
+ b.into_box()
+ }
+}
+
+impl From<Box<str>> for OwnedStr {
+ #[inline]
+ fn from(b: Box<str>) -> Self {
+ Self::from(b.into_string())
+ }
+}
+
+impl From<String> for OwnedStr {
+ #[inline]
+ fn from(s: String) -> Self {
+ OwnedStr(s.into_bytes().into())
+ }
+}
diff --git a/servo/components/style_traits/specified_value_info.rs b/servo/components/style_traits/specified_value_info.rs
new file mode 100644
index 0000000000..1dd368d36e
--- /dev/null
+++ b/servo/components/style_traits/specified_value_info.rs
@@ -0,0 +1,138 @@
+/* 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/. */
+
+//! Value information for devtools.
+
+use crate::arc_slice::ArcSlice;
+use crate::owned_slice::OwnedSlice;
+use servo_arc::Arc;
+use std::ops::Range;
+use std::sync::Arc as StdArc;
+
+/// Type of value that a property supports. This is used by Gecko's
+/// devtools to make sense about value it parses, and types listed
+/// here should match InspectorPropertyType in InspectorUtils.webidl.
+///
+/// XXX This should really be a bitflags rather than a namespace mod,
+/// but currently we cannot use bitflags in const.
+#[allow(non_snake_case)]
+pub mod CssType {
+ /// <color>
+ pub const COLOR: u8 = 1 << 0;
+ /// <gradient>
+ pub const GRADIENT: u8 = 1 << 1;
+ /// <timing-function>
+ pub const TIMING_FUNCTION: u8 = 1 << 2;
+}
+
+/// See SpecifiedValueInfo::collect_completion_keywords.
+pub type KeywordsCollectFn<'a> = &'a mut dyn FnMut(&[&'static str]);
+
+/// Information of values of a given specified value type.
+///
+/// This trait is derivable with `#[derive(SpecifiedValueInfo)]`.
+///
+/// The algorithm traverses the type definition. For `SUPPORTED_TYPES`,
+/// it puts an or'ed value of `SUPPORTED_TYPES` of all types it finds.
+/// For `collect_completion_keywords`, it recursively invokes this
+/// method on types found, and lists all keyword values and function
+/// names following the same rule as `ToCss` in that method.
+///
+/// Some attributes of `ToCss` can affect the behavior, specifically:
+/// * If `#[css(function)]` is found, the content inside the annotated
+/// variant (or the whole type) isn't traversed, only the function
+/// name is listed in `collect_completion_keywords`.
+/// * If `#[css(skip)]` is found, the content inside the variant or
+/// field is ignored.
+/// * Values listed in `#[css(if_empty)]`, `#[parse(aliases)]`, and
+/// `#[css(keyword)]` are added into `collect_completion_keywords`.
+///
+/// In addition to `css` attributes, it also has `value_info` helper
+/// attributes, including:
+/// * `#[value_info(ty = "TYPE")]` can be used to specify a constant
+/// from `CssType` to `SUPPORTED_TYPES`.
+/// * `#[value_info(other_values = "value1,value2")]` can be used to
+/// add other values related to a field, variant, or the type itself
+/// into `collect_completion_keywords`.
+/// * `#[value_info(starts_with_keyword)]` can be used on variants to
+/// add the name of a non-unit variant (serialized like `ToCss`) into
+/// `collect_completion_keywords`.
+pub trait SpecifiedValueInfo {
+ /// Supported CssTypes by the given value type.
+ ///
+ /// XXX This should be typed CssType when that becomes a bitflags.
+ /// Currently we cannot do so since bitflags cannot be used in constant.
+ const SUPPORTED_TYPES: u8 = 0;
+
+ /// Collect value starting words for the given specified value type.
+ /// This includes keyword and function names which can appear at the
+ /// beginning of a value of this type.
+ ///
+ /// Caller should pass in a callback function to accept the list of
+ /// values. The callback function can be called multiple times, and
+ /// some values passed to the callback may be duplicate.
+ fn collect_completion_keywords(_f: KeywordsCollectFn) {}
+}
+
+impl SpecifiedValueInfo for bool {}
+impl SpecifiedValueInfo for f32 {}
+impl SpecifiedValueInfo for i8 {}
+impl SpecifiedValueInfo for i32 {}
+impl SpecifiedValueInfo for u8 {}
+impl SpecifiedValueInfo for u16 {}
+impl SpecifiedValueInfo for u32 {}
+impl SpecifiedValueInfo for usize {}
+impl SpecifiedValueInfo for str {}
+impl SpecifiedValueInfo for String {}
+impl SpecifiedValueInfo for crate::owned_str::OwnedStr {}
+
+#[cfg(feature = "servo")]
+impl SpecifiedValueInfo for ::servo_atoms::Atom {}
+#[cfg(feature = "servo")]
+impl SpecifiedValueInfo for ::servo_url::ServoUrl {}
+
+impl<T: SpecifiedValueInfo + ?Sized> SpecifiedValueInfo for Box<T> {
+ const SUPPORTED_TYPES: u8 = T::SUPPORTED_TYPES;
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ T::collect_completion_keywords(f);
+ }
+}
+
+impl<T: SpecifiedValueInfo> SpecifiedValueInfo for [T] {
+ const SUPPORTED_TYPES: u8 = T::SUPPORTED_TYPES;
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ T::collect_completion_keywords(f);
+ }
+}
+
+macro_rules! impl_generic_specified_value_info {
+ ($ty:ident<$param:ident>) => {
+ impl<$param: SpecifiedValueInfo> SpecifiedValueInfo for $ty<$param> {
+ const SUPPORTED_TYPES: u8 = $param::SUPPORTED_TYPES;
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ $param::collect_completion_keywords(f);
+ }
+ }
+ };
+}
+impl_generic_specified_value_info!(Option<T>);
+impl_generic_specified_value_info!(OwnedSlice<T>);
+impl_generic_specified_value_info!(Vec<T>);
+impl_generic_specified_value_info!(Arc<T>);
+impl_generic_specified_value_info!(StdArc<T>);
+impl_generic_specified_value_info!(ArcSlice<T>);
+impl_generic_specified_value_info!(Range<Idx>);
+
+impl<T1, T2> SpecifiedValueInfo for (T1, T2)
+where
+ T1: SpecifiedValueInfo,
+ T2: SpecifiedValueInfo,
+{
+ const SUPPORTED_TYPES: u8 = T1::SUPPORTED_TYPES | T2::SUPPORTED_TYPES;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ T1::collect_completion_keywords(f);
+ T2::collect_completion_keywords(f);
+ }
+}
diff --git a/servo/components/style_traits/values.rs b/servo/components/style_traits/values.rs
new file mode 100644
index 0000000000..a004b577c1
--- /dev/null
+++ b/servo/components/style_traits/values.rs
@@ -0,0 +1,569 @@
+/* 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/. */
+
+//! Helper types and traits for the handling of CSS values.
+
+use app_units::Au;
+use cssparser::ToCss as CssparserToCss;
+use cssparser::{serialize_string, ParseError, Parser, Token, UnicodeRange};
+use nsstring::nsCString;
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+
+/// Serialises a value according to its CSS representation.
+///
+/// This trait is implemented for `str` and its friends, serialising the string
+/// contents as a CSS quoted string.
+///
+/// This trait is derivable with `#[derive(ToCss)]`, with the following behaviour:
+/// * unit variants get serialised as the `snake-case` representation
+/// of their name;
+/// * unit variants whose name starts with "Moz" or "Webkit" are prepended
+/// with a "-";
+/// * if `#[css(comma)]` is found on a variant, its fields are separated by
+/// commas, otherwise, by spaces;
+/// * if `#[css(function)]` is found on a variant, the variant name gets
+/// serialised like unit variants and its fields are surrounded by parentheses;
+/// * if `#[css(iterable)]` is found on a function variant, that variant needs
+/// to have a single member, and that member needs to be iterable. The
+/// iterable will be serialized as the arguments for the function;
+/// * an iterable field can also be annotated with `#[css(if_empty = "foo")]`
+/// to print `"foo"` if the iterator is empty;
+/// * if `#[css(dimension)]` is found on a variant, that variant needs
+/// to have a single member. The variant would be serialized as a CSS
+/// dimension token, like: <member><identifier>;
+/// * if `#[css(skip)]` is found on a field, the `ToCss` call for that field
+/// is skipped;
+/// * if `#[css(skip_if = "function")]` is found on a field, the `ToCss` call
+/// for that field is skipped if `function` returns true. This function is
+/// provided the field as an argument;
+/// * if `#[css(contextual_skip_if = "function")]` is found on a field, the
+/// `ToCss` call for that field is skipped if `function` returns true. This
+/// function is given all the fields in the current struct or variant as an
+/// argument;
+/// * `#[css(represents_keyword)]` can be used on bool fields in order to
+/// serialize the field name if the field is true, or nothing otherwise. It
+/// also collects those keywords for `SpecifiedValueInfo`.
+/// * `#[css(bitflags(single="", mixed="", validate="", overlapping_bits)]` can
+/// be used to derive parse / serialize / etc on bitflags. The rules for parsing
+/// bitflags are the following:
+///
+/// * `single` flags can only appear on their own. It's common that bitflags
+/// properties at least have one such value like `none` or `auto`.
+/// * `mixed` properties can appear mixed together, but not along any other
+/// flag that shares a bit with itself. For example, if you have three
+/// bitflags like:
+///
+/// FOO = 1 << 0;
+/// BAR = 1 << 1;
+/// BAZ = 1 << 2;
+/// BAZZ = BAR | BAZ;
+///
+/// Then the following combinations won't be valid:
+///
+/// * foo foo: (every flag shares a bit with itself)
+/// * bar bazz: (bazz shares a bit with bar)
+///
+/// But `bar baz` will be valid, as they don't share bits, and so would
+/// `foo` with any other flag, or `bazz` on its own.
+/// * `overlapping_bits` enables some tracking during serialization of mixed
+/// flags to avoid serializing variants that can subsume other variants.
+/// In the example above, you could do:
+/// mixed="foo,bazz,bar,baz", overlapping_bits
+/// to ensure that if bazz is serialized, bar and baz aren't, even though
+/// their bits are set. Note that the serialization order is canonical,
+/// and thus depends on the order you specify the flags in.
+///
+/// * finally, one can put `#[css(derive_debug)]` on the whole type, to
+/// implement `Debug` by a single call to `ToCss::to_css`.
+pub trait ToCss {
+ /// Serialize `self` in CSS syntax, writing to `dest`.
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write;
+
+ /// Serialize `self` in CSS syntax and return a string.
+ ///
+ /// (This is a convenience wrapper for `to_css` and probably should not be overridden.)
+ #[inline]
+ fn to_css_string(&self) -> String {
+ let mut s = String::new();
+ self.to_css(&mut CssWriter::new(&mut s)).unwrap();
+ s
+ }
+
+ /// Serialize `self` in CSS syntax and return a nsCString.
+ ///
+ /// (This is a convenience wrapper for `to_css` and probably should not be overridden.)
+ #[inline]
+ fn to_css_nscstring(&self) -> nsCString {
+ let mut s = nsCString::new();
+ self.to_css(&mut CssWriter::new(&mut s)).unwrap();
+ s
+ }
+}
+
+impl<'a, T> ToCss for &'a T
+where
+ T: ToCss + ?Sized,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ (*self).to_css(dest)
+ }
+}
+
+impl ToCss for crate::owned_str::OwnedStr {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_string(self, dest)
+ }
+}
+
+impl ToCss for str {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_string(self, dest)
+ }
+}
+
+impl ToCss for String {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_string(self, dest)
+ }
+}
+
+impl<T> ToCss for Option<T>
+where
+ T: ToCss,
+{
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.as_ref().map_or(Ok(()), |value| value.to_css(dest))
+ }
+}
+
+impl ToCss for () {
+ #[inline]
+ fn to_css<W>(&self, _: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ Ok(())
+ }
+}
+
+/// A writer tailored for serialising CSS.
+///
+/// Coupled with SequenceWriter, this allows callers to transparently handle
+/// things like comma-separated values etc.
+pub struct CssWriter<'w, W: 'w> {
+ inner: &'w mut W,
+ prefix: Option<&'static str>,
+}
+
+impl<'w, W> CssWriter<'w, W>
+where
+ W: Write,
+{
+ /// Creates a new `CssWriter`.
+ #[inline]
+ pub fn new(inner: &'w mut W) -> Self {
+ Self {
+ inner,
+ prefix: Some(""),
+ }
+ }
+}
+
+impl<'w, W> Write for CssWriter<'w, W>
+where
+ W: Write,
+{
+ #[inline]
+ fn write_str(&mut self, s: &str) -> fmt::Result {
+ if s.is_empty() {
+ return Ok(());
+ }
+ if let Some(prefix) = self.prefix.take() {
+ // We are going to write things, but first we need to write
+ // the prefix that was set by `SequenceWriter::item`.
+ if !prefix.is_empty() {
+ self.inner.write_str(prefix)?;
+ }
+ }
+ self.inner.write_str(s)
+ }
+
+ #[inline]
+ fn write_char(&mut self, c: char) -> fmt::Result {
+ if let Some(prefix) = self.prefix.take() {
+ // See comment in `write_str`.
+ if !prefix.is_empty() {
+ self.inner.write_str(prefix)?;
+ }
+ }
+ self.inner.write_char(c)
+ }
+}
+
+/// Convenience wrapper to serialise CSS values separated by a given string.
+pub struct SequenceWriter<'a, 'b: 'a, W: 'b> {
+ inner: &'a mut CssWriter<'b, W>,
+ separator: &'static str,
+}
+
+impl<'a, 'b, W> SequenceWriter<'a, 'b, W>
+where
+ W: Write + 'b,
+{
+ /// Create a new sequence writer.
+ #[inline]
+ pub fn new(inner: &'a mut CssWriter<'b, W>, separator: &'static str) -> Self {
+ if inner.prefix.is_none() {
+ // See comment in `item`.
+ inner.prefix = Some("");
+ }
+ Self { inner, separator }
+ }
+
+ #[inline]
+ fn write_item<F>(&mut self, f: F) -> fmt::Result
+ where
+ F: FnOnce(&mut CssWriter<'b, W>) -> fmt::Result,
+ {
+ // Separate non-generic functions so that this code is not repeated
+ // in every monomorphization with a different type `F` or `W`.
+ // https://github.com/servo/servo/issues/26713
+ fn before(
+ prefix: &mut Option<&'static str>,
+ separator: &'static str,
+ ) -> Option<&'static str> {
+ let old_prefix = *prefix;
+ if old_prefix.is_none() {
+ // If there is no prefix in the inner writer, a previous
+ // call to this method produced output, which means we need
+ // to write the separator next time we produce output again.
+ *prefix = Some(separator);
+ }
+ old_prefix
+ }
+ fn after(
+ old_prefix: Option<&'static str>,
+ prefix: &mut Option<&'static str>,
+ separator: &'static str,
+ ) {
+ match (old_prefix, *prefix) {
+ (_, None) => {
+ // This call produced output and cleaned up after itself.
+ },
+ (None, Some(p)) => {
+ // Some previous call to `item` produced output,
+ // but this one did not, prefix should be the same as
+ // the one we set.
+ debug_assert_eq!(separator, p);
+ // We clean up here even though it's not necessary just
+ // to be able to do all these assertion checks.
+ *prefix = None;
+ },
+ (Some(old), Some(new)) => {
+ // No previous call to `item` produced output, and this one
+ // either.
+ debug_assert_eq!(old, new);
+ },
+ }
+ }
+
+ let old_prefix = before(&mut self.inner.prefix, self.separator);
+ f(self.inner)?;
+ after(old_prefix, &mut self.inner.prefix, self.separator);
+ Ok(())
+ }
+
+ /// Serialises a CSS value, writing any separator as necessary.
+ ///
+ /// The separator is never written before any `item` produces any output,
+ /// and is written in subsequent calls only if the `item` produces some
+ /// output on its own again. This lets us handle `Option<T>` fields by
+ /// just not printing anything on `None`.
+ #[inline]
+ pub fn item<T>(&mut self, item: &T) -> fmt::Result
+ where
+ T: ToCss,
+ {
+ self.write_item(|inner| item.to_css(inner))
+ }
+
+ /// Writes a string as-is (i.e. not escaped or wrapped in quotes)
+ /// with any separator as necessary.
+ ///
+ /// See SequenceWriter::item.
+ #[inline]
+ pub fn raw_item(&mut self, item: &str) -> fmt::Result {
+ self.write_item(|inner| inner.write_str(item))
+ }
+}
+
+/// Type used as the associated type in the `OneOrMoreSeparated` trait on a
+/// type to indicate that a serialized list of elements of this type is
+/// separated by commas.
+pub struct Comma;
+
+/// Type used as the associated type in the `OneOrMoreSeparated` trait on a
+/// type to indicate that a serialized list of elements of this type is
+/// separated by spaces.
+pub struct Space;
+
+/// Type used as the associated type in the `OneOrMoreSeparated` trait on a
+/// type to indicate that a serialized list of elements of this type is
+/// separated by commas, but spaces without commas are also allowed when
+/// parsing.
+pub struct CommaWithSpace;
+
+/// A trait satisfied by the types corresponding to separators.
+pub trait Separator {
+ /// The separator string that the satisfying separator type corresponds to.
+ fn separator() -> &'static str;
+
+ /// Parses a sequence of values separated by this separator.
+ ///
+ /// The given closure is called repeatedly for each item in the sequence.
+ ///
+ /// Successful results are accumulated in a vector.
+ ///
+ /// This method returns `Err(_)` the first time a closure does or if
+ /// the separators aren't correct.
+ fn parse<'i, 't, F, T, E>(
+ parser: &mut Parser<'i, 't>,
+ parse_one: F,
+ ) -> Result<Vec<T>, ParseError<'i, E>>
+ where
+ F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>;
+}
+
+impl Separator for Comma {
+ fn separator() -> &'static str {
+ ", "
+ }
+
+ fn parse<'i, 't, F, T, E>(
+ input: &mut Parser<'i, 't>,
+ parse_one: F,
+ ) -> Result<Vec<T>, ParseError<'i, E>>
+ where
+ F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>,
+ {
+ input.parse_comma_separated(parse_one)
+ }
+}
+
+impl Separator for Space {
+ fn separator() -> &'static str {
+ " "
+ }
+
+ fn parse<'i, 't, F, T, E>(
+ input: &mut Parser<'i, 't>,
+ mut parse_one: F,
+ ) -> Result<Vec<T>, ParseError<'i, E>>
+ where
+ F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>,
+ {
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ let mut results = vec![parse_one(input)?];
+ loop {
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ if let Ok(item) = input.try(&mut parse_one) {
+ results.push(item);
+ } else {
+ return Ok(results);
+ }
+ }
+ }
+}
+
+impl Separator for CommaWithSpace {
+ fn separator() -> &'static str {
+ ", "
+ }
+
+ fn parse<'i, 't, F, T, E>(
+ input: &mut Parser<'i, 't>,
+ mut parse_one: F,
+ ) -> Result<Vec<T>, ParseError<'i, E>>
+ where
+ F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>,
+ {
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ let mut results = vec![parse_one(input)?];
+ loop {
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ let comma_location = input.current_source_location();
+ let comma = input.try(|i| i.expect_comma()).is_ok();
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ if let Ok(item) = input.try(&mut parse_one) {
+ results.push(item);
+ } else if comma {
+ return Err(comma_location.new_unexpected_token_error(Token::Comma));
+ } else {
+ break;
+ }
+ }
+ Ok(results)
+ }
+}
+
+/// Marker trait on T to automatically implement ToCss for Vec<T> when T's are
+/// separated by some delimiter `delim`.
+pub trait OneOrMoreSeparated {
+ /// Associated type indicating which separator is used.
+ type S: Separator;
+}
+
+impl OneOrMoreSeparated for UnicodeRange {
+ type S = Comma;
+}
+
+impl<T> ToCss for Vec<T>
+where
+ T: ToCss + OneOrMoreSeparated,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let mut iter = self.iter();
+ iter.next().unwrap().to_css(dest)?;
+ for item in iter {
+ dest.write_str(<T as OneOrMoreSeparated>::S::separator())?;
+ item.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl<T> ToCss for Box<T>
+where
+ T: ?Sized + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ (**self).to_css(dest)
+ }
+}
+
+impl<T> ToCss for Arc<T>
+where
+ T: ?Sized + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ (**self).to_css(dest)
+ }
+}
+
+impl ToCss for Au {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.to_f64_px().to_css(dest)?;
+ dest.write_str("px")
+ }
+}
+
+macro_rules! impl_to_css_for_predefined_type {
+ ($name: ty) => {
+ impl<'a> ToCss for $name {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ ::cssparser::ToCss::to_css(self, dest)
+ }
+ }
+ };
+}
+
+impl_to_css_for_predefined_type!(f32);
+impl_to_css_for_predefined_type!(i8);
+impl_to_css_for_predefined_type!(i32);
+impl_to_css_for_predefined_type!(u16);
+impl_to_css_for_predefined_type!(u32);
+impl_to_css_for_predefined_type!(::cssparser::Token<'a>);
+impl_to_css_for_predefined_type!(::cssparser::UnicodeRange);
+
+/// Helper types for the handling of specified values.
+pub mod specified {
+ use crate::ParsingMode;
+
+ /// Whether to allow negative lengths or not.
+ #[repr(u8)]
+ #[derive(
+ Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, PartialOrd, Serialize, ToShmem,
+ )]
+ pub enum AllowedNumericType {
+ /// Allow all kind of numeric values.
+ All,
+ /// Allow only non-negative numeric values.
+ NonNegative,
+ /// Allow only numeric values greater or equal to 1.0.
+ AtLeastOne,
+ /// Allow only numeric values from 0 to 1.0.
+ ZeroToOne,
+ }
+
+ impl Default for AllowedNumericType {
+ #[inline]
+ fn default() -> Self {
+ AllowedNumericType::All
+ }
+ }
+
+ impl AllowedNumericType {
+ /// Whether the value fits the rules of this numeric type.
+ #[inline]
+ pub fn is_ok(&self, parsing_mode: ParsingMode, val: f32) -> bool {
+ if parsing_mode.allows_all_numeric_values() {
+ return true;
+ }
+ match *self {
+ AllowedNumericType::All => true,
+ AllowedNumericType::NonNegative => val >= 0.0,
+ AllowedNumericType::AtLeastOne => val >= 1.0,
+ AllowedNumericType::ZeroToOne => val >= 0.0 && val <= 1.0,
+ }
+ }
+
+ /// Clamp the value following the rules of this numeric type.
+ #[inline]
+ pub fn clamp(&self, val: f32) -> f32 {
+ match *self {
+ AllowedNumericType::All => val,
+ AllowedNumericType::NonNegative => val.max(0.),
+ AllowedNumericType::AtLeastOne => val.max(1.),
+ AllowedNumericType::ZeroToOne => val.max(0.).min(1.),
+ }
+ }
+ }
+}
diff --git a/servo/components/to_shmem/Cargo.toml b/servo/components/to_shmem/Cargo.toml
new file mode 100644
index 0000000000..09e78f57d2
--- /dev/null
+++ b/servo/components/to_shmem/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "to_shmem"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+publish = false
+
+[lib]
+name = "to_shmem"
+path = "lib.rs"
+
+[features]
+servo = ["cssparser/serde", "string_cache"]
+gecko = []
+
+[dependencies]
+cssparser = "0.33"
+servo_arc = { path = "../servo_arc" }
+smallbitvec = "2.1.1"
+smallvec = "1.0"
+string_cache = { version = "0.8", optional = true }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
diff --git a/servo/components/to_shmem/lib.rs b/servo/components/to_shmem/lib.rs
new file mode 100644
index 0000000000..54fee3a420
--- /dev/null
+++ b/servo/components/to_shmem/lib.rs
@@ -0,0 +1,618 @@
+/* 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/. */
+
+//! Trait for cloning data into a shared memory buffer.
+//!
+//! This module contains the SharedMemoryBuilder type and ToShmem trait.
+//!
+//! We put them here (and not in style_traits) so that we can derive ToShmem
+//! from the selectors and style crates.
+
+#![crate_name = "to_shmem"]
+#![crate_type = "rlib"]
+
+extern crate cssparser;
+extern crate servo_arc;
+extern crate smallbitvec;
+extern crate smallvec;
+#[cfg(feature = "string_cache")]
+extern crate string_cache;
+extern crate thin_vec;
+
+use servo_arc::{Arc, ArcUnion, ArcUnionBorrow, HeaderSlice};
+use smallbitvec::{InternalStorage, SmallBitVec};
+use smallvec::{Array, SmallVec};
+use std::alloc::Layout;
+use std::collections::HashSet;
+use std::ffi::CString;
+use std::isize;
+use std::marker::PhantomData;
+use std::mem::{self, ManuallyDrop};
+use std::num::Wrapping;
+use std::ops::Range;
+use std::os::raw::c_char;
+#[cfg(debug_assertions)]
+use std::os::raw::c_void;
+use std::ptr::{self, NonNull};
+use std::slice;
+use std::str;
+use thin_vec::ThinVec;
+
+/// Result type for ToShmem::to_shmem.
+///
+/// The String is an error message describing why the call failed.
+pub type Result<T> = std::result::Result<ManuallyDrop<T>, String>;
+
+// Various pointer arithmetic functions in this file can be replaced with
+// functions on `Layout` once they have stabilized:
+//
+// https://github.com/rust-lang/rust/issues/55724
+
+/// A builder object that transforms and copies values into a fixed size buffer.
+pub struct SharedMemoryBuilder {
+ /// The buffer into which values will be copied.
+ buffer: *mut u8,
+ /// The size of the buffer.
+ capacity: usize,
+ /// The current position in the buffer, where the next value will be written
+ /// at.
+ index: usize,
+ /// Pointers to every shareable value that we store in the shared memory
+ /// buffer. We use this to assert against encountering the same value
+ /// twice, e.g. through another Arc reference, so that we don't
+ /// inadvertently store duplicate copies of values.
+ #[cfg(debug_assertions)]
+ shared_values: HashSet<*const c_void>,
+}
+
+/// Amount of padding needed after `size` bytes to ensure that the following
+/// address will satisfy `align`.
+fn padding_needed_for(size: usize, align: usize) -> usize {
+ padded_size(size, align).wrapping_sub(size)
+}
+
+/// Rounds up `size` so that the following address will satisfy `align`.
+fn padded_size(size: usize, align: usize) -> usize {
+ size.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1)
+}
+
+impl SharedMemoryBuilder {
+ /// Creates a new SharedMemoryBuilder using the specified buffer.
+ pub unsafe fn new(buffer: *mut u8, capacity: usize) -> SharedMemoryBuilder {
+ SharedMemoryBuilder {
+ buffer,
+ capacity,
+ index: 0,
+ #[cfg(debug_assertions)]
+ shared_values: HashSet::new(),
+ }
+ }
+
+ /// Returns the number of bytes currently used in the buffer.
+ #[inline]
+ pub fn len(&self) -> usize {
+ self.index
+ }
+
+ /// Writes a value into the shared memory buffer and returns a pointer to
+ /// it in the buffer.
+ ///
+ /// The value is cloned and converted into a form suitable for placing into
+ /// a shared memory buffer by calling ToShmem::to_shmem on it.
+ ///
+ /// Panics if there is insufficient space in the buffer.
+ pub fn write<T: ToShmem>(&mut self, value: &T) -> std::result::Result<*mut T, String> {
+ // Reserve space for the value.
+ let dest: *mut T = self.alloc_value();
+
+ // Make a clone of the value with all of its heap allocations
+ // placed in the shared memory buffer.
+ let value = value.to_shmem(self)?;
+
+ unsafe {
+ // Copy the value into the buffer.
+ ptr::write(dest, ManuallyDrop::into_inner(value));
+ }
+
+ // Return a pointer to the shared value.
+ Ok(dest)
+ }
+
+ /// Reserves space in the shared memory buffer to fit a value of type T,
+ /// and returns a pointer to that reserved space.
+ ///
+ /// Panics if there is insufficient space in the buffer.
+ pub fn alloc_value<T>(&mut self) -> *mut T {
+ self.alloc(Layout::new::<T>())
+ }
+
+ /// Reserves space in the shared memory buffer to fit an array of values of
+ /// type T, and returns a pointer to that reserved space.
+ ///
+ /// Panics if there is insufficient space in the buffer.
+ pub fn alloc_array<T>(&mut self, len: usize) -> *mut T {
+ if len == 0 {
+ return NonNull::dangling().as_ptr();
+ }
+
+ let size = mem::size_of::<T>();
+ let align = mem::align_of::<T>();
+
+ self.alloc(Layout::from_size_align(padded_size(size, align) * len, align).unwrap())
+ }
+
+ /// Reserves space in the shared memory buffer that conforms to the
+ /// specified layout, and returns a pointer to that reserved space.
+ ///
+ /// Panics if there is insufficient space in the buffer.
+ pub fn alloc<T>(&mut self, layout: Layout) -> *mut T {
+ // Amount of padding to align the value.
+ //
+ // The addition can't overflow, since self.index <= self.capacity, and
+ // for us to have successfully allocated the buffer, `buffer + capacity`
+ // can't overflow.
+ let padding = padding_needed_for(self.buffer as usize + self.index, layout.align());
+
+ // Reserve space for the padding.
+ let start = self.index.checked_add(padding).unwrap();
+ assert!(start <= std::isize::MAX as usize); // for the cast below
+
+ // Reserve space for the value.
+ let end = start.checked_add(layout.size()).unwrap();
+ assert!(end <= self.capacity);
+
+ self.index = end;
+ unsafe { self.buffer.add(start) as *mut T }
+ }
+}
+
+/// A type that can be copied into a SharedMemoryBuilder.
+pub trait ToShmem: Sized {
+ /// Clones this value into a form suitable for writing into a
+ /// SharedMemoryBuilder.
+ ///
+ /// If this value owns any heap allocations, they should be written into
+ /// `builder` so that the return value of this function can point to the
+ /// copy in the shared memory buffer.
+ ///
+ /// The return type is wrapped in ManuallyDrop to make it harder to
+ /// accidentally invoke the destructor of the value that is produced.
+ ///
+ /// Returns a Result so that we can gracefully recover from unexpected
+ /// content.
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self>;
+}
+
+#[macro_export]
+macro_rules! impl_trivial_to_shmem {
+ ($($ty:ty),*) => {
+ $(
+ impl $crate::ToShmem for $ty {
+ fn to_shmem(
+ &self,
+ _builder: &mut $crate::SharedMemoryBuilder,
+ ) -> $crate::Result<Self> {
+ $crate::Result::Ok(::std::mem::ManuallyDrop::new(*self))
+ }
+ }
+ )*
+ };
+}
+
+impl_trivial_to_shmem!(
+ (),
+ bool,
+ f32,
+ f64,
+ i8,
+ i16,
+ i32,
+ i64,
+ u8,
+ u16,
+ u32,
+ u64,
+ isize,
+ usize,
+ std::num::NonZeroUsize
+);
+
+impl_trivial_to_shmem!(
+ cssparser::SourceLocation,
+ cssparser::SourcePosition,
+ cssparser::TokenSerializationType
+);
+
+impl<T> ToShmem for PhantomData<T> {
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ Ok(ManuallyDrop::new(*self))
+ }
+}
+
+impl<T: ToShmem> ToShmem for Range<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ Ok(ManuallyDrop::new(Range {
+ start: ManuallyDrop::into_inner(self.start.to_shmem(builder)?),
+ end: ManuallyDrop::into_inner(self.end.to_shmem(builder)?),
+ }))
+ }
+}
+
+impl ToShmem for cssparser::UnicodeRange {
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ Ok(ManuallyDrop::new(cssparser::UnicodeRange {
+ start: self.start,
+ end: self.end,
+ }))
+ }
+}
+
+impl<T: ToShmem, U: ToShmem> ToShmem for (T, U) {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ Ok(ManuallyDrop::new((
+ ManuallyDrop::into_inner(self.0.to_shmem(builder)?),
+ ManuallyDrop::into_inner(self.1.to_shmem(builder)?),
+ )))
+ }
+}
+
+impl<T: ToShmem> ToShmem for Wrapping<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ Ok(ManuallyDrop::new(Wrapping(ManuallyDrop::into_inner(
+ self.0.to_shmem(builder)?,
+ ))))
+ }
+}
+
+impl<T: ToShmem> ToShmem for Box<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ // Reserve space for the boxed value.
+ let dest: *mut T = builder.alloc_value();
+
+ // Make a clone of the boxed value with all of its heap allocations
+ // placed in the shared memory buffer.
+ let value = (**self).to_shmem(builder)?;
+
+ unsafe {
+ // Copy the value into the buffer.
+ ptr::write(dest, ManuallyDrop::into_inner(value));
+
+ Ok(ManuallyDrop::new(Box::from_raw(dest)))
+ }
+ }
+}
+
+/// Converts all the items in `src` into shared memory form, writes them into
+/// the specified buffer, and returns a pointer to the slice.
+unsafe fn to_shmem_slice_ptr<'a, T, I>(
+ src: I,
+ dest: *mut T,
+ builder: &mut SharedMemoryBuilder,
+) -> std::result::Result<*mut [T], String>
+where
+ T: 'a + ToShmem,
+ I: ExactSizeIterator<Item = &'a T>,
+{
+ let dest = slice::from_raw_parts_mut(dest, src.len());
+
+ // Make a clone of each element from the iterator with its own heap
+ // allocations placed in the buffer, and copy that clone into the buffer.
+ for (src, dest) in src.zip(dest.iter_mut()) {
+ ptr::write(dest, ManuallyDrop::into_inner(src.to_shmem(builder)?));
+ }
+
+ Ok(dest)
+}
+
+/// Writes all the items in `src` into a slice in the shared memory buffer and
+/// returns a pointer to the slice.
+pub unsafe fn to_shmem_slice<'a, T, I>(
+ src: I,
+ builder: &mut SharedMemoryBuilder,
+) -> std::result::Result<*mut [T], String>
+where
+ T: 'a + ToShmem,
+ I: ExactSizeIterator<Item = &'a T>,
+{
+ let dest = builder.alloc_array(src.len());
+ to_shmem_slice_ptr(src, dest, builder)
+}
+
+impl<T: ToShmem> ToShmem for Box<[T]> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ unsafe {
+ let dest = to_shmem_slice(self.iter(), builder)?;
+ Ok(ManuallyDrop::new(Box::from_raw(dest)))
+ }
+ }
+}
+
+impl ToShmem for Box<str> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ // Reserve space for the string bytes.
+ let dest: *mut u8 = builder.alloc_array(self.len());
+
+ unsafe {
+ // Copy the value into the buffer.
+ ptr::copy(self.as_ptr(), dest, self.len());
+
+ Ok(ManuallyDrop::new(Box::from_raw(
+ str::from_utf8_unchecked_mut(slice::from_raw_parts_mut(dest, self.len())),
+ )))
+ }
+ }
+}
+
+impl ToShmem for String {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ // Reserve space for the string bytes.
+ let dest: *mut u8 = builder.alloc_array(self.len());
+
+ unsafe {
+ // Copy the value into the buffer.
+ ptr::copy(self.as_ptr(), dest, self.len());
+
+ Ok(ManuallyDrop::new(String::from_raw_parts(
+ dest,
+ self.len(),
+ self.len(),
+ )))
+ }
+ }
+}
+
+impl ToShmem for CString {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ let len = self.as_bytes_with_nul().len();
+
+ // Reserve space for the string bytes.
+ let dest: *mut c_char = builder.alloc_array(len);
+
+ unsafe {
+ // Copy the value into the buffer.
+ ptr::copy(self.as_ptr(), dest, len);
+
+ Ok(ManuallyDrop::new(CString::from_raw(dest)))
+ }
+ }
+}
+
+impl<T: ToShmem> ToShmem for Vec<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ unsafe {
+ let dest = to_shmem_slice(self.iter(), builder)? as *mut T;
+ let dest_vec = Vec::from_raw_parts(dest, self.len(), self.len());
+ Ok(ManuallyDrop::new(dest_vec))
+ }
+ }
+}
+
+impl<T: ToShmem, A: Array<Item = T>> ToShmem for SmallVec<A> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ let dest_vec = unsafe {
+ if self.spilled() {
+ // Place the items in a separate allocation in the shared memory
+ // buffer.
+ let dest = to_shmem_slice(self.iter(), builder)? as *mut T;
+ SmallVec::from_raw_parts(dest, self.len(), self.len())
+ } else {
+ // Place the items inline.
+ let mut s = SmallVec::new();
+ to_shmem_slice_ptr(self.iter(), s.as_mut_ptr(), builder)?;
+ s.set_len(self.len());
+ s
+ }
+ };
+
+ Ok(ManuallyDrop::new(dest_vec))
+ }
+}
+
+impl<T: ToShmem> ToShmem for Option<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ let v = match self {
+ Some(v) => Some(ManuallyDrop::into_inner(v.to_shmem(builder)?)),
+ None => None,
+ };
+
+ Ok(ManuallyDrop::new(v))
+ }
+}
+
+impl<T: ToShmem, S> ToShmem for HashSet<T, S>
+where
+ Self: Default,
+{
+ fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ if !self.is_empty() {
+ return Err(format!(
+ "ToShmem failed for HashSet: We only support empty sets \
+ (we don't expect custom properties in UA sheets, they're observable by content)",
+ ));
+ }
+ Ok(ManuallyDrop::new(Self::default()))
+ }
+}
+
+impl<A: 'static, B: 'static> ToShmem for ArcUnion<A, B>
+where
+ Arc<A>: ToShmem,
+ Arc<B>: ToShmem,
+{
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ Ok(ManuallyDrop::new(match self.borrow() {
+ ArcUnionBorrow::First(first) => Self::from_first(ManuallyDrop::into_inner(
+ first.with_arc(|a| a.to_shmem(builder))?,
+ )),
+ ArcUnionBorrow::Second(second) => Self::from_second(ManuallyDrop::into_inner(
+ second.with_arc(|a| a.to_shmem(builder))?,
+ )),
+ }))
+ }
+}
+
+impl<T: ToShmem> ToShmem for Arc<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ // Assert that we don't encounter any shared references to values we
+ // don't expect.
+ #[cfg(debug_assertions)]
+ assert!(
+ !builder.shared_values.contains(&self.heap_ptr()),
+ "ToShmem failed for Arc<{}>: encountered a value with multiple \
+ references.",
+ std::any::type_name::<T>()
+ );
+
+ // Make a clone of the Arc-owned value with all of its heap allocations
+ // placed in the shared memory buffer.
+ let value = (**self).to_shmem(builder)?;
+
+ // Create a new Arc with the shared value and have it place its
+ // ArcInner in the shared memory buffer.
+ unsafe {
+ let static_arc = Arc::new_static(
+ |layout| builder.alloc(layout),
+ ManuallyDrop::into_inner(value),
+ );
+
+ #[cfg(debug_assertions)]
+ builder.shared_values.insert(self.heap_ptr());
+
+ Ok(ManuallyDrop::new(static_arc))
+ }
+ }
+}
+
+impl<H: ToShmem, T: ToShmem> ToShmem for Arc<HeaderSlice<H, T>> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ // We don't currently have any shared ThinArc values in stylesheets,
+ // so don't support them for now.
+ #[cfg(debug_assertions)]
+ assert!(
+ !builder.shared_values.contains(&self.heap_ptr()),
+ "ToShmem failed for ThinArc<T>: encountered a value with multiple references, which \
+ is not currently supported",
+ );
+
+ // Make a clone of the Arc-owned header and slice values with all of
+ // their heap allocations placed in the shared memory buffer.
+ let header = self.header.to_shmem(builder)?;
+ let mut values = Vec::with_capacity(self.len());
+ for v in self.slice().iter() {
+ values.push(v.to_shmem(builder)?);
+ }
+
+ // Create a new ThinArc with the shared value and have it place
+ // its ArcInner in the shared memory buffer.
+ let len = values.len();
+ let static_arc = Self::from_header_and_iter_alloc(
+ |layout| builder.alloc(layout),
+ ManuallyDrop::into_inner(header),
+ values.into_iter().map(ManuallyDrop::into_inner),
+ len,
+ /* is_static = */ true,
+ );
+
+ #[cfg(debug_assertions)]
+ builder.shared_values.insert(self.heap_ptr());
+
+ Ok(ManuallyDrop::new(static_arc))
+ }
+}
+
+impl<T: ToShmem> ToShmem for ThinVec<T> {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ assert_eq!(mem::size_of::<Self>(), mem::size_of::<*const ()>());
+
+ // NOTE: We need to do the work of allocating the header in shared memory even if the
+ // length is zero, because an empty ThinVec, even though it doesn't allocate, references
+ // static memory which will not be mapped to other processes, see bug 1841011.
+ let len = self.len();
+
+ // nsTArrayHeader size.
+ // FIXME: Would be nice not to hard-code this, but in practice thin-vec crate also relies
+ // on this.
+ let header_size = 2 * mem::size_of::<u32>();
+ let header_align = mem::size_of::<u32>();
+
+ let item_size = mem::size_of::<T>();
+ let item_align = mem::align_of::<T>();
+
+ // We don't need to support underalignment for now, this could be supported if needed.
+ assert!(item_align >= header_align);
+
+ // This is explicitly unsupported by ThinVec, see:
+ // https://searchfox.org/mozilla-central/rev/ad732108b073742d7324f998c085f459674a6846/third_party/rust/thin-vec/src/lib.rs#375-386
+ assert!(item_align <= header_size);
+ let header_padding = 0;
+
+ let layout = Layout::from_size_align(
+ header_size + header_padding + padded_size(item_size, item_align) * len,
+ item_align,
+ )
+ .unwrap();
+
+ let shmem_header_ptr = builder.alloc::<u8>(layout);
+ let shmem_data_ptr = unsafe { shmem_header_ptr.add(header_size + header_padding) };
+
+ let data_ptr = self.as_ptr() as *const T as *const u8;
+ let header_ptr = unsafe { data_ptr.sub(header_size + header_padding) };
+
+ unsafe {
+ // Copy the header. Note this might copy a wrong capacity, but it doesn't matter,
+ // because shared memory ptrs are immutable anyways, and we can't relocate.
+ ptr::copy(header_ptr, shmem_header_ptr, header_size);
+ // ToShmem + copy the contents into the shared buffer.
+ to_shmem_slice_ptr(self.iter(), shmem_data_ptr as *mut T, builder)?;
+ // Return the new ThinVec, which is just a pointer to the shared memory buffer.
+ let shmem_thinvec: Self = mem::transmute(shmem_header_ptr);
+
+ // Sanity-check that the ptr and length match.
+ debug_assert_eq!(shmem_thinvec.as_ptr(), shmem_data_ptr as *const T);
+ debug_assert_eq!(shmem_thinvec.len(), len);
+
+ Ok(ManuallyDrop::new(shmem_thinvec))
+ }
+ }
+}
+
+impl ToShmem for SmallBitVec {
+ fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> {
+ let storage = match self.clone().into_storage() {
+ InternalStorage::Spilled(vs) => {
+ // Reserve space for the boxed slice values.
+ let len = vs.len();
+ let dest: *mut usize = builder.alloc_array(len);
+
+ unsafe {
+ // Copy the value into the buffer.
+ let src = vs.as_ptr() as *const usize;
+ ptr::copy(src, dest, len);
+
+ let dest_slice =
+ Box::from_raw(slice::from_raw_parts_mut(dest, len) as *mut [usize]);
+ InternalStorage::Spilled(dest_slice)
+ }
+ },
+ InternalStorage::Inline(x) => InternalStorage::Inline(x),
+ };
+ Ok(ManuallyDrop::new(unsafe {
+ SmallBitVec::from_storage(storage)
+ }))
+ }
+}
+
+#[cfg(feature = "string_cache")]
+impl<Static: string_cache::StaticAtomSet> ToShmem for string_cache::Atom<Static> {
+ fn to_shmem(&self, _: &mut SharedMemoryBuilder) -> Result<Self> {
+ // NOTE(emilio): In practice, this can be implemented trivially if
+ // string_cache could expose the implementation detail of static atoms
+ // being an index into the static table (and panicking in the
+ // non-static, non-inline cases).
+ unimplemented!(
+ "If servo wants to share stylesheets across processes, \
+ then ToShmem for Atom needs to be implemented"
+ )
+ }
+}
diff --git a/servo/components/to_shmem_derive/Cargo.toml b/servo/components/to_shmem_derive/Cargo.toml
new file mode 100644
index 0000000000..a3fd73249e
--- /dev/null
+++ b/servo/components/to_shmem_derive/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "to_shmem_derive"
+version = "0.0.1"
+authors = ["The Servo Project Developers"]
+license = "MPL-2.0"
+publish = false
+
+[lib]
+path = "lib.rs"
+proc-macro = true
+
+[dependencies]
+darling = { version = "0.20", default-features = false }
+derive_common = { path = "../derive_common" }
+proc-macro2 = "1"
+quote = "1"
+syn = { version = "2", default-features = false, features = ["derive", "parsing"] }
+synstructure = "0.13"
diff --git a/servo/components/to_shmem_derive/lib.rs b/servo/components/to_shmem_derive/lib.rs
new file mode 100644
index 0000000000..b820e7f85d
--- /dev/null
+++ b/servo/components/to_shmem_derive/lib.rs
@@ -0,0 +1,26 @@
+/* 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/. */
+
+#![recursion_limit = "128"]
+
+#[macro_use]
+extern crate darling;
+extern crate derive_common;
+extern crate proc_macro;
+extern crate proc_macro2;
+#[macro_use]
+extern crate quote;
+#[macro_use]
+extern crate syn;
+extern crate synstructure;
+
+use proc_macro::TokenStream;
+
+mod to_shmem;
+
+#[proc_macro_derive(ToShmem, attributes(shmem))]
+pub fn derive_to_shmem(stream: TokenStream) -> TokenStream {
+ let input = syn::parse(stream).unwrap();
+ to_shmem::derive(input).into()
+}
diff --git a/servo/components/to_shmem_derive/to_shmem.rs b/servo/components/to_shmem_derive/to_shmem.rs
new file mode 100644
index 0000000000..157730c5a5
--- /dev/null
+++ b/servo/components/to_shmem_derive/to_shmem.rs
@@ -0,0 +1,78 @@
+/* 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/. */
+
+use derive_common::cg;
+use proc_macro2::TokenStream;
+use syn;
+use synstructure::{BindStyle, Structure};
+
+pub fn derive(mut input: syn::DeriveInput) -> TokenStream {
+ let mut where_clause = input.generics.where_clause.take();
+ let attrs = cg::parse_input_attrs::<ShmemInputAttrs>(&input);
+ if !attrs.no_bounds {
+ for param in input.generics.type_params() {
+ cg::add_predicate(&mut where_clause, parse_quote!(#param: ::to_shmem::ToShmem));
+ }
+ }
+ for variant in Structure::new(&input).variants() {
+ for binding in variant.bindings() {
+ let attrs = cg::parse_field_attrs::<ShmemFieldAttrs>(&binding.ast());
+ if attrs.field_bound {
+ let ty = &binding.ast().ty;
+ cg::add_predicate(&mut where_clause, parse_quote!(#ty: ::to_shmem::ToShmem))
+ }
+ }
+ }
+
+ input.generics.where_clause = where_clause;
+
+ // Do all of the `to_shmem()?` calls before the `ManuallyDrop::into_inner()`
+ // calls, so that we don't drop a value in the shared memory buffer if one
+ // of the `to_shmem`s fails.
+ let match_body = cg::fmap2_match(
+ &input,
+ BindStyle::Ref,
+ |binding| {
+ quote! {
+ ::to_shmem::ToShmem::to_shmem(#binding, builder)?
+ }
+ },
+ |binding| {
+ Some(quote! {
+ ::std::mem::ManuallyDrop::into_inner(#binding)
+ })
+ },
+ );
+
+ let name = &input.ident;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+ quote! {
+ impl #impl_generics ::to_shmem::ToShmem for #name #ty_generics #where_clause {
+ #[allow(unused_variables, unreachable_code)]
+ fn to_shmem(
+ &self,
+ builder: &mut ::to_shmem::SharedMemoryBuilder,
+ ) -> ::to_shmem::Result<Self> {
+ Ok(::std::mem::ManuallyDrop::new(
+ match *self {
+ #match_body
+ }
+ ))
+ }
+ }
+ }
+}
+
+#[derive(Default, FromDeriveInput)]
+#[darling(attributes(shmem), default)]
+pub struct ShmemInputAttrs {
+ pub no_bounds: bool,
+}
+
+#[derive(Default, FromField)]
+#[darling(attributes(shmem), default)]
+pub struct ShmemFieldAttrs {
+ pub field_bound: bool,
+}