ladybird/Meta/Generators/generate_libweb_css_property_id.py
Andreas Kling 164ed80244 Meta: Enable exit-time destructor warnings for libraries
Enable -Wexit-time-destructors for all in-tree library targets and
update process-lifetime library statics so they no longer register
exit-time destructors. Long-lived caches, lookup tables, singleton
registries, and generated constants now use NeverDestroyed or leaked
references where the data is intended to live until process exit.

Update LibWeb, LibLine, and the binding generators so regenerated
sources follow the same rule instead of reintroducing destructed
statics.
2026-06-04 19:20:49 +02:00

1631 lines
55 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
# Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
# Copyright (c) 2026-present, the Ladybird developers.
#
# SPDX-License-Identifier: BSD-2-Clause
import argparse
import copy
import json
import sys
from pathlib import Path
from typing import TextIO
sys.path.append(str(Path(__file__).resolve().parent.parent))
from Utils.utils import camel_casify
from Utils.utils import snake_casify
from Utils.utils import title_casify
from Utils.utils import underlying_type_for_enum
def is_legacy_alias(prop: dict) -> bool:
return "legacy-alias-for" in prop
def verify_alphabetical(data: dict, path: str) -> None:
most_recent_name = ""
for name in data:
if name < most_recent_name:
print(
f"`{name}` is in the wrong position in `{path}`. Please keep this list alphabetical!", file=sys.stderr
)
sys.exit(1)
most_recent_name = name
def replace_logical_aliases(properties: dict, logical_property_groups: dict) -> None:
# Grab the first physical property in each logical group, to use as the template
first_physical_property_in_logical_group = {}
for group_name, group in logical_property_groups.items():
physical = group["physical"]
if not physical:
raise ValueError(f"No physical properties in group {group_name}")
first_physical_property_in_logical_group[group_name] = next(iter(physical.values()))
logical_aliases = {}
for name, value in properties.items():
logical_alias_for = value.get("logical-alias-for")
if logical_alias_for is not None:
group_name = logical_alias_for.get("group")
if group_name is None:
print(f"Logical alias '{name}' is missing its group", file=sys.stderr)
sys.exit(1)
physical_property_name = first_physical_property_in_logical_group.get(group_name)
if physical_property_name is None:
print(f"Logical property group '{group_name}' not found! (Property: '{name}')", file=sys.stderr)
sys.exit(1)
logical_aliases[name] = physical_property_name
for name, alias in logical_aliases.items():
if alias not in properties:
print(f"No property '{alias}' found for logical alias '{name}'", file=sys.stderr)
sys.exit(1)
alias_object = copy.deepcopy(properties[alias])
# Copy over anything the logical property overrides
for key, val in properties[name].items():
alias_object[key] = val
# Quirks don't carry across to logical aliases
alias_object.pop("quirks", None)
properties[name] = alias_object
def populate_all_property_longhands(properties: dict) -> None:
all_entry = properties.get("all")
if all_entry is None:
raise ValueError("Missing 'all' property")
for name, value in properties.items():
if "longhands" in value or "legacy-alias-for" in value or name == "direction" or name == "unicode-bidi":
continue
all_entry["longhands"].append(name)
def is_animatable_property(properties: dict, property_name: str) -> bool:
prop = properties[property_name]
animation_type = prop.get("animation-type")
if animation_type is not None:
return animation_type != "none"
if "longhands" not in prop:
print(f"Property '{property_name}' must specify either 'animation-type' or 'longhands'", file=sys.stderr)
sys.exit(1)
for subproperty_name in prop["longhands"]:
if is_animatable_property(properties, subproperty_name):
return True
return False
def generate_bounds_checking_function(
out: TextIO, properties: dict, css_type_name: str, type_name: str, value_getter: str = ""
) -> None:
out.write(f"""
bool property_accepts_{css_type_name}(PropertyID property_id, [[maybe_unused]] {type_name} const& value)
{{
switch (property_id) {{
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
valid_types = value.get("valid-types")
if not valid_types:
continue
for valid_type in valid_types:
type_and_range = valid_type.split(" ")
if type_and_range[0] != css_type_name:
continue
out.write(f"""
case PropertyID::{title_casify(name)}:
return """)
if len(type_and_range) > 1:
range_str = type_and_range[1]
if not (range_str.startswith("[") and range_str.endswith("]") and "," in range_str):
raise ValueError(f"Bad range: {range_str}")
comma_index = range_str.index(",")
min_value_string = range_str[1:comma_index]
max_value_string = range_str[comma_index + 1 : -1]
if min_value_string == "-∞":
min_value_string = ""
if max_value_string == "":
max_value_string = ""
if not min_value_string and not max_value_string:
out.write("true;\n")
break
parts = []
if min_value_string:
parts.append(f"{value_getter} >= {min_value_string}")
if max_value_string:
parts.append(f"{value_getter} <= {max_value_string}")
out.write(" && ".join(parts))
out.write(";\n")
else:
out.write("true;\n")
break
out.write("""
default:
return false;
}
}
""")
def format_numeric_range(type_name: str, min_val: str, max_val: str) -> tuple[str, str]:
if type_name == "integer":
min_str = "AK::NumericLimits<i32>::min()" if min_val == "-∞" else min_val
max_str = "AK::NumericLimits<i32>::max()" if max_val == "" else max_val
else:
min_str = "AK::NumericLimits<float>::lowest()" if min_val == "-∞" else min_val
max_str = "AK::NumericLimits<float>::max()" if max_val == "" else max_val
return min_str, max_str
def write_header_file(out: TextIO, properties: dict, logical_property_groups: dict) -> None:
property_id_underlying_type = underlying_type_for_enum(len(properties))
logical_property_group_underlying_type = underlying_type_for_enum(len(logical_property_groups))
out.write(f"""
#pragma once
#include <AK/NonnullRefPtr.h>
#include <AK/StringView.h>
#include <AK/Traits.h>
#include <AK/Variant.h>
#include <LibJS/Forward.h>
#include <LibWeb/CSS/NumericRange.h>
#include <LibWeb/CSS/ValueType.h>
#include <LibWeb/Export.h>
#include <LibWeb/Forward.h>
namespace Web::CSS {{
enum class PropertyID : {property_id_underlying_type} {{
Custom,
""")
shorthand_property_ids = []
inherited_longhand_property_ids = []
noninherited_longhand_property_ids = []
for name, value in properties.items():
# Legacy aliases don't get a PropertyID
if is_legacy_alias(value):
continue
inherited = value.get("inherited")
if "longhands" in value:
if inherited is not None:
print(f"Property '{name}' with longhands cannot specify 'inherited'", file=sys.stderr)
sys.exit(1)
shorthand_property_ids.append(name)
else:
if inherited is None:
print(f"Property '{name}' is missing 'inherited'", file=sys.stderr)
sys.exit(1)
if inherited:
inherited_longhand_property_ids.append(name)
else:
noninherited_longhand_property_ids.append(name)
first_property_id = shorthand_property_ids[0]
last_property_id = noninherited_longhand_property_ids[-1]
first_longhand_property_id = inherited_longhand_property_ids[0]
last_longhand_property_id = noninherited_longhand_property_ids[-1]
first_inherited_property_id = inherited_longhand_property_ids[0]
last_inherited_property_id = inherited_longhand_property_ids[-1]
for ids in (shorthand_property_ids, inherited_longhand_property_ids, noninherited_longhand_property_ids):
for name in ids:
out.write(f"""
{title_casify(name)},
""")
# FIXME: property_accepts_{number,percentage}() and property_accepted_ranges_by_value_type provide the same data, we should consolidate them.
out.write(f"""
}};
enum class AnimationType {{
Discrete,
ByComputedValue,
RepeatableList,
Custom,
None,
}};
AnimationType animation_type_from_longhand_property(PropertyID);
bool is_animatable_property(PropertyID);
Optional<PropertyID> property_id_from_camel_case_string(StringView);
WEB_API Optional<PropertyID> property_id_from_string(StringView);
[[nodiscard]] WEB_API FlyString const& string_from_property_id(PropertyID);
[[nodiscard]] FlyString const& camel_case_string_from_property_id(PropertyID);
WEB_API bool is_inherited_property(PropertyID);
NonnullRefPtr<StyleValue const> property_initial_value(PropertyID);
enum class PropertyMultiplicity {{
Single,
List,
CoordinatingList,
}};
PropertyMultiplicity property_multiplicity(PropertyID);
bool property_is_single_valued(PropertyID);
bool property_is_list_valued(PropertyID);
bool property_accepts_type(PropertyID, ValueType);
NumericRangesByValueType property_accepted_ranges_by_value_type(PropertyID);
bool property_accepts_keyword(PropertyID, Keyword);
Optional<Keyword> resolve_legacy_value_alias(PropertyID, Keyword);
Optional<ValueType> property_resolves_percentages_relative_to(PropertyID);
Vector<StringView> property_custom_ident_blacklist(PropertyID);
// These perform range-checking, but are also safe to call with properties that don't accept that type. (They'll just return false.)
bool property_accepts_angle(PropertyID, Angle const&);
bool property_accepts_flex(PropertyID, Flex const&);
bool property_accepts_frequency(PropertyID, Frequency const&);
bool property_accepts_integer(PropertyID, i64 const&);
bool property_accepts_length(PropertyID, Length const&);
bool property_accepts_number(PropertyID, double const&);
bool property_accepts_percentage(PropertyID, Percentage const&);
bool property_accepts_resolution(PropertyID, Resolution const&);
bool property_accepts_time(PropertyID, Time const&);
bool property_is_shorthand(PropertyID);
Vector<PropertyID> const& longhands_for_shorthand(PropertyID);
Vector<PropertyID> const& expanded_longhands_for_shorthand(PropertyID);
bool property_maps_to_shorthand(PropertyID);
Vector<PropertyID> const& shorthands_for_longhand(PropertyID);
Vector<PropertyID> const& property_computation_order();
bool property_is_positional_value_list_shorthand(PropertyID);
bool property_requires_computation_with_inherited_value(PropertyID);
bool property_requires_computation_with_initial_value(PropertyID);
bool property_requires_computation_with_cascaded_value(PropertyID);
size_t property_maximum_value_count(PropertyID);
bool property_affects_layout(PropertyID);
bool property_affects_stacking_context(PropertyID);
bool property_affects_accumulated_visual_contexts(PropertyID);
bool property_needs_layout_for_getcomputedstyle(PropertyID);
bool property_needs_layout_node_for_resolved_value(PropertyID);
constexpr PropertyID first_property_id = PropertyID::{title_casify(first_property_id)};
constexpr PropertyID last_property_id = PropertyID::{title_casify(last_property_id)};
constexpr PropertyID first_inherited_property_id = PropertyID::{title_casify(first_inherited_property_id)};
constexpr PropertyID last_inherited_property_id = PropertyID::{title_casify(last_inherited_property_id)};
constexpr PropertyID first_longhand_property_id = PropertyID::{title_casify(first_longhand_property_id)};
constexpr PropertyID last_longhand_property_id = PropertyID::{title_casify(last_longhand_property_id)};
constexpr size_t number_of_longhand_properties = to_underlying(last_longhand_property_id) - to_underlying(first_longhand_property_id) + 1;
enum class Quirk {{
// https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk
HashlessHexColor,
// https://quirks.spec.whatwg.org/#the-unitless-length-quirk
UnitlessLength,
}};
bool property_has_quirk(PropertyID, Quirk);
struct LogicalAliasMappingContext {{
WritingMode writing_mode;
Direction direction;
// TODO: text-orientation
}};
bool property_is_logical_alias(PropertyID);
PropertyID map_logical_alias_to_physical_property(PropertyID logical_property_id, LogicalAliasMappingContext const&);
PropertyID map_physical_property_to_logical_alias(PropertyID physical_property_id, LogicalAliasMappingContext const&);
enum class LogicalPropertyGroup : {logical_property_group_underlying_type} {{
""")
for name in logical_property_groups:
out.write(f"""
{title_casify(name)},
""")
out.write("""
};
Optional<LogicalPropertyGroup> logical_property_group_for_property(PropertyID);
} // namespace Web::CSS
namespace AK {
template<>
struct Formatter<Web::CSS::PropertyID> : Formatter<StringView> {
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::PropertyID const& property_id)
{
return Formatter<StringView>::format(builder, Web::CSS::string_from_property_id(property_id));
}
};
} // namespace AK
""")
def write_implementation_file(out: TextIO, properties: dict, logical_property_groups: dict, enum_names: list) -> None:
out.write("""
#include <AK/Assertions.h>
#include <AK/NeverDestroyed.h>
#include <LibWeb/CSS/Enums.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/PropertyName.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/Infra/Strings.h>
namespace Web::CSS {
static auto generate_camel_case_property_table()
{
HashMap<StringView, PropertyID, CaseInsensitiveASCIIStringViewTraits> table;
""")
for name, value in properties.items():
legacy_alias_for = value.get("legacy-alias-for")
target = title_casify(legacy_alias_for) if legacy_alias_for is not None else title_casify(name)
out.write(f"""
table.set("{camel_casify(name)}"sv, PropertyID::{target});
""")
out.write("""
return table;
}
static auto const& camel_case_properties_table = *new HashMap<StringView, PropertyID, CaseInsensitiveASCIIStringViewTraits>(generate_camel_case_property_table());
Optional<PropertyID> property_id_from_camel_case_string(StringView string)
{
return camel_case_properties_table.get(string);
}
static auto generate_properties_table()
{
HashMap<StringView, PropertyID, CaseInsensitiveASCIIStringViewTraits> table;
""")
for name, value in properties.items():
legacy_alias_for = value.get("legacy-alias-for")
target = title_casify(legacy_alias_for) if legacy_alias_for is not None else title_casify(name)
out.write(f"""
table.set("{name}"sv, PropertyID::{target});
""")
out.write("""
return table;
}
static auto const& properties_table = *new HashMap<StringView, PropertyID, CaseInsensitiveASCIIStringViewTraits>(generate_properties_table());
Optional<PropertyID> property_id_from_string(StringView string)
{
if (is_a_custom_property_name_string(string))
return PropertyID::Custom;
return properties_table.get(string);
}
FlyString const& string_from_property_id(PropertyID property_id) {
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
out.write(f"""
case PropertyID::{title_casify(name)}: {{
static FlyString const& name = *new FlyString("{name}"_fly_string);
return name;
}}
""")
out.write("""
default: {
static FlyString const& invalid_property_id_string = *new FlyString("(invalid CSS::PropertyID)"_fly_string);
return invalid_property_id_string;
}
}
}
FlyString const& camel_case_string_from_property_id(PropertyID property_id) {
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
out.write(f"""
case PropertyID::{title_casify(name)}: {{
static FlyString const& name = *new FlyString("{camel_casify(name)}"_fly_string);
return name;
}}
""")
out.write("""
default: {
static FlyString const& invalid_property_id_string = *new FlyString("(invalid CSS::PropertyID)"_fly_string);
return invalid_property_id_string;
}
}
}
AnimationType animation_type_from_longhand_property(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "longhands" in value:
if "animation-type" in value:
print(f"Property '{name}' with longhands cannot specify 'animation-type'", file=sys.stderr)
sys.exit(1)
out.write(f"""
case PropertyID::{title_casify(name)}:
VERIFY_NOT_REACHED();
""")
continue
animation_type = value.get("animation-type")
if animation_type is None:
print(f"No animation-type specified for property '{name}'", file=sys.stderr)
sys.exit(1)
out.write(f"""
case PropertyID::{title_casify(name)}:
return AnimationType::{title_casify(animation_type)};
""")
out.write("""
default:
return AnimationType::None;
}
}
bool is_animatable_property(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if is_animatable_property(properties, name):
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool is_inherited_property(PropertyID property_id)
{
if (property_id >= first_inherited_property_id && property_id <= last_inherited_property_id)
return true;
return false;
}
bool property_affects_layout(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
affects_layout = value.get("affects-layout", True)
if affects_layout:
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool property_affects_stacking_context(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if value.get("affects-stacking-context", False):
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool property_affects_accumulated_visual_contexts(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if value.get("affects-accumulated-visual-contexts", False):
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool property_needs_layout_for_getcomputedstyle(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if value.get("needs-layout-for-getcomputedstyle", False):
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool property_needs_layout_node_for_resolved_value(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if value.get("needs-layout-node-for-resolved-value", False):
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
NonnullRefPtr<StyleValue const> property_initial_value(PropertyID property_id)
{
static NeverDestroyed<Array<RefPtr<StyleValue const>, to_underlying(last_property_id) + 1>> initial_values;
if (auto initial_value = (*initial_values)[to_underlying(property_id)])
return initial_value.release_nonnull();
// Lazily parse initial values as needed.
// This ensures the shorthands will always be able to get the initial values of their longhands.
// This also now allows a longhand have its own longhand (like background-position-x).
Parser::ParsingParams parsing_params;
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "initial" not in value:
print(f"No initial value specified for property '{name}'", file=sys.stderr)
sys.exit(1)
initial_value_string = value["initial"]
title = title_casify(name)
out.write(f""" case PropertyID::{title}:
{{
auto parsed_value = parse_css_value(parsing_params, "{initial_value_string}"sv, PropertyID::{title});
VERIFY(!parsed_value.is_null());
auto initial_value = parsed_value.release_nonnull();
(*initial_values)[to_underlying(PropertyID::{title})] = initial_value;
return initial_value;
}}
""")
out.write(""" default: VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
}
PropertyMultiplicity property_multiplicity(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
multiplicity = value.get("multiplicity")
if multiplicity is not None and multiplicity != "single":
if multiplicity not in ("single", "list", "coordinating-list"):
print(
f"'{multiplicity}' is not a valid value for 'multiplicity'. "
"Accepted values are: 'single', 'list', 'coordinating-list'",
file=sys.stderr,
)
sys.exit(1)
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write(f" return PropertyMultiplicity::{title_casify(multiplicity)};\n")
out.write("""
default:
return PropertyMultiplicity::Single;
}
VERIFY_NOT_REACHED();
}
bool property_is_single_valued(PropertyID property_id)
{
return !property_is_list_valued(property_id);
}
bool property_is_list_valued(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
multiplicity = value.get("multiplicity")
if multiplicity is not None and multiplicity != "single":
if multiplicity not in ("list", "coordinating-list"):
print(
f"'{multiplicity}' is not a valid value for 'multiplicity'. "
"Accepted values are: 'single', 'list', 'coordinating-list'",
file=sys.stderr,
)
sys.exit(1)
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool property_has_quirk(PropertyID property_id, Quirk quirk)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
quirks = value.get("quirks")
if quirks:
out.write(f"""
case PropertyID::{title_casify(name)}: {{
switch (quirk) {{
""")
for quirk in quirks:
out.write(f"""
case Quirk::{title_casify(quirk)}:
return true;
""")
out.write("""
default:
return false;
}
}
""")
out.write("""
default:
return false;
}
}
bool property_accepts_type(PropertyID property_id, ValueType value_type)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
valid_types = value.get("valid-types")
if not valid_types:
continue
out.write(f"""
case PropertyID::{title_casify(name)}: {{
switch (value_type) {{
""")
did_output_accepted_type = False
for type_entry in valid_types:
type_name = type_entry.split(" ")[0]
if type_name in enum_names:
continue
out.write(f" case ValueType::{title_casify(type_name)}:\n")
did_output_accepted_type = True
if did_output_accepted_type:
out.write(" return true;\n")
out.write("""
default:
return false;
}
}
""")
out.write("""
default:
return false;
}
}
NumericRangesByValueType property_accepted_ranges_by_value_type(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
valid_types = value.get("valid-types")
if not valid_types:
continue
ranges_parts = []
for type_entry in valid_types:
type_parts = type_entry.split(" ")
if len(type_parts) < 2:
continue
type_name = type_parts[0]
if type_name == "custom-ident":
continue
# Drop brackets: "[-∞,∞]" -> "-∞,∞"
type_range = type_parts[1][1:-1]
limits = type_range.split(",")
if len(limits) != 2:
raise ValueError(f"Bad range: {type_parts[1]}")
min_str, max_str = format_numeric_range(type_name, limits[0], limits[1])
ranges_parts.append(f"{{ ValueType::{title_casify(type_name)}, {{ {min_str}, {max_str} }} }}")
ranges = ", ".join(ranges_parts)
out.write(f"""
case PropertyID::{title_casify(name)}: {{
return {{ {ranges} }};
}}""")
out.write("""
default: {
return { };
}
}
}
bool property_accepts_keyword(PropertyID property_id, Keyword keyword)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
out.write(f" case PropertyID::{title_casify(name)}: {{\n")
valid_identifiers = value.get("valid-identifiers")
if valid_identifiers:
out.write(" switch (keyword) {\n")
for keyword_string in valid_identifiers:
if ">" in keyword_string:
parts = keyword_string.split(">", 1)
keyword_titlecase = title_casify(parts[0])
else:
keyword_titlecase = title_casify(keyword_string)
out.write(f" case Keyword::{keyword_titlecase}:\n")
out.write("""
return true;
default:
break;
}
""")
valid_types = value.get("valid-types")
if valid_types:
for valid_type in valid_types:
type_name = valid_type.split(" ")[0]
if type_name not in enum_names:
continue
out.write(f"""
if (keyword_to_{snake_casify(type_name)}(keyword).has_value())
return true;
""")
out.write("""
return false;
}
""")
out.write("""
default:
return false;
}
}
Optional<Keyword> resolve_legacy_value_alias(PropertyID property_id, Keyword keyword)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
valid_identifiers = value.get("valid-identifiers")
if not valid_identifiers:
continue
has_any_legacy_value_aliases = any(">" in k for k in valid_identifiers)
if not has_any_legacy_value_aliases:
continue
out.write(f"""
case PropertyID::{title_casify(name)}:
switch (keyword) {{""")
for keyword_string in valid_identifiers:
if ">" not in keyword_string:
continue
parts = keyword_string.split(">", 1)
out.write(f"""
case Keyword::{title_casify(parts[0])}:
return Keyword::{title_casify(parts[1])};""")
out.write("""
default:
break;
}
break;
""")
out.write("""
default:
break;
}
return {};
}
Optional<ValueType> property_resolves_percentages_relative_to(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
resolved_type = value.get("percentages-resolve-to")
if resolved_type is None:
continue
out.write(f"""
case PropertyID::{title_casify(name)}:
return ValueType::{title_casify(resolved_type)};
""")
out.write("""
default:
return {};
}
}
Vector<StringView> property_custom_ident_blacklist(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
valid_types = value.get("valid-types")
if not valid_types:
continue
for valid_type in valid_types:
type_and_parameters = valid_type.split(" ")
if type_and_parameters[0] != "custom-ident" or len(type_and_parameters) == 1:
continue
if len(type_and_parameters) != 2:
raise ValueError(f"Bad custom-ident parameters: {valid_type}")
parameters_string = type_and_parameters[1]
if not (parameters_string.startswith("![") and parameters_string.endswith("]")):
raise ValueError(f"Bad custom-ident parameters: {parameters_string}")
blacklisted_keywords = parameters_string[2:-1].split(",")
out.write(f"""
case PropertyID::{title_casify(name)}:
return Vector {{ """)
for keyword in blacklisted_keywords:
out.write(f'"{keyword}"sv, ')
out.write("};\n")
out.write("""
default:
return {};
}
}
size_t property_maximum_value_count(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "max-values" in value:
max_values = value["max-values"]
out.write(f"""
case PropertyID::{title_casify(name)}:
return {max_values};
""")
out.write("""
default:
return 1;
}
}""")
generate_bounds_checking_function(out, properties, "angle", "Angle", "value.raw_value()")
generate_bounds_checking_function(out, properties, "flex", "Flex", "value.raw_value()")
generate_bounds_checking_function(out, properties, "frequency", "Frequency", "value.raw_value()")
generate_bounds_checking_function(out, properties, "integer", "i64", "value")
generate_bounds_checking_function(out, properties, "length", "Length", "value.raw_value()")
generate_bounds_checking_function(out, properties, "number", "double", "value")
generate_bounds_checking_function(out, properties, "percentage", "Percentage", "value.value()")
generate_bounds_checking_function(out, properties, "resolution", "Resolution", "value.raw_value()")
generate_bounds_checking_function(out, properties, "time", "Time", "value.raw_value()")
out.write("""
bool property_is_shorthand(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "longhands" in value:
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
""")
def get_longhands(property_id: str) -> list:
return list(properties[property_id]["longhands"])
out.write("""
Vector<PropertyID> const& longhands_for_shorthand(PropertyID property_id)
{
switch (property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "longhands" in value:
longhands = ", ".join(f"PropertyID::{title_casify(lh)}" for lh in get_longhands(name))
out.write(f"""
case PropertyID::{title_casify(name)}: {{
static auto const& longhands = *new Vector<PropertyID> {{ {longhands} }};
return longhands;
}}""")
out.write("""
default:
static auto const& empty_longhands = *new Vector<PropertyID>;
return empty_longhands;
}
}
Vector<PropertyID> const& expanded_longhands_for_shorthand(PropertyID property_id)
{
switch (property_id) {
""")
def get_expanded_longhands(property_id: str) -> list:
expanded = []
for longhand_id in get_longhands(property_id):
prop = properties[longhand_id]
if "longhands" in prop:
expanded.extend(get_expanded_longhands(longhand_id))
else:
expanded.append(longhand_id)
return expanded
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "longhands" in value:
longhands = ", ".join(f"PropertyID::{title_casify(lh)}" for lh in get_expanded_longhands(name))
out.write(f"""
case PropertyID::{title_casify(name)}: {{
static auto const& longhands = *new Vector<PropertyID> {{ {longhands} }};
return longhands;
}}""")
out.write("""
default: {
static auto const& empty_longhands = *new Vector<PropertyID>;
return empty_longhands;
}
}
}
""")
# shorthands_for_longhand_map: for each longhand, which shorthand(s) contain it.
shorthands_for_longhand_map = {}
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "longhands" in value:
for longhand_name in value["longhands"]:
shorthands_for_longhand_map.setdefault(longhand_name, []).append(name)
out.write("""
bool property_maps_to_shorthand(PropertyID property_id)
{
switch (property_id) {
""")
for longhand in shorthands_for_longhand_map.keys():
out.write(f" case PropertyID::{title_casify(longhand)}:\n")
out.write("""
return true;
default:
return false;
}
}
Vector<PropertyID> const& shorthands_for_longhand(PropertyID property_id)
{
switch (property_id) {
""")
def get_shorthands_for_longhand(longhand: str) -> list:
shorthands = []
for immediate in shorthands_for_longhand_map.get(longhand, []):
shorthands.append(immediate)
if immediate in shorthands_for_longhand_map:
shorthands.extend(get_shorthands_for_longhand(immediate))
# https://www.w3.org/TR/cssom/#concept-shorthands-preferred-order
# Combined sort key:
# 4. Number of longhand properties (greatest first)
# 2. Items starting with "-" last
# 3. Items starting with "-webkit-" before other "-" items
# 1. Lexicographic
def sort_key(shorthand):
num_longhands = len(get_expanded_longhands(shorthand))
starts_with_dash = shorthand.startswith("-")
starts_with_webkit = shorthand.startswith("-webkit-")
# We want descending on num_longhands, so negate.
# For dash ordering: starts_with_dash True should sort LATER (tuple sort is ascending).
# Among dashed: -webkit- should be first (True < False? no, False < True).
# Actually, if both start with dash, the one starting with -webkit- should come first.
# So (starts_with_dash=True, starts_with_webkit=True) < (starts_with_dash=True, starts_with_webkit=False)
# means starts_with_webkit=True must be smaller: use NOT starts_with_webkit.
return (
-num_longhands,
starts_with_dash,
not starts_with_webkit,
shorthand,
)
shorthands.sort(key=sort_key)
return shorthands
for longhand in shorthands_for_longhand_map.keys():
shorthands = ", ".join(f"PropertyID::{title_casify(s)}" for s in get_shorthands_for_longhand(longhand))
out.write(f"""
case PropertyID::{title_casify(longhand)}: {{
static auto const& shorthands = *new Vector<PropertyID> {{ {shorthands} }};
return shorthands;
}}""")
out.write("""
default: {
static auto const& empty_shorthands = *new Vector<PropertyID>;
return empty_shorthands;
}
}
}
""")
manually_specified_computation_order = [
# math-depth is required to compute font-size
"MathDepth",
# Font properties are required to absolutize font-relative units used in other properties, including line-height.
"FontFamily",
"FontFeatureSettings",
"FontKerning",
"FontOpticalSizing",
"FontSize",
"FontStyle",
"FontVariantAlternates",
"FontVariantCaps",
"FontVariantEastAsian",
"FontVariantEmoji",
"FontVariantLigatures",
"FontVariantNumeric",
"FontVariantPosition",
"FontVariationSettings",
"FontWeight",
"FontWidth",
"TextRendering",
# line-height is required to absolutize `lh` units used in other properties.
"LineHeight",
# color-scheme is included in the generic computation context in order to compute light-dark() color functions
"ColorScheme",
# background-image is required to compute the other background-* properties
"BackgroundImage",
# text direction and writing mode properties are required to map logical properties to their physical counterparts
"Direction",
"WritingMode",
]
out.write("""
Vector<PropertyID> const& property_computation_order() {
static auto const& order = *new Vector<PropertyID> {
""")
for property_name in manually_specified_computation_order:
out.write(f" PropertyID::{property_name},\n")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "longhands" in value:
continue
if title_casify(name) in manually_specified_computation_order:
continue
out.write(f" PropertyID::{title_casify(name)},\n")
out.write("""
};
return order;
}
bool property_is_positional_value_list_shorthand(PropertyID property_id)
{
switch (property_id)
{
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "positional-value-list-shorthand" in value:
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
""")
properties_requiring_inherited = []
properties_requiring_initial = []
properties_requiring_cascaded = []
for name, value in properties.items():
if is_legacy_alias(value):
continue
requires_computation = value.get("requires-computation")
if requires_computation is not None and "longhands" in value:
print(f"Property '{name}' is a shorthand and cannot have 'requires-computation' set.", file=sys.stderr)
sys.exit(1)
if "longhands" in value:
continue
if requires_computation is None:
print(f"Property '{name}' is missing 'requires-computation' field.", file=sys.stderr)
sys.exit(1)
if requires_computation == "always":
properties_requiring_inherited.append(name)
properties_requiring_initial.append(name)
properties_requiring_cascaded.append(name)
elif requires_computation == "non-inherited-value":
properties_requiring_initial.append(name)
properties_requiring_cascaded.append(name)
elif requires_computation == "cascaded-value":
properties_requiring_cascaded.append(name)
elif requires_computation != "never":
print(
f"Property '{name}' has unrecognized 'requires-computation' value '{requires_computation}'",
file=sys.stderr,
)
sys.exit(1)
out.write("""
bool property_requires_computation_with_inherited_value(PropertyID property_id)
{
switch(property_id) {
""")
for property_name in properties_requiring_inherited:
out.write(f" case PropertyID::{title_casify(property_name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool property_requires_computation_with_initial_value(PropertyID property_id)
{
switch(property_id) {
""")
for property_name in properties_requiring_initial:
out.write(f" case PropertyID::{title_casify(property_name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool property_requires_computation_with_cascaded_value(PropertyID property_id)
{
switch(property_id) {
""")
for property_name in properties_requiring_cascaded:
out.write(f" case PropertyID::{title_casify(property_name)}:\n")
out.write("""
return true;
default:
return false;
}
}
bool property_is_logical_alias(PropertyID property_id)
{
switch(property_id) {
""")
for name, value in properties.items():
if is_legacy_alias(value):
continue
if "logical-alias-for" in value:
out.write(f" case PropertyID::{title_casify(name)}:\n")
out.write("""
return true;
default:
return false;
}
}
PropertyID map_logical_alias_to_physical_property(PropertyID property_id, LogicalAliasMappingContext const& mapping_context)
{
// https://drafts.csswg.org/css-writing-modes-4/#logical-to-physical
// FIXME: Note: The used direction depends on the computed writing-mode and text-orientation: in vertical writing
// modes, a text-orientation value of upright forces the used direction to ltr.
auto used_direction = mapping_context.direction;
switch(property_id) {
""")
for property_name, value in properties.items():
if is_legacy_alias(value):
continue
logical_alias_for = value.get("logical-alias-for")
if logical_alias_for is None:
continue
group_name = logical_alias_for.get("group")
mapping = logical_alias_for.get("mapping")
if group_name is None or mapping is None:
print(f"Logical alias '{property_name}' is missing either its group or its mapping!", file=sys.stderr)
sys.exit(1)
group = logical_property_groups.get(group_name)
if group is None:
print(f"Logical alias '{property_name}' has unrecognized group '{group_name}'", file=sys.stderr)
sys.exit(1)
def mapped_property(entry_name: str, group=group, group_name=group_name, property_name=property_name) -> str:
physical = group["physical"]
if entry_name not in physical:
print(
f"Logical property group '{group_name}' is missing entry for '{entry_name}', "
f"requested by property '{property_name}'.",
file=sys.stderr,
)
sys.exit(1)
return title_casify(physical[entry_name])
name_titlecase = title_casify(property_name)
out.write(f" case PropertyID::{name_titlecase}:\n")
if mapping == "block-end":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb)
return PropertyID::{mapped_property("bottom")};
if (first_is_one_of(mapping_context.writing_mode, WritingMode::VerticalRl, WritingMode::SidewaysRl))
return PropertyID::{mapped_property("left")};
return PropertyID::{mapped_property("right")};
""")
elif mapping == "block-size":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb)
return PropertyID::{mapped_property("height")};
return PropertyID::{mapped_property("width")};
""")
elif mapping == "block-start":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb)
return PropertyID::{mapped_property("top")};
if (first_is_one_of(mapping_context.writing_mode, WritingMode::VerticalRl, WritingMode::SidewaysRl))
return PropertyID::{mapped_property("right")};
return PropertyID::{mapped_property("left")};
""")
elif mapping == "end-end":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom-right")};
return PropertyID::{mapped_property("bottom-left")};
}}
if (first_is_one_of(mapping_context.writing_mode, WritingMode::VerticalRl, WritingMode::SidewaysRl)) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom-left")};
return PropertyID::{mapped_property("top-left")};
}}
if (mapping_context.writing_mode == WritingMode::VerticalLr) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom-right")};
return PropertyID::{mapped_property("top-right")};
}}
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top-right")};
return PropertyID::{mapped_property("bottom-right")};
""")
elif mapping == "end-start":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom-left")};
return PropertyID::{mapped_property("bottom-right")};
}}
if (first_is_one_of(mapping_context.writing_mode, WritingMode::VerticalRl, WritingMode::SidewaysRl)) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top-left")};
return PropertyID::{mapped_property("bottom-left")};
}}
if (mapping_context.writing_mode == WritingMode::VerticalLr) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top-right")};
return PropertyID::{mapped_property("bottom-right")};
}}
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom-right")};
return PropertyID::{mapped_property("top-right")};
""")
elif mapping == "inline-end":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("right")};
return PropertyID::{mapped_property("left")};
}}
if (first_is_one_of(mapping_context.writing_mode, WritingMode::VerticalRl, WritingMode::SidewaysRl, WritingMode::VerticalLr)) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom")};
return PropertyID::{mapped_property("top")};
}}
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top")};
return PropertyID::{mapped_property("bottom")};
""")
elif mapping == "inline-size":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb)
return PropertyID::{mapped_property("width")};
return PropertyID::{mapped_property("height")};
""")
elif mapping == "inline-start":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("left")};
return PropertyID::{mapped_property("right")};
}}
if (first_is_one_of(mapping_context.writing_mode, WritingMode::VerticalRl, WritingMode::SidewaysRl, WritingMode::VerticalLr)) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top")};
return PropertyID::{mapped_property("bottom")};
}}
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom")};
return PropertyID::{mapped_property("top")};
""")
elif mapping == "start-end":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top-right")};
return PropertyID::{mapped_property("top-left")};
}}
if (first_is_one_of(mapping_context.writing_mode, WritingMode::VerticalRl, WritingMode::SidewaysRl)) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom-right")};
return PropertyID::{mapped_property("top-right")};
}}
if (mapping_context.writing_mode == WritingMode::VerticalLr) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom-left")};
return PropertyID::{mapped_property("top-left")};
}}
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top-left")};
return PropertyID::{mapped_property("bottom-left")};
""")
elif mapping == "start-start":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top-left")};
return PropertyID::{mapped_property("top-right")};
}}
if (first_is_one_of(mapping_context.writing_mode, WritingMode::VerticalRl, WritingMode::SidewaysRl)) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top-right")};
return PropertyID::{mapped_property("bottom-right")};
}}
if (mapping_context.writing_mode == WritingMode::VerticalLr) {{
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("top-left")};
return PropertyID::{mapped_property("bottom-left")};
}}
if (used_direction == Direction::Ltr)
return PropertyID::{mapped_property("bottom-left")};
return PropertyID::{mapped_property("top-left")};
""")
elif mapping == "block-xy":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb)
return PropertyID::{mapped_property("y")};
return PropertyID::{mapped_property("x")};
""")
elif mapping == "inline-xy":
out.write(f"""
if (mapping_context.writing_mode == WritingMode::HorizontalTb)
return PropertyID::{mapped_property("x")};
return PropertyID::{mapped_property("y")};
""")
else:
print(f"Logical alias '{property_name}' has unrecognized mapping '{mapping}'", file=sys.stderr)
sys.exit(1)
out.write("""
default:
VERIFY(!property_is_logical_alias(property_id));
return property_id;
}
}
PropertyID map_physical_property_to_logical_alias(PropertyID property_id, LogicalAliasMappingContext const& mapping_context)
{
switch (property_id) {
""")
for group in logical_property_groups.values():
physical_properties = group["physical"]
logical_properties = group["logical"]
for physical_property_name in physical_properties.values():
out.write(f" case PropertyID::{title_casify(physical_property_name)}:\n")
for logical_property_name in logical_properties.values():
out.write(f"""
if (map_logical_alias_to_physical_property(PropertyID::{title_casify(logical_property_name)}, mapping_context) == property_id)
return PropertyID::{title_casify(logical_property_name)};
""")
out.write(" VERIFY_NOT_REACHED();\n")
out.write("""
default:
VERIFY(!logical_property_group_for_property(property_id).has_value() || property_is_logical_alias(property_id));
return property_id;
}
}
Optional<LogicalPropertyGroup> logical_property_group_for_property(PropertyID property_id)
{
switch(property_id) {
""")
# Build HashMap<String, Vector<String>> of all properties (physical + logical) per group
logical_property_group_members = {}
for group_name, mapping in logical_property_groups.items():
members = logical_property_group_members.setdefault(group_name, [])
for physical_property in mapping["physical"].values():
members.append(physical_property)
for logical_property in mapping["logical"].values():
members.append(logical_property)
for group_name, group_properties in logical_property_group_members.items():
for prop_name in group_properties:
out.write(f" case PropertyID::{title_casify(prop_name)}:\n")
out.write(f" return LogicalPropertyGroup::{title_casify(group_name)};\n")
out.write("""
default:
return {};
}
}
} // namespace Web::CSS
""")
def main():
parser = argparse.ArgumentParser(description="Generate CSS PropertyID", add_help=False)
parser.add_argument("--help", action="help", help="Show this help message and exit")
parser.add_argument("-h", "--header", required=True, help="Path to the PropertyID header file to generate")
parser.add_argument(
"-c", "--implementation", required=True, help="Path to the PropertyID implementation file to generate"
)
parser.add_argument("-j", "--properties-json", required=True, help="Path to the properties JSON file to read from")
parser.add_argument("-e", "--enums-json", required=True, help="Path to the enums JSON file to read from")
parser.add_argument(
"-g", "--groups-json", required=True, help="Path to the logical property groups JSON file to read from"
)
args = parser.parse_args()
with open(args.properties_json, "r", encoding="utf-8") as f:
properties = json.load(f)
with open(args.enums_json, "r", encoding="utf-8") as f:
enums = json.load(f)
with open(args.groups_json, "r", encoding="utf-8") as f:
logical_property_groups = json.load(f)
verify_alphabetical(properties, args.properties_json)
verify_alphabetical(logical_property_groups, args.groups_json)
verify_alphabetical(enums, args.enums_json)
enum_names = list(enums.keys())
replace_logical_aliases(properties, logical_property_groups)
populate_all_property_longhands(properties)
with open(args.header, "w", encoding="utf-8") as f:
write_header_file(f, properties, logical_property_groups)
with open(args.implementation, "w", encoding="utf-8") as f:
write_implementation_file(f, properties, logical_property_groups, enum_names)
if __name__ == "__main__":
main()