Serenity Operating System
at master 610 lines 23 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021-2022, Sam Atkins <atkinssj@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "GeneratorUtil.h" 9#include <AK/SourceGenerator.h> 10#include <AK/StringBuilder.h> 11#include <LibCore/ArgsParser.h> 12#include <LibMain/Main.h> 13 14ErrorOr<void> generate_header_file(JsonObject& properties, Core::File& file); 15ErrorOr<void> generate_implementation_file(JsonObject& properties, Core::File& file); 16 17ErrorOr<int> serenity_main(Main::Arguments arguments) 18{ 19 StringView generated_header_path; 20 StringView generated_implementation_path; 21 StringView properties_json_path; 22 23 Core::ArgsParser args_parser; 24 args_parser.add_option(generated_header_path, "Path to the PropertyID header file to generate", "generated-header-path", 'h', "generated-header-path"); 25 args_parser.add_option(generated_implementation_path, "Path to the PropertyID implementation file to generate", "generated-implementation-path", 'c', "generated-implementation-path"); 26 args_parser.add_option(properties_json_path, "Path to the JSON file to read from", "json-path", 'j', "json-path"); 27 args_parser.parse(arguments); 28 29 auto json = TRY(read_entire_file_as_json(properties_json_path)); 30 VERIFY(json.is_object()); 31 auto properties = json.as_object(); 32 33 auto generated_header_file = TRY(Core::File::open(generated_header_path, Core::File::OpenMode::Write)); 34 auto generated_implementation_file = TRY(Core::File::open(generated_implementation_path, Core::File::OpenMode::Write)); 35 36 TRY(generate_header_file(properties, *generated_header_file)); 37 TRY(generate_implementation_file(properties, *generated_implementation_file)); 38 39 return 0; 40} 41 42ErrorOr<void> generate_header_file(JsonObject& properties, Core::File& file) 43{ 44 StringBuilder builder; 45 SourceGenerator generator { builder }; 46 generator.append(R"~~~( 47#pragma once 48 49#include <AK/NonnullRefPtr.h> 50#include <AK/StringView.h> 51#include <AK/Traits.h> 52#include <LibJS/Forward.h> 53#include <LibWeb/Forward.h> 54 55namespace Web::CSS { 56 57enum class PropertyID { 58 Invalid, 59 Custom, 60)~~~"); 61 62 Vector<DeprecatedString> shorthand_property_ids; 63 Vector<DeprecatedString> longhand_property_ids; 64 65 properties.for_each_member([&](auto& name, auto& value) { 66 VERIFY(value.is_object()); 67 if (value.as_object().has("longhands"sv)) 68 shorthand_property_ids.append(name); 69 else 70 longhand_property_ids.append(name); 71 }); 72 73 auto first_property_id = shorthand_property_ids.first(); 74 auto last_property_id = longhand_property_ids.last(); 75 76 for (auto& name : shorthand_property_ids) { 77 auto member_generator = generator.fork(); 78 member_generator.set("name:titlecase", title_casify(name)); 79 80 member_generator.append(R"~~~( 81 @name:titlecase@, 82)~~~"); 83 } 84 85 for (auto& name : longhand_property_ids) { 86 auto member_generator = generator.fork(); 87 member_generator.set("name:titlecase", title_casify(name)); 88 89 member_generator.append(R"~~~( 90 @name:titlecase@, 91)~~~"); 92 } 93 94 generator.set("first_property_id", title_casify(first_property_id)); 95 generator.set("last_property_id", title_casify(last_property_id)); 96 97 generator.set("first_shorthand_property_id", title_casify(shorthand_property_ids.first())); 98 generator.set("last_shorthand_property_id", title_casify(shorthand_property_ids.last())); 99 100 generator.set("first_longhand_property_id", title_casify(longhand_property_ids.first())); 101 generator.set("last_longhand_property_id", title_casify(longhand_property_ids.last())); 102 103 generator.append(R"~~~( 104}; 105 106PropertyID property_id_from_camel_case_string(StringView); 107PropertyID property_id_from_string(StringView); 108StringView string_from_property_id(PropertyID); 109bool is_inherited_property(PropertyID); 110NonnullRefPtr<StyleValue> property_initial_value(JS::Realm&, PropertyID); 111 112bool property_accepts_value(PropertyID, StyleValue&); 113size_t property_maximum_value_count(PropertyID); 114 115bool property_affects_layout(PropertyID); 116bool property_affects_stacking_context(PropertyID); 117 118constexpr PropertyID first_property_id = PropertyID::@first_property_id@; 119constexpr PropertyID last_property_id = PropertyID::@last_property_id@; 120constexpr PropertyID first_shorthand_property_id = PropertyID::@first_shorthand_property_id@; 121constexpr PropertyID last_shorthand_property_id = PropertyID::@last_shorthand_property_id@; 122constexpr PropertyID first_longhand_property_id = PropertyID::@first_longhand_property_id@; 123constexpr PropertyID last_longhand_property_id = PropertyID::@last_longhand_property_id@; 124 125enum class Quirk { 126 // https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk 127 HashlessHexColor, 128 // https://quirks.spec.whatwg.org/#the-unitless-length-quirk 129 UnitlessLength, 130}; 131bool property_has_quirk(PropertyID, Quirk); 132 133} // namespace Web::CSS 134 135namespace AK { 136template<> 137struct Traits<Web::CSS::PropertyID> : public GenericTraits<Web::CSS::PropertyID> { 138 static unsigned hash(Web::CSS::PropertyID property_id) { return int_hash((unsigned)property_id); } 139}; 140} // namespace AK 141)~~~"); 142 143 TRY(file.write_until_depleted(generator.as_string_view().bytes())); 144 return {}; 145} 146 147ErrorOr<void> generate_implementation_file(JsonObject& properties, Core::File& file) 148{ 149 StringBuilder builder; 150 SourceGenerator generator { builder }; 151 152 generator.append(R"~~~( 153#include <AK/Assertions.h> 154#include <LibWeb/CSS/Enums.h> 155#include <LibWeb/CSS/Parser/Parser.h> 156#include <LibWeb/CSS/PropertyID.h> 157#include <LibWeb/CSS/StyleValue.h> 158#include <LibWeb/Infra/Strings.h> 159 160namespace Web::CSS { 161 162PropertyID property_id_from_camel_case_string(StringView string) 163{ 164)~~~"); 165 166 properties.for_each_member([&](auto& name, auto& value) { 167 VERIFY(value.is_object()); 168 169 auto member_generator = generator.fork(); 170 member_generator.set("name", name); 171 member_generator.set("name:titlecase", title_casify(name)); 172 member_generator.set("name:camelcase", camel_casify(name)); 173 member_generator.append(R"~~~( 174 if (string.equals_ignoring_ascii_case("@name:camelcase@"sv)) 175 return PropertyID::@name:titlecase@; 176)~~~"); 177 }); 178 179 generator.append(R"~~~( 180 return PropertyID::Invalid; 181} 182 183PropertyID property_id_from_string(StringView string) 184{ 185)~~~"); 186 187 properties.for_each_member([&](auto& name, auto& value) { 188 VERIFY(value.is_object()); 189 190 auto member_generator = generator.fork(); 191 member_generator.set("name", name); 192 member_generator.set("name:titlecase", title_casify(name)); 193 member_generator.append(R"~~~( 194 if (Infra::is_ascii_case_insensitive_match(string, "@name@"sv)) 195 return PropertyID::@name:titlecase@; 196)~~~"); 197 }); 198 199 generator.append(R"~~~( 200 return PropertyID::Invalid; 201} 202 203StringView string_from_property_id(PropertyID property_id) { 204 switch (property_id) { 205)~~~"); 206 207 properties.for_each_member([&](auto& name, auto& value) { 208 VERIFY(value.is_object()); 209 210 auto member_generator = generator.fork(); 211 member_generator.set("name", name); 212 member_generator.set("name:titlecase", title_casify(name)); 213 member_generator.append(R"~~~( 214 case PropertyID::@name:titlecase@: 215 return "@name@"sv; 216)~~~"); 217 }); 218 219 generator.append(R"~~~( 220 default: 221 return "(invalid CSS::PropertyID)"sv; 222 } 223} 224 225bool is_inherited_property(PropertyID property_id) 226{ 227 switch (property_id) { 228)~~~"); 229 230 properties.for_each_member([&](auto& name, auto& value) { 231 VERIFY(value.is_object()); 232 233 bool inherited = false; 234 if (value.as_object().has("inherited"sv)) { 235 auto inherited_value = value.as_object().get_bool("inherited"sv); 236 VERIFY(inherited_value.has_value()); 237 inherited = inherited_value.value(); 238 } 239 240 if (inherited) { 241 auto member_generator = generator.fork(); 242 member_generator.set("name:titlecase", title_casify(name)); 243 member_generator.append(R"~~~( 244 case PropertyID::@name:titlecase@: 245 return true; 246)~~~"); 247 } 248 }); 249 250 generator.append(R"~~~( 251 default: 252 return false; 253 } 254} 255 256bool property_affects_layout(PropertyID property_id) 257{ 258 switch (property_id) { 259)~~~"); 260 261 properties.for_each_member([&](auto& name, auto& value) { 262 VERIFY(value.is_object()); 263 264 bool affects_layout = true; 265 if (value.as_object().has("affects-layout"sv)) 266 affects_layout = value.as_object().get_bool("affects-layout"sv).value_or(false); 267 268 if (affects_layout) { 269 auto member_generator = generator.fork(); 270 member_generator.set("name:titlecase", title_casify(name)); 271 member_generator.append(R"~~~( 272 case PropertyID::@name:titlecase@: 273)~~~"); 274 } 275 }); 276 277 generator.append(R"~~~( 278 return true; 279 default: 280 return false; 281 } 282} 283 284bool property_affects_stacking_context(PropertyID property_id) 285{ 286 switch (property_id) { 287)~~~"); 288 289 properties.for_each_member([&](auto& name, auto& value) { 290 VERIFY(value.is_object()); 291 292 bool affects_stacking_context = false; 293 if (value.as_object().has("affects-stacking-context"sv)) 294 affects_stacking_context = value.as_object().get_bool("affects-stacking-context"sv).value_or(false); 295 296 if (affects_stacking_context) { 297 auto member_generator = generator.fork(); 298 member_generator.set("name:titlecase", title_casify(name)); 299 member_generator.append(R"~~~( 300 case PropertyID::@name:titlecase@: 301)~~~"); 302 } 303 }); 304 305 generator.append(R"~~~( 306 return true; 307 default: 308 return false; 309 } 310} 311 312NonnullRefPtr<StyleValue> property_initial_value(JS::Realm& context_realm, PropertyID property_id) 313{ 314 static Array<RefPtr<StyleValue>, to_underlying(last_property_id) + 1> initial_values; 315 static bool initialized = false; 316 if (!initialized) { 317 initialized = true; 318 Parser::ParsingContext parsing_context(context_realm); 319)~~~"); 320 321 // NOTE: Parsing a shorthand property requires that its longhands are already available here. 322 // So, we do this in two passes: First longhands, then shorthands. 323 // Probably we should build a dependency graph and then handle them in order, but this 324 // works for now! :^) 325 326 auto output_initial_value_code = [&](auto& name, auto& object) { 327 if (!object.has("initial"sv)) { 328 dbgln("No initial value specified for property '{}'", name); 329 VERIFY_NOT_REACHED(); 330 } 331 auto initial_value = object.get_deprecated_string("initial"sv); 332 VERIFY(initial_value.has_value()); 333 auto& initial_value_string = initial_value.value(); 334 335 auto member_generator = generator.fork(); 336 member_generator.set("name:titlecase", title_casify(name)); 337 member_generator.set("initial_value_string", initial_value_string); 338 member_generator.append(R"~~~( 339 { 340 auto parsed_value = parse_css_value(parsing_context, "@initial_value_string@"sv, PropertyID::@name:titlecase@); 341 VERIFY(!parsed_value.is_null()); 342 initial_values[to_underlying(PropertyID::@name:titlecase@)] = parsed_value.release_nonnull(); 343 } 344)~~~"); 345 }; 346 347 properties.for_each_member([&](auto& name, auto& value) { 348 VERIFY(value.is_object()); 349 if (value.as_object().has("longhands"sv)) 350 return; 351 output_initial_value_code(name, value.as_object()); 352 }); 353 354 properties.for_each_member([&](auto& name, auto& value) { 355 VERIFY(value.is_object()); 356 if (!value.as_object().has("longhands"sv)) 357 return; 358 output_initial_value_code(name, value.as_object()); 359 }); 360 361 generator.append(R"~~~( 362 } 363 364 return *initial_values[to_underlying(property_id)]; 365} 366 367bool property_has_quirk(PropertyID property_id, Quirk quirk) 368{ 369 switch (property_id) { 370)~~~"); 371 372 properties.for_each_member([&](auto& name, auto& value) { 373 VERIFY(value.is_object()); 374 if (value.as_object().has("quirks"sv)) { 375 auto quirks_value = value.as_object().get_array("quirks"sv); 376 VERIFY(quirks_value.has_value()); 377 auto& quirks = quirks_value.value(); 378 379 if (!quirks.is_empty()) { 380 auto property_generator = generator.fork(); 381 property_generator.set("name:titlecase", title_casify(name)); 382 property_generator.append(R"~~~( 383 case PropertyID::@name:titlecase@: { 384 switch (quirk) { 385)~~~"); 386 for (auto& quirk : quirks.values()) { 387 VERIFY(quirk.is_string()); 388 auto quirk_generator = property_generator.fork(); 389 quirk_generator.set("quirk:titlecase", title_casify(quirk.as_string())); 390 quirk_generator.append(R"~~~( 391 case Quirk::@quirk:titlecase@: 392 return true; 393)~~~"); 394 } 395 property_generator.append(R"~~~( 396 default: 397 return false; 398 } 399 } 400)~~~"); 401 } 402 } 403 }); 404 405 generator.append(R"~~~( 406 default: 407 return false; 408 } 409} 410 411bool property_accepts_value(PropertyID property_id, StyleValue& style_value) 412{ 413 if (style_value.is_builtin()) 414 return true; 415 416 switch (property_id) { 417)~~~"); 418 419 properties.for_each_member([&](auto& name, auto& value) { 420 VERIFY(value.is_object()); 421 auto& object = value.as_object(); 422 bool has_valid_types = object.has("valid-types"sv); 423 auto has_valid_identifiers = object.has("valid-identifiers"sv); 424 if (has_valid_types || has_valid_identifiers) { 425 auto property_generator = generator.fork(); 426 property_generator.set("name:titlecase", title_casify(name)); 427 property_generator.append(R"~~~( 428 case PropertyID::@name:titlecase@: { 429)~~~"); 430 431 auto output_numeric_value_check = [](SourceGenerator& generator, StringView type_check_function, StringView value_getter, Span<StringView> resolved_type_names, StringView min_value, StringView max_value) { 432 auto test_generator = generator.fork(); 433 test_generator.set("type_check_function", type_check_function); 434 test_generator.set("value_getter", value_getter); 435 test_generator.append(R"~~~( 436 if ((style_value.@type_check_function@())~~~"); 437 if (!min_value.is_empty() && min_value != "-∞") { 438 test_generator.set("minvalue", min_value); 439 test_generator.append(" && (style_value.@value_getter@ >= @minvalue@)"); 440 } 441 if (!max_value.is_empty() && max_value != "") { 442 test_generator.set("maxvalue", max_value); 443 test_generator.append(" && (style_value.@value_getter@ <= @maxvalue@)"); 444 } 445 test_generator.append(")"); 446 if (!resolved_type_names.is_empty()) { 447 test_generator.append(R"~~~( 448 || (style_value.is_calculated() && ()~~~"); 449 bool first = true; 450 for (auto& type_name : resolved_type_names) { 451 test_generator.set("resolved_type_name", type_name); 452 if (!first) 453 test_generator.append(" || "); 454 test_generator.append("style_value.as_calculated().resolved_type() == CalculatedStyleValue::ResolvedType::@resolved_type_name@"); 455 first = false; 456 } 457 test_generator.append("))"); 458 } 459 test_generator.append(R"~~~() { 460 return true; 461 } 462)~~~"); 463 }; 464 465 if (has_valid_types) { 466 auto valid_types_value = object.get_array("valid-types"sv); 467 VERIFY(valid_types_value.has_value()); 468 auto& valid_types = valid_types_value.value(); 469 if (!valid_types.is_empty()) { 470 for (auto& type : valid_types.values()) { 471 VERIFY(type.is_string()); 472 auto type_parts = type.as_string().split_view(' '); 473 auto type_name = type_parts.first(); 474 auto type_args = type_parts.size() > 1 ? type_parts[1] : ""sv; 475 StringView min_value; 476 StringView max_value; 477 if (!type_args.is_empty()) { 478 VERIFY(type_args.starts_with('[') && type_args.ends_with(']')); 479 auto comma_index = type_args.find(',').value(); 480 min_value = type_args.substring_view(1, comma_index - 1); 481 max_value = type_args.substring_view(comma_index + 1, type_args.length() - comma_index - 2); 482 } 483 484 if (type_name == "angle") { 485 output_numeric_value_check(property_generator, "is_angle"sv, "as_angle().angle().to_degrees()"sv, Array { "Angle"sv }, min_value, max_value); 486 } else if (type_name == "color") { 487 property_generator.append(R"~~~( 488 if (style_value.has_color()) 489 return true; 490)~~~"); 491 } else if (type_name == "filter-value-list") { 492 property_generator.append(R"~~~( 493 if (style_value.is_filter_value_list()) 494 return true; 495)~~~"); 496 } else if (type_name == "frequency") { 497 output_numeric_value_check(property_generator, "is_frequency"sv, "as_frequency().frequency().to_hertz()"sv, Array { "Frequency"sv }, min_value, max_value); 498 } else if (type_name == "image") { 499 property_generator.append(R"~~~( 500 if (style_value.is_abstract_image()) 501 return true; 502)~~~"); 503 } else if (type_name == "integer") { 504 output_numeric_value_check(property_generator, "has_integer"sv, "to_integer()"sv, Array { "Integer"sv }, min_value, max_value); 505 } else if (type_name == "length") { 506 output_numeric_value_check(property_generator, "has_length"sv, "to_length().raw_value()"sv, Array { "Length"sv }, min_value, max_value); 507 } else if (type_name == "number") { 508 output_numeric_value_check(property_generator, "has_number"sv, "to_number()"sv, Array { "Integer"sv, "Number"sv }, min_value, max_value); 509 } else if (type_name == "percentage") { 510 output_numeric_value_check(property_generator, "is_percentage"sv, "as_percentage().percentage().value()"sv, Array { "Percentage"sv }, min_value, max_value); 511 } else if (type_name == "rect") { 512 property_generator.append(R"~~~( 513 if (style_value.has_rect()) 514 return true; 515)~~~"); 516 } else if (type_name == "resolution") { 517 output_numeric_value_check(property_generator, "is_resolution"sv, "as_resolution().resolution().to_dots_per_pixel()"sv, Array<StringView, 0> {}, min_value, max_value); 518 } else if (type_name == "string") { 519 property_generator.append(R"~~~( 520 if (style_value.is_string()) 521 return true; 522)~~~"); 523 } else if (type_name == "time") { 524 output_numeric_value_check(property_generator, "is_time"sv, "as_time().time().to_seconds()"sv, Array { "Time"sv }, min_value, max_value); 525 } else if (type_name == "url") { 526 // FIXME: Handle urls! 527 } else { 528 // Assume that any other type names are defined in Enums.json. 529 // If they're not, the output won't compile, but that's fine since it's invalid. 530 property_generator.set("type_name:snakecase", snake_casify(type_name)); 531 property_generator.append(R"~~~( 532 if (auto converted_identifier = value_id_to_@type_name:snakecase@(style_value.to_identifier()); converted_identifier.has_value()) 533 return true; 534)~~~"); 535 } 536 } 537 } 538 } 539 540 if (has_valid_identifiers) { 541 auto valid_identifiers_value = object.get_array("valid-identifiers"sv); 542 VERIFY(valid_identifiers_value.has_value()); 543 auto& valid_identifiers = valid_identifiers_value.value(); 544 if (!valid_identifiers.is_empty()) { 545 property_generator.append(R"~~~( 546 switch (style_value.to_identifier()) { 547)~~~"); 548 for (auto& identifier : valid_identifiers.values()) { 549 VERIFY(identifier.is_string()); 550 auto identifier_generator = generator.fork(); 551 identifier_generator.set("identifier:titlecase", title_casify(identifier.as_string())); 552 identifier_generator.append(R"~~~( 553 case ValueID::@identifier:titlecase@: 554)~~~"); 555 } 556 property_generator.append(R"~~~( 557 return true; 558 default: 559 break; 560 } 561)~~~"); 562 } 563 } 564 565 generator.append(R"~~~( 566 return false; 567 } 568)~~~"); 569 } 570 }); 571 572 generator.append(R"~~~( 573 default: 574 return true; 575 } 576} 577 578size_t property_maximum_value_count(PropertyID property_id) 579{ 580 switch (property_id) { 581)~~~"); 582 583 properties.for_each_member([&](auto& name, auto& value) { 584 VERIFY(value.is_object()); 585 if (value.as_object().has("max-values"sv)) { 586 auto max_values = value.as_object().get("max-values"sv); 587 VERIFY(max_values.has_value() && max_values->is_number() && !max_values->is_double()); 588 auto property_generator = generator.fork(); 589 property_generator.set("name:titlecase", title_casify(name)); 590 property_generator.set("max_values", max_values->to_deprecated_string()); 591 property_generator.append(R"~~~( 592 case PropertyID::@name:titlecase@: 593 return @max_values@; 594)~~~"); 595 } 596 }); 597 598 generator.append(R"~~~( 599 default: 600 return 1; 601 } 602} 603 604} // namespace Web::CSS 605 606)~~~"); 607 608 TRY(file.write_until_depleted(generator.as_string_view().bytes())); 609 return {}; 610}