main.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. /*
  2. * Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/Forward.h>
  7. #include <AK/HashTable.h>
  8. #include <AK/LexicalPath.h>
  9. #include <AK/SourceGenerator.h>
  10. #include <AK/String.h>
  11. #include <AK/Try.h>
  12. #include <AK/Utf8View.h>
  13. #include <LibCore/ArgsParser.h>
  14. #include <LibCore/File.h>
  15. #include <LibGUI/GML/Parser.h>
  16. #include <LibGUI/UIDimensions.h>
  17. #include <LibMain/Main.h>
  18. enum class UseObjectConstructor : bool {
  19. No,
  20. Yes,
  21. };
  22. // Classes whose header doesn't have the same name as the class.
  23. static Optional<StringView> map_class_to_file(StringView class_)
  24. {
  25. static HashMap<StringView, StringView> class_file_mappings {
  26. { "GUI::HorizontalSplitter"sv, "GUI/Splitter"sv },
  27. { "GUI::VerticalSplitter"sv, "GUI/Splitter"sv },
  28. { "GUI::HorizontalSeparator"sv, "GUI/SeparatorWidget"sv },
  29. { "GUI::VerticalSeparator"sv, "GUI/SeparatorWidget"sv },
  30. { "GUI::HorizontalBoxLayout"sv, "GUI/BoxLayout"sv },
  31. { "GUI::VerticalBoxLayout"sv, "GUI/BoxLayout"sv },
  32. { "GUI::HorizontalProgressbar"sv, "GUI/Progressbar"sv },
  33. { "GUI::VerticalProgressbar"sv, "GUI/Progressbar"sv },
  34. { "GUI::DialogButton"sv, "GUI/Button"sv },
  35. { "GUI::PasswordBox"sv, "GUI/TextBox"sv },
  36. // Map Layout::Spacer to the Layout header even though it's a pseudo class.
  37. { "GUI::Layout::Spacer"sv, "GUI/Layout"sv },
  38. };
  39. return class_file_mappings.get(class_);
  40. }
  41. // Properties which don't take a direct JSON-like primitive (StringView, int, bool, Array etc) as arguments and need the arguments to be wrapped in a constructor call.
  42. static Optional<StringView> map_property_to_type(StringView property)
  43. {
  44. static HashMap<StringView, StringView> property_to_type_mappings {
  45. { "container_margins"sv, "GUI::Margins"sv },
  46. { "margins"sv, "GUI::Margins"sv },
  47. };
  48. return property_to_type_mappings.get(property);
  49. }
  50. // Properties which take a UIDimension which can handle JSON directly.
  51. static bool is_ui_dimension_property(StringView property)
  52. {
  53. static HashTable<StringView> ui_dimension_properties;
  54. if (ui_dimension_properties.is_empty()) {
  55. ui_dimension_properties.set("min_width"sv);
  56. ui_dimension_properties.set("max_width"sv);
  57. ui_dimension_properties.set("preferred_width"sv);
  58. ui_dimension_properties.set("min_height"sv);
  59. ui_dimension_properties.set("max_height"sv);
  60. ui_dimension_properties.set("preferred_height"sv);
  61. }
  62. return ui_dimension_properties.contains(property);
  63. }
  64. // FIXME: Since normal string-based properties take either String or StringView (and the latter can be implicitly constructed from the former),
  65. // we need to special-case DeprecatedString property setters while those still exist.
  66. // Please remove a setter from this list once it uses StringView or String.
  67. static bool takes_deprecated_string(StringView property)
  68. {
  69. static HashTable<StringView> deprecated_string_properties;
  70. if (deprecated_string_properties.is_empty()) {
  71. deprecated_string_properties.set("icon_from_path"sv);
  72. deprecated_string_properties.set("name"sv);
  73. }
  74. return deprecated_string_properties.contains(property);
  75. }
  76. static ErrorOr<String> include_path_for(StringView class_name, LexicalPath const& gml_file_name)
  77. {
  78. String pathed_name;
  79. if (auto mapping = map_class_to_file(class_name); mapping.has_value())
  80. pathed_name = TRY(String::from_utf8(mapping.value()));
  81. else
  82. pathed_name = TRY(TRY(String::from_utf8(class_name)).replace("::"sv, "/"sv, ReplaceMode::All));
  83. if (class_name.starts_with("GUI::"sv) || class_name.starts_with("WebView::"sv))
  84. return String::formatted("<Lib{}.h>", pathed_name);
  85. // We assume that all other paths are within the current application, for now.
  86. // To figure out what kind of userland program this is (application, service, ...) we consider the path to the original GML file.
  87. auto const& paths = gml_file_name.parts_view();
  88. auto path_iter = paths.find("Userland"sv);
  89. path_iter++;
  90. auto const userland_subdirectory = (path_iter == paths.end()) ? "Applications"_string : TRY(String::from_utf8(*path_iter));
  91. return String::formatted("<{}/{}.h>", userland_subdirectory, pathed_name);
  92. }
  93. // Each entry is an include path, without the "#include" itself.
  94. static ErrorOr<HashTable<String>> extract_necessary_includes(GUI::GML::Object const& gml_hierarchy, LexicalPath const& gml_file_name)
  95. {
  96. HashTable<String> necessary_includes;
  97. TRY(necessary_includes.try_set(TRY(include_path_for(gml_hierarchy.name(), gml_file_name))));
  98. if (gml_hierarchy.layout_object() != nullptr)
  99. TRY(necessary_includes.try_set(TRY(include_path_for(gml_hierarchy.layout_object()->name(), gml_file_name))));
  100. TRY(gml_hierarchy.try_for_each_child_object([&](auto const& object) -> ErrorOr<void> {
  101. auto necessary_child_includes = TRY(extract_necessary_includes(object, gml_file_name));
  102. for (auto const& include : necessary_child_includes)
  103. TRY(necessary_includes.try_set(include));
  104. return {};
  105. }));
  106. return necessary_includes;
  107. }
  108. static char const header[] = R"~~~(
  109. /*
  110. * Auto-generated by the GML compiler
  111. */
  112. )~~~";
  113. static char const function_start[] = R"~~~(
  114. // Creates a @main_class_name@ and initializes it.
  115. // This function was auto-generated by the GML compiler.
  116. ErrorOr<NonnullRefPtr<@main_class_name@>> @main_class_name@::try_create()
  117. {
  118. RefPtr<::@main_class_name@> main_object;
  119. )~~~";
  120. static char const footer[] = R"~~~(
  121. return main_object.release_nonnull();
  122. }
  123. )~~~";
  124. static ErrorOr<String> escape_string(JsonValue to_escape)
  125. {
  126. auto string = TRY(String::from_deprecated_string(to_escape.as_string()));
  127. // All C++ simple escape sequences; see https://en.cppreference.com/w/cpp/language/escape
  128. // Other commonly-escaped characters are hard-to-type Unicode and therefore fine to include verbatim in UTF-8 coded strings.
  129. static HashMap<StringView, StringView> escape_sequences = {
  130. { "\\"sv, "\\\\"sv }, // This needs to be the first because otherwise the the backslashes of other items will be double escaped
  131. { "\0"sv, "\\0"sv },
  132. { "\'"sv, "\\'"sv },
  133. { "\""sv, "\\\""sv },
  134. { "\a"sv, "\\a"sv },
  135. { "\b"sv, "\\b"sv },
  136. { "\f"sv, "\\f"sv },
  137. { "\n"sv, "\\n"sv },
  138. { "\r"sv, "\\r"sv },
  139. { "\t"sv, "\\t"sv },
  140. { "\v"sv, "\\v"sv },
  141. };
  142. for (auto const& entries : escape_sequences)
  143. string = TRY(string.replace(entries.key, entries.value, ReplaceMode::All));
  144. return string;
  145. }
  146. // This function assumes that the string is already the same as its enum constant's name.
  147. // Therefore, it does not handle UI dimensions.
  148. static ErrorOr<Optional<String>> generate_enum_initializer_for(StringView property_name, JsonValue value)
  149. {
  150. // The value is the enum's type name.
  151. static HashMap<StringView, StringView> enum_properties = {
  152. { "text_alignment"sv, "Gfx::TextAlignment"sv },
  153. { "focus_policy"sv, "GUI::FocusPolicy"sv },
  154. { "foreground_role"sv, "Gfx::ColorRole"sv },
  155. { "frame_style"sv, "Gfx::FrameStyle"sv },
  156. { "text_wrapping"sv, "Gfx::TextWrapping"sv },
  157. };
  158. auto const& enum_type_name = enum_properties.get(property_name);
  159. if (!enum_type_name.has_value())
  160. return Optional<String> {};
  161. return String::formatted("{}::{}", *enum_type_name, value.as_string());
  162. }
  163. // FIXME: In case of error, propagate the precise array+property that triggered the error.
  164. static ErrorOr<String> generate_initializer_for(Optional<StringView> property_name, JsonValue value)
  165. {
  166. if (value.is_string()) {
  167. if (property_name.has_value()) {
  168. if (takes_deprecated_string(*property_name))
  169. return String::formatted(R"~~~("{}"sv)~~~", TRY(escape_string(value)));
  170. if (auto const enum_value = TRY(generate_enum_initializer_for(*property_name, value)); enum_value.has_value())
  171. return String::formatted("{}", *enum_value);
  172. if (*property_name == "bitmap"sv)
  173. return String::formatted(R"~~~(TRY(Gfx::Bitmap::load_from_file("{}"sv)))~~~", TRY(escape_string(value)));
  174. }
  175. return String::formatted(R"~~~("{}"_string)~~~", TRY(escape_string(value)));
  176. }
  177. // No need to handle the smaller integer types separately.
  178. if (value.is_integer<i64>())
  179. return String::formatted("static_cast<i64>({})", value.as_integer<i64>());
  180. if (value.is_integer<u64>())
  181. return String::formatted("static_cast<u64>({})", value.as_integer<u64>());
  182. if (value.is_bool())
  183. return String::formatted("{}", value.as_bool());
  184. if (value.is_double())
  185. return String::formatted("static_cast<double>({})", value.as_double());
  186. if (value.is_array()) {
  187. auto const& array = value.as_array();
  188. auto child_type = Optional<StringView> {};
  189. for (auto const& child_value : array.values()) {
  190. if (child_value.is_array())
  191. return Error::from_string_view("Nested arrays are not supported"sv);
  192. #define HANDLE_TYPE(type_name, is_type) \
  193. if (child_value.is_type() && (!child_type.has_value() || child_type.value() == #type_name##sv)) \
  194. child_type = #type_name##sv; \
  195. else
  196. HANDLE_TYPE(StringView, is_string)
  197. HANDLE_TYPE(i64, is_integer<i64>)
  198. HANDLE_TYPE(u64, is_integer<u64>)
  199. HANDLE_TYPE(bool, is_bool)
  200. HANDLE_TYPE(double, is_double)
  201. return Error::from_string_view("Inconsistent contained type in JSON array"sv);
  202. #undef HANDLE_TYPE
  203. }
  204. if (!child_type.has_value())
  205. return Error::from_string_view("Empty JSON array; cannot deduce type."sv);
  206. StringBuilder initializer;
  207. initializer.appendff("Array<{}, {}> {{ "sv, child_type.release_value(), array.size());
  208. for (auto const& child_value : array.values())
  209. initializer.appendff("{}, ", TRY(generate_initializer_for({}, child_value)));
  210. initializer.append("}"sv);
  211. return initializer.to_string();
  212. }
  213. return Error::from_string_view("Unsupported JSON value"sv);
  214. }
  215. // Loads an object and assigns it to the RefPtr<Widget> variable named object_name.
  216. // All loading happens in a separate block.
  217. static ErrorOr<void> generate_loader_for_object(GUI::GML::Object const& gml_object, SourceGenerator generator, String object_name, size_t indentation, UseObjectConstructor use_object_constructor)
  218. {
  219. generator.set("object_name", object_name.to_deprecated_string());
  220. generator.set("class_name", gml_object.name());
  221. auto append = [&]<size_t N>(auto& generator, char const(&text)[N]) -> ErrorOr<void> {
  222. generator.append(TRY(String::repeated(' ', indentation * 4)).bytes_as_string_view());
  223. generator.appendln(text);
  224. return {};
  225. };
  226. generator.append(TRY(String::repeated(' ', (indentation - 1) * 4)).bytes_as_string_view());
  227. generator.appendln("{");
  228. if (use_object_constructor == UseObjectConstructor::Yes)
  229. TRY(append(generator, "@object_name@ = TRY(@class_name@::try_create());"));
  230. else
  231. TRY(append(generator, "@object_name@ = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) ::@class_name@()));"));
  232. // Properties
  233. TRY(gml_object.try_for_each_property([&](StringView key, NonnullRefPtr<GUI::GML::JsonValueNode> value) -> ErrorOr<void> {
  234. auto value_code = TRY(generate_initializer_for(key, value));
  235. if (is_ui_dimension_property(key)) {
  236. if (auto ui_dimension = GUI::UIDimension::construct_from_json_value(value); ui_dimension.has_value())
  237. value_code = TRY(ui_dimension->as_cpp_source());
  238. else
  239. // FIXME: propagate precise error cause
  240. return Error::from_string_view("UI dimension invalid"sv);
  241. } else {
  242. // Wrap value in an extra constructor call if necessary.
  243. if (auto type = map_property_to_type(key); type.has_value())
  244. value_code = TRY(String::formatted("{} {{ {} }}", type.release_value(), value_code));
  245. }
  246. auto property_generator = generator.fork();
  247. property_generator.set("key", key);
  248. property_generator.set("value", value_code.bytes_as_string_view());
  249. TRY(append(property_generator, R"~~~(@object_name@->set_@key@(@value@);)~~~"));
  250. return {};
  251. }));
  252. generator.appendln("");
  253. // Layout
  254. if (gml_object.layout_object() != nullptr) {
  255. TRY(append(generator, "RefPtr<GUI::Layout> layout;"));
  256. TRY(generate_loader_for_object(*gml_object.layout_object(), generator.fork(), TRY(String::from_utf8("layout"sv)), indentation + 1, UseObjectConstructor::Yes));
  257. TRY(append(generator, "@object_name@->set_layout(layout.release_nonnull());"));
  258. generator.appendln("");
  259. }
  260. // Children
  261. size_t current_child_index = 0;
  262. auto next_child_name = [&]() {
  263. return String::formatted("{}_child_{}", object_name, current_child_index++);
  264. };
  265. TRY(gml_object.try_for_each_child_object([&](auto const& child) -> ErrorOr<void> {
  266. // Spacer is a pseudo-class that insteads causes a call to `Widget::add_spacer` on the parent object.
  267. if (child.name() == "GUI::Layout::Spacer"sv) {
  268. TRY(append(generator, "@object_name@->add_spacer();"));
  269. return {};
  270. }
  271. auto child_generator = generator.fork();
  272. auto child_variable_name = TRY(next_child_name());
  273. child_generator.set("child_variable_name", child_variable_name.bytes_as_string_view());
  274. child_generator.set("child_class_name", child.name());
  275. TRY(append(child_generator, "RefPtr<@child_class_name@> @child_variable_name@;"));
  276. TRY(generate_loader_for_object(child, child_generator.fork(), child_variable_name, indentation + 1, UseObjectConstructor::Yes));
  277. // Handle the current two special cases of child adding.
  278. if (gml_object.name() == "GUI::ScrollableContainerWidget"sv)
  279. TRY(append(child_generator, "static_ptr_cast<GUI::ScrollableContainerWidget>(@object_name@)->set_widget(*@child_variable_name@);"));
  280. else if (gml_object.name() == "GUI::TabWidget"sv)
  281. TRY(append(child_generator, "static_ptr_cast<GUI::TabWidget>(@object_name@)->add_widget(*@child_variable_name@);"));
  282. else
  283. TRY(append(child_generator, "TRY(@object_name@->try_add_child(*@child_variable_name@));"));
  284. child_generator.appendln("");
  285. return {};
  286. }));
  287. generator.append(TRY(String::repeated(' ', (indentation - 1) * 4)).bytes_as_string_view());
  288. generator.appendln("}");
  289. return {};
  290. }
  291. static ErrorOr<String> generate_cpp(NonnullRefPtr<GUI::GML::GMLFile> gml, LexicalPath const& gml_file_name)
  292. {
  293. StringBuilder builder;
  294. SourceGenerator generator { builder };
  295. generator.append(header);
  296. auto& main_class = gml->main_class();
  297. auto necessary_includes = TRY(extract_necessary_includes(main_class, gml_file_name));
  298. static String const always_necessary_includes[] = {
  299. TRY(String::from_utf8("<AK/Error.h>"sv)),
  300. TRY(String::from_utf8("<AK/JsonValue.h>"sv)),
  301. TRY(String::from_utf8("<AK/NonnullRefPtr.h>"sv)),
  302. TRY(String::from_utf8("<AK/RefPtr.h>"sv)),
  303. // For Gfx::ColorRole
  304. TRY(String::from_utf8("<LibGfx/SystemTheme.h>"sv)),
  305. TRY(String::from_utf8("<LibGUI/Widget.h>"sv)),
  306. };
  307. TRY(necessary_includes.try_set_from(always_necessary_includes));
  308. for (auto const& include : necessary_includes)
  309. generator.appendln(TRY(String::formatted("#include {}", include)).bytes_as_string_view());
  310. // FIXME: Use a UTF-8 aware function once possible.
  311. generator.set("main_class_name", main_class.name());
  312. generator.append(function_start);
  313. TRY(generate_loader_for_object(main_class, generator.fork(), "main_object"_string, 2, UseObjectConstructor::No));
  314. generator.append(footer);
  315. return builder.to_string();
  316. }
  317. ErrorOr<int> serenity_main(Main::Arguments arguments)
  318. {
  319. Core::ArgsParser argument_parser;
  320. StringView gml_file_name;
  321. argument_parser.add_positional_argument(gml_file_name, "GML file to compile", "GML_FILE", Core::ArgsParser::Required::Yes);
  322. argument_parser.parse(arguments);
  323. auto gml_text = TRY(TRY(Core::File::open(gml_file_name, Core::File::OpenMode::Read))->read_until_eof());
  324. auto parsed_gml = TRY(GUI::GML::parse_gml(gml_text));
  325. auto generated_cpp = TRY(generate_cpp(parsed_gml, LexicalPath { gml_file_name }));
  326. outln("{}", generated_cpp);
  327. return 0;
  328. }