ladybird/Meta/Generators/generate_libweb_css_descriptors.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

418 lines
12 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2022-2026, Sam Atkins <sam@ladybird.org>
# Copyright (c) 2024, Luke Wilde <luke@ladybird.org>
# Copyright (c) 2026-present, the Ladybird developers.
#
# SPDX-License-Identifier: BSD-2-Clause
import argparse
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 title_casify
from Utils.utils import underlying_type_for_enum
def is_legacy_alias(descriptor: dict) -> bool:
return "legacy-alias-for" in descriptor
def collect_all_descriptors(at_rules_data: dict) -> list:
names = set()
for at_rule in at_rules_data.values():
for descriptor_name, descriptor in at_rule.get("descriptors", {}).items():
if is_legacy_alias(descriptor):
continue
names.add(descriptor_name)
return sorted(names)
def write_header_file(out: TextIO, at_rules_data: dict, all_descriptors: list) -> None:
at_rule_count = len(at_rules_data)
at_rule_id_underlying_type = underlying_type_for_enum(at_rule_count)
descriptor_id_underlying_type = underlying_type_for_enum(len(all_descriptors))
out.write(f"""
#pragma once
#include <AK/FlyString.h>
#include <AK/Optional.h>
#include <AK/Types.h>
#include <LibWeb/Forward.h>
namespace Web::CSS {{
enum class AtRuleID : {at_rule_id_underlying_type} {{
""")
for at_rule_name in at_rules_data:
out.write(f" {title_casify(at_rule_name)},\n")
out.write(f"""
}};
FlyString to_string(AtRuleID);
enum class DescriptorID : {descriptor_id_underlying_type} {{
""")
for descriptor_name in all_descriptors:
out.write(f" {title_casify(descriptor_name)},\n")
out.write("""
Custom,
};
Optional<DescriptorID> descriptor_id_from_string(AtRuleID, StringView);
FlyString to_string(DescriptorID);
bool at_rule_supports_descriptor(AtRuleID, DescriptorID);
RefPtr<StyleValue const> descriptor_initial_value(AtRuleID, DescriptorID);
struct DescriptorMetadata {
enum class ValueType {
// FIXME: Parse the grammar instead of hard-coding all the options!
CounterStyleSystem,
CounterStyleAdditiveSymbols,
CounterStyleName,
CounterStyleNegative,
CounterStylePad,
CounterStyleRange,
CropOrCross,
FamilyName,
FontSrcList,
FontWeightAbsolutePair,
Length,
OptionalDeclarationValue,
PageSize,
PositivePercentage,
String,
Symbol,
Symbols,
UnicodeRangeTokens,
};
Vector<Variant<Keyword, PropertyID, ValueType>> syntax;
bool allow_arbitrary_substitution_functions { false };
};
DescriptorMetadata get_descriptor_metadata(AtRuleID, DescriptorID);
}
""")
VALUE_TYPE_NAMES = {
"<family-name>": "FamilyName",
"<font-src-list>": "FontSrcList",
"<font-weight-absolute>{1,2}": "FontWeightAbsolutePair",
"<declaration-value>?": "OptionalDeclarationValue",
"<length>": "Length",
"<page-size>": "PageSize",
"<percentage [0,∞]>": "PositivePercentage",
"<string>": "String",
"<unicode-range-token>#": "UnicodeRangeTokens",
"<counter-style-system>": "CounterStyleSystem",
"<counter-style-negative>": "CounterStyleNegative",
"<symbol>": "Symbol",
"<symbol>+": "Symbols",
"<counter-style-range>": "CounterStyleRange",
"<counter-style-pad>": "CounterStylePad",
"<counter-style-name>": "CounterStyleName",
"<counter-style-additive-symbols>": "CounterStyleAdditiveSymbols",
}
def generate_syntax_list(out: TextIO, syntax: list) -> None:
for entry in syntax:
if entry.startswith("<'"):
# Property reference like <'font-feature-settings'>
property_name = title_casify(entry[2:-2])
out.write(f"""
metadata.syntax.empend(PropertyID::{property_name});
""")
elif entry.startswith("<"):
# Value type
# FIXME: Actually parse the grammar, instead of hard-coding the options!
if entry not in VALUE_TYPE_NAMES:
print(f"Unrecognized value type: `{entry}`", file=sys.stderr)
sys.exit(1)
value_type = VALUE_TYPE_NAMES[entry]
out.write(f"""
metadata.syntax.empend(DescriptorMetadata::ValueType::{value_type});
""")
elif entry == "crop || cross":
# FIXME: This is extra hacky.
out.write("""
metadata.syntax.empend(DescriptorMetadata::ValueType::CropOrCross);
""")
else:
# Keyword
out.write(f"""
metadata.syntax.empend(Keyword::{title_casify(entry)});
""")
def write_implementation_file(out: TextIO, at_rules_data: dict, all_descriptors: list) -> None:
at_rule_count = len(at_rules_data)
descriptor_count = len(all_descriptors)
out.write("""
#include <AK/NeverDestroyed.h>
#include <LibWeb/CSS/DescriptorID.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/PropertyID.h>
namespace Web::CSS {
FlyString to_string(AtRuleID at_rule_id)
{
switch (at_rule_id) {
""")
for at_rule_name in at_rules_data:
out.write(f"""
case AtRuleID::{title_casify(at_rule_name)}:
return "@{at_rule_name}"_fly_string;
""")
out.write("""
}
VERIFY_NOT_REACHED();
}
Optional<DescriptorID> descriptor_id_from_string(AtRuleID at_rule_id, StringView string)
{
switch (at_rule_id) {
""")
for at_rule_name, at_rule in at_rules_data.items():
out.write(f"""
case AtRuleID::{title_casify(at_rule_name)}:
""")
if "custom-descriptors" in at_rule:
out.write("""
if (is_a_custom_property_name_string(string))
return DescriptorID::Custom;
""")
descriptors = at_rule["descriptors"]
for descriptor_name, descriptor in descriptors.items():
alias_for = descriptor.get("legacy-alias-for")
result_name = title_casify(alias_for) if alias_for is not None else title_casify(descriptor_name)
out.write(f"""
if (string.equals_ignoring_ascii_case("{descriptor_name}"sv))
return DescriptorID::{result_name};
""")
out.write("""
break;
""")
out.write("""
}
return {};
}
FlyString to_string(DescriptorID descriptor_id)
{
switch (descriptor_id) {
""")
for descriptor_name in all_descriptors:
out.write(f"""
case DescriptorID::{title_casify(descriptor_name)}:
return "{descriptor_name}"_fly_string;
""")
out.write("""
case DescriptorID::Custom:
VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
}
bool at_rule_supports_descriptor(AtRuleID at_rule_id, DescriptorID descriptor_id)
{
switch (at_rule_id) {
""")
for at_rule_name, at_rule in at_rules_data.items():
out.write(f"""
case AtRuleID::{title_casify(at_rule_name)}:
switch (descriptor_id) {{
""")
descriptors = at_rule["descriptors"]
for descriptor_name, descriptor in descriptors.items():
if is_legacy_alias(descriptor):
continue
out.write(f" case DescriptorID::{title_casify(descriptor_name)}:\n")
if "custom-descriptors" in at_rule:
out.write("""
case DescriptorID::Custom:
""")
out.write("""
return true;
default:
return false;
}
""")
out.write(f"""
}}
VERIFY_NOT_REACHED();
}}
RefPtr<StyleValue const> descriptor_initial_value(AtRuleID at_rule_id, DescriptorID descriptor_id)
{{
if (!at_rule_supports_descriptor(at_rule_id, descriptor_id))
return nullptr;
static NeverDestroyed<Array<Array<RefPtr<StyleValue const>, {descriptor_count}>, {at_rule_count}>> initial_values;
if (auto initial_value = (*initial_values)[to_underlying(at_rule_id)][to_underlying(descriptor_id)])
return initial_value.release_nonnull();
// Lazily parse initial values as needed.
Parser::ParsingParams parsing_params;
switch (at_rule_id) {{
""")
for at_rule_name, at_rule in at_rules_data.items():
at_rule_titlecase = title_casify(at_rule_name)
out.write(f"""
case AtRuleID::{at_rule_titlecase}:
switch (descriptor_id) {{
""")
descriptors = at_rule["descriptors"]
for descriptor_name, descriptor in descriptors.items():
if is_legacy_alias(descriptor):
continue
descriptor_titlecase = title_casify(descriptor_name)
initial_value = descriptor.get("initial")
if initial_value is not None:
out.write(f"""
case DescriptorID::{descriptor_titlecase}: {{
auto parsed_value = parse_css_descriptor(parsing_params, AtRuleID::{at_rule_titlecase}, DescriptorNameAndID::from_id(DescriptorID::{descriptor_titlecase}), "{initial_value}"sv);
VERIFY(!parsed_value.is_null());
auto initial_value = parsed_value.release_nonnull();
(*initial_values)[to_underlying(at_rule_id)][to_underlying(descriptor_id)] = initial_value;
return initial_value;
}}
""")
else:
out.write(f"""
case DescriptorID::{descriptor_titlecase}:
return nullptr;
""")
out.write("""
default:
VERIFY_NOT_REACHED();
}
""")
out.write("""
}
VERIFY_NOT_REACHED();
}
DescriptorMetadata get_descriptor_metadata(AtRuleID at_rule_id, DescriptorID descriptor_id)
{
switch (at_rule_id) {
""")
for at_rule_name, at_rule in at_rules_data.items():
out.write(f"""
case AtRuleID::{title_casify(at_rule_name)}:
switch (descriptor_id) {{
""")
descriptors = at_rule["descriptors"]
for descriptor_name, descriptor in descriptors.items():
if is_legacy_alias(descriptor):
continue
descriptor_titlecase = title_casify(descriptor_name)
out.write(f"""
case DescriptorID::{descriptor_titlecase}: {{
DescriptorMetadata metadata;
""")
generate_syntax_list(out, descriptor["syntax"])
allow_arbitrary = "true" if descriptor.get("allow-arbitrary-substitution-functions", False) else "false"
out.write(f"""
metadata.allow_arbitrary_substitution_functions = {allow_arbitrary};
return metadata;
}}
""")
if "custom-descriptors" in at_rule:
custom_descriptors = at_rule["custom-descriptors"]
out.write("""
case DescriptorID::Custom: {
DescriptorMetadata metadata;
""")
generate_syntax_list(out, custom_descriptors["syntax"])
allow_arbitrary = (
"true" if custom_descriptors.get("allow-arbitrary-substitution-functions", False) else "false"
)
out.write(f"""
metadata.allow_arbitrary_substitution_functions = {allow_arbitrary};
return metadata;
}}
""")
out.write("""
default:
VERIFY_NOT_REACHED();
}
""")
out.write("""
}
VERIFY_NOT_REACHED();
}
}
""")
def main():
parser = argparse.ArgumentParser(description="Generate CSS DescriptorID", 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 DescriptorID header file to generate")
parser.add_argument(
"-c",
"--implementation",
required=True,
help="Path to the DescriptorID implementation file to generate",
)
parser.add_argument("-j", "--json", required=True, help="Path to the JSON file to read from")
args = parser.parse_args()
with open(args.json, "r", encoding="utf-8") as input_file:
at_rules_data = json.load(input_file)
all_descriptors = collect_all_descriptors(at_rules_data)
with open(args.header, "w", encoding="utf-8") as output_file:
write_header_file(output_file, at_rules_data, all_descriptors)
with open(args.implementation, "w", encoding="utf-8") as output_file:
write_implementation_file(output_file, at_rules_data, all_descriptors)
if __name__ == "__main__":
main()