From 41ef4f11dcd595441e44d03e3bbd5f84f2c4a268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Fri, 4 Feb 2022 17:19:22 +0100 Subject: LibGUI: Move GML parsing and formatting to new AST This commit introduces a couple of connected changes that are hard to untangle, unfortunately: - Parse GML into the AST instead of JSON - Change the load_from_json API on Widget to load_from_gml_ast - Remove this same API from Core::Object as it isn't used outside of LibGUI and was a workaround for the object registration detection; by verifying the objects we're getting and casting we can remove this constraint. - Format GML by calling the formating APIs on the AST itself; remove GMLFormatter.cpp as it's not needed anymore. After this change, GML formatting already respects comments :^) --- Userland/Libraries/LibCore/Object.h | 2 - Userland/Libraries/LibGUI/CMakeLists.txt | 1 - Userland/Libraries/LibGUI/GML/Formatter.cpp | 101 ------------------ Userland/Libraries/LibGUI/GML/Formatter.h | 11 +- Userland/Libraries/LibGUI/GML/Parser.cpp | 117 +++++++++------------ Userland/Libraries/LibGUI/GML/Parser.h | 3 +- .../Libraries/LibGUI/ScrollableContainerWidget.cpp | 41 ++++---- .../Libraries/LibGUI/ScrollableContainerWidget.h | 2 +- Userland/Libraries/LibGUI/Widget.cpp | 80 +++++++------- Userland/Libraries/LibGUI/Widget.h | 7 +- 10 files changed, 128 insertions(+), 237 deletions(-) delete mode 100644 Userland/Libraries/LibGUI/GML/Formatter.cpp diff --git a/Userland/Libraries/LibCore/Object.h b/Userland/Libraries/LibCore/Object.h index 8d468b54b0..6bcb75b675 100644 --- a/Userland/Libraries/LibCore/Object.h +++ b/Userland/Libraries/LibCore/Object.h @@ -181,8 +181,6 @@ public: void increment_inspector_count(Badge); void decrement_inspector_count(Badge); - virtual bool load_from_json(const JsonObject&, RefPtr (*)(const String&)) { return false; } - protected: explicit Object(Object* parent = nullptr); diff --git a/Userland/Libraries/LibGUI/CMakeLists.txt b/Userland/Libraries/LibGUI/CMakeLists.txt index 5c26a05a85..41dc578768 100644 --- a/Userland/Libraries/LibGUI/CMakeLists.txt +++ b/Userland/Libraries/LibGUI/CMakeLists.txt @@ -47,7 +47,6 @@ set(SOURCES GitCommitSyntaxHighlighter.cpp GlyphMapWidget.cpp GML/AutocompleteProvider.cpp - GML/Formatter.cpp GML/Lexer.cpp GML/Parser.cpp GML/SyntaxHighlighter.cpp diff --git a/Userland/Libraries/LibGUI/GML/Formatter.cpp b/Userland/Libraries/LibGUI/GML/Formatter.cpp deleted file mode 100644 index b1fcbc93fe..0000000000 --- a/Userland/Libraries/LibGUI/GML/Formatter.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2021, Linus Groh - * Copyright (c) 2022, kleines Filmröllchen - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "Formatter.h" -#include "Parser.h" -#include -#include -#include - -namespace GUI::GML { - -static String format_gml_object(const JsonObject& node, size_t indentation = 0, bool is_inline = false) -{ - StringBuilder builder; - - auto indent = [&builder](size_t indentation) { - for (size_t i = 0; i < indentation; ++i) - builder.append(" "); - }; - - struct Property { - String key; - JsonValue value; - }; - Vector properties; - node.for_each_member([&](auto& key, auto& value) { - if (key != "class" && key != "layout" && key != "children") - properties.append({ key, value }); - return IterationDecision::Continue; - }); - - if (!is_inline) - indent(indentation); - builder.append('@'); - builder.append(node.get("class").as_string()); - builder.append(" {\n"); - - for (auto& property : properties) { - indent(indentation + 1); - builder.append(property.key); - builder.append(": "); - if (property.value.is_array()) { - // custom array serialization as AK's doesn't pretty-print - // objects and arrays (we only care about arrays (for now)) - builder.append("["); - auto first = true; - property.value.as_array().for_each([&](auto& value) { - if (!first) - builder.append(", "); - first = false; - value.serialize(builder); - }); - builder.append("]"); - } else { - property.value.serialize(builder); - } - builder.append("\n"); - } - - if (node.has("layout")) { - auto layout = node.get("layout").as_object(); - if (!properties.is_empty()) - builder.append("\n"); - indent(indentation + 1); - builder.append("layout: "); - builder.append(format_gml_object(move(layout), indentation + 1, true)); - } - - if (node.has("children")) { - auto children = node.get("children").as_array(); - auto first = properties.is_empty() && !node.has("layout"); - children.for_each([&](auto& value) { - if (!first) - builder.append("\n"); - first = false; - builder.append(format_gml_object(value.as_object(), indentation + 1)); - }); - } - - indent(indentation); - builder.append("}\n"); - - return builder.to_string(); -} - -String format_gml(StringView string) -{ - // FIXME: Preserve comments somehow, they're not contained - // in the JSON object returned by parse_gml() - auto ast = parse_gml(string); - if (ast.is_null()) - return {}; - VERIFY(ast.is_object()); - return format_gml_object(ast.as_object()); -} - -} diff --git a/Userland/Libraries/LibGUI/GML/Formatter.h b/Userland/Libraries/LibGUI/GML/Formatter.h index 91eec86a43..c5d7cfe809 100644 --- a/Userland/Libraries/LibGUI/GML/Formatter.h +++ b/Userland/Libraries/LibGUI/GML/Formatter.h @@ -7,9 +7,18 @@ #pragma once #include +#include +#include +#include namespace GUI::GML { -String format_gml(StringView); +inline String format_gml(StringView string) +{ + auto ast = parse_gml(string); + if (ast.is_error()) + return {}; + return ast.value()->to_string(); +} } diff --git a/Userland/Libraries/LibGUI/GML/Parser.cpp b/Userland/Libraries/LibGUI/GML/Parser.cpp index 2e7e71b225..013bb6607c 100644 --- a/Userland/Libraries/LibGUI/GML/Parser.cpp +++ b/Userland/Libraries/LibGUI/GML/Parser.cpp @@ -6,18 +6,20 @@ */ #include "Parser.h" +#include "AST.h" #include "Lexer.h" +#include #include #include #include #include +#include namespace GUI::GML { -static Optional parse_core_object(Queue& tokens) +static ErrorOr> parse_gml_object(Queue& tokens) { - JsonObject object; - JsonArray children; + auto object = TRY(try_make_ref_counted()); auto peek = [&] { if (tokens.is_empty()) @@ -25,23 +27,21 @@ static Optional parse_core_object(Queue& tokens) return tokens.head().m_type; }; - while (peek() == Token::Type::Comment) - tokens.dequeue(); - - if (peek() != Token::Type::ClassMarker) { - dbgln("Expected class marker"); - return {}; + while (peek() == Token::Type::Comment) { + dbgln("found comment {}", tokens.head().m_view); + TRY(object->add_child(TRY(Node::from_token(tokens.dequeue())))); } + if (peek() != Token::Type::ClassMarker) + return Error::from_string_literal("Expected class marker"sv); + tokens.dequeue(); - if (peek() != Token::Type::ClassName) { - dbgln("Expected class name"); - return {}; - } + if (peek() != Token::Type::ClassName) + return Error::from_string_literal("Expected class name"sv); auto class_name = tokens.dequeue(); - object.set("class", JsonValue(class_name.m_view)); + object->set_name(class_name.m_view); if (peek() != Token::Type::LeftCurly) { // Empty object @@ -57,72 +57,44 @@ static Optional parse_core_object(Queue& tokens) if (peek() == Token::Type::ClassMarker) { // It's a child object. - auto value = parse_core_object(tokens); - if (!value.has_value()) { - dbgln("Parsing child object failed"); - return {}; - } - if (!value.value().is_object()) { - dbgln("Expected child to be Core::Object"); - return {}; - } - children.append(value.release_value()); + TRY(object->add_child(TRY(parse_gml_object(tokens)))); } else if (peek() == Token::Type::Identifier) { // It's a property. auto property_name = tokens.dequeue(); - if (property_name.m_view.is_empty()) { - dbgln("Expected non-empty property name"); - return {}; - } + if (property_name.m_view.is_empty()) + return Error::from_string_literal("Expected non-empty property name"sv); + + if (peek() != Token::Type::Colon) + return Error::from_string_literal("Expected ':'"sv); - if (peek() != Token::Type::Colon) { - dbgln("Expected ':'"); - return {}; - } tokens.dequeue(); - JsonValue value; - if (peek() == Token::Type::ClassMarker) { - auto parsed_value = parse_core_object(tokens); - if (!parsed_value.has_value()) - return {}; - if (!parsed_value.value().is_object()) { - dbgln("Expected property to be Core::Object"); - return {}; - } - value = parsed_value.release_value(); - } else if (peek() == Token::Type::JsonValue) { - auto value_string = tokens.dequeue(); - auto parsed_value = JsonValue::from_string(value_string.m_view); - if (parsed_value.is_error()) { - dbgln("Expected property to be JSON value"); - return {}; - } - value = parsed_value.release_value(); - } - object.set(property_name.m_view, move(value)); + RefPtr value; + if (peek() == Token::Type::ClassMarker) + value = TRY(parse_gml_object(tokens)); + else if (peek() == Token::Type::JsonValue) + value = TRY(try_make_ref_counted(TRY(JsonValueNode::from_string(tokens.dequeue().m_view)))); + + auto property = TRY(try_make_ref_counted(property_name.m_view, value.release_nonnull())); + TRY(object->add_child(property)); + } else if (peek() == Token::Type::Comment) { - tokens.dequeue(); + TRY(object->add_child(TRY(Node::from_token(tokens.dequeue())))); } else { - dbgln("Expected child, property, comment, or }}"); - return {}; + return Error::from_string_literal("Expected child, property, comment, or }}"sv); } } - if (peek() != Token::Type::RightCurly) { - dbgln("Expected }}"); - return {}; - } - tokens.dequeue(); + if (peek() != Token::Type::RightCurly) + return Error::from_string_literal("Expected }}"sv); - if (!children.is_empty()) - object.set("children", move(children)); + tokens.dequeue(); return object; } -JsonValue parse_gml(StringView string) +ErrorOr> parse_gml(StringView string) { auto lexer = Lexer(string); @@ -130,12 +102,23 @@ JsonValue parse_gml(StringView string) for (auto& token : lexer.lex()) tokens.enqueue(token); - auto root = parse_core_object(tokens); + auto file = TRY(try_make_ref_counted()); + + auto peek = [&] { + if (tokens.is_empty()) + return Token::Type::Unknown; + return tokens.head().m_type; + }; + + while (peek() == Token::Type::Comment) + TRY(file->add_child(TRY(Node::from_token(tokens.dequeue())))); + + TRY(file->add_child(TRY(parse_gml_object(tokens)))); - if (!root.has_value()) - return JsonValue(); + while (!tokens.is_empty()) + TRY(file->add_child(TRY(Node::from_token(tokens.dequeue())))); - return root.release_value(); + return file; } } diff --git a/Userland/Libraries/LibGUI/GML/Parser.h b/Userland/Libraries/LibGUI/GML/Parser.h index 041fd76914..5642221e52 100644 --- a/Userland/Libraries/LibGUI/GML/Parser.h +++ b/Userland/Libraries/LibGUI/GML/Parser.h @@ -8,9 +8,10 @@ #pragma once #include +#include namespace GUI::GML { -JsonValue parse_gml(StringView); +ErrorOr> parse_gml(StringView); } diff --git a/Userland/Libraries/LibGUI/ScrollableContainerWidget.cpp b/Userland/Libraries/LibGUI/ScrollableContainerWidget.cpp index 3d608450b5..e8020143d5 100644 --- a/Userland/Libraries/LibGUI/ScrollableContainerWidget.cpp +++ b/Userland/Libraries/LibGUI/ScrollableContainerWidget.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include @@ -85,48 +86,46 @@ void ScrollableContainerWidget::set_widget(GUI::Widget* widget) update_widget_position(); } -bool ScrollableContainerWidget::load_from_json(const JsonObject& json, RefPtr (*unregistered_child_handler)(const String&)) +bool ScrollableContainerWidget::load_from_gml_ast(NonnullRefPtr ast, RefPtr (*unregistered_child_handler)(const String&)) { - json.for_each_member([&](auto& key, auto& value) { + if (is(ast.ptr())) + return load_from_gml_ast(static_ptr_cast(ast)->main_class(), unregistered_child_handler); + + VERIFY(is(ast.ptr())); + auto object = static_ptr_cast(ast); + + object->for_each_property([&](auto key, auto value) { set_property(key, value); }); - auto layout_value = json.get("layout"); - if (!layout_value.is_null()) { - dbgln("Layout specified in ScrollableContainerWidget, this is not supported."); - return false; - } - - auto content_widget_value = json.get("content_widget"); - if (!content_widget_value.is_null() && !content_widget_value.is_object()) { + auto content_widget_value = object->get_property("content_widget"sv); + if (!content_widget_value.is_null() && !is(content_widget_value.ptr())) { dbgln("content widget is not an object"); return false; } - if (!json.get("children").is_null()) { + auto has_children = false; + object->for_each_child_object([&](auto) { has_children = true; }); + if (has_children) { dbgln("children specified for ScrollableContainerWidget, but only 1 widget as content_widget is supported"); return false; } - if (content_widget_value.is_object()) { - auto& content_widget = content_widget_value.as_object(); - auto class_name = content_widget.get("class"); - if (!class_name.is_string()) { - dbgln("Invalid content widget class name"); - return false; - } + if (!content_widget_value.is_null() && is(content_widget_value.ptr())) { + auto content_widget = static_ptr_cast(content_widget_value); + auto class_name = content_widget->name(); RefPtr child; - if (auto* registration = Core::ObjectClassRegistration::find(class_name.as_string())) { + if (auto* registration = Core::ObjectClassRegistration::find(class_name)) { child = registration->construct(); } else { - child = unregistered_child_handler(class_name.as_string()); + child = unregistered_child_handler(class_name); } if (!child) return false; auto widget_ptr = verify_cast(child.ptr()); set_widget(widget_ptr); - child->load_from_json(content_widget, unregistered_child_handler); + static_ptr_cast(child)->load_from_gml_ast(content_widget.release_nonnull(), unregistered_child_handler); return true; } diff --git a/Userland/Libraries/LibGUI/ScrollableContainerWidget.h b/Userland/Libraries/LibGUI/ScrollableContainerWidget.h index 755a0966d6..5fc3f8b0a5 100644 --- a/Userland/Libraries/LibGUI/ScrollableContainerWidget.h +++ b/Userland/Libraries/LibGUI/ScrollableContainerWidget.h @@ -27,7 +27,7 @@ protected: private: void update_widget_size(); void update_widget_position(); - virtual bool load_from_json(const JsonObject&, RefPtr (*unregistered_child_handler)(const String&)) override; + virtual bool load_from_gml_ast(NonnullRefPtr ast, RefPtr (*unregistered_child_handler)(const String&)) override; ScrollableContainerWidget(); diff --git a/Userland/Libraries/LibGUI/Widget.cpp b/Userland/Libraries/LibGUI/Widget.cpp index f24ad8f835..6429b96fc5 100644 --- a/Userland/Libraries/LibGUI/Widget.cpp +++ b/Userland/Libraries/LibGUI/Widget.cpp @@ -6,11 +6,15 @@ #include #include +#include #include +#include +#include #include #include #include #include +#include #include #include #include @@ -1067,32 +1071,36 @@ bool Widget::load_from_gml(StringView gml_string) bool Widget::load_from_gml(StringView gml_string, RefPtr (*unregistered_child_handler)(const String&)) { auto value = GML::parse_gml(gml_string); - if (!value.is_object()) + if (value.is_error()) { + // FIXME: We don't report the error, so at least print it. + dbgln("Error while parsing GML: {}", value.error()); return false; - return load_from_json(value.as_object(), unregistered_child_handler); + } + return load_from_gml_ast(value.release_value(), unregistered_child_handler); } -bool Widget::load_from_json(const JsonObject& json, RefPtr (*unregistered_child_handler)(const String&)) +bool Widget::load_from_gml_ast(NonnullRefPtr ast, RefPtr (*unregistered_child_handler)(const String&)) { - json.for_each_member([&](auto& key, auto& value) { + if (is(ast.ptr())) + return load_from_gml_ast(static_ptr_cast(ast)->main_class(), unregistered_child_handler); + + VERIFY(is(ast.ptr())); + auto object = static_ptr_cast(ast); + + object->for_each_property([&](auto key, auto value) { set_property(key, value); }); - auto layout_value = json.get("layout"); - if (!layout_value.is_null() && !layout_value.is_object()) { - dbgln("layout is not an object"); - return false; - } - if (layout_value.is_object()) { - auto& layout = layout_value.as_object(); - auto class_name = layout.get("class"); + auto layout = object->layout_object(); + if (!layout.is_null()) { + auto class_name = layout->name(); if (class_name.is_null()) { dbgln("Invalid layout class name"); return false; } auto& layout_class = *Core::ObjectClassRegistration::find("GUI::Layout"); - if (auto* registration = Core::ObjectClassRegistration::find(class_name.as_string())) { + if (auto* registration = Core::ObjectClassRegistration::find(class_name)) { auto layout = registration->construct(); if (!layout || !registration->is_derived_from(layout_class)) { dbgln("Invalid layout class: '{}'", class_name.to_string()); @@ -1104,40 +1112,32 @@ bool Widget::load_from_json(const JsonObject& json, RefPtr (*unreg return false; } - layout.for_each_member([&](auto& key, auto& value) { + layout->for_each_property([&](auto key, auto value) { this->layout()->set_property(key, value); }); } auto& widget_class = *Core::ObjectClassRegistration::find("GUI::Widget"); - auto children = json.get("children"); - if (children.is_array()) { - for (auto& child_json_value : children.as_array().values()) { - if (!child_json_value.is_object()) - return false; - auto& child_json = child_json_value.as_object(); - auto class_name = child_json.get("class"); - if (!class_name.is_string()) { - dbgln("No class name in entry"); - return false; + object->for_each_child_object_interruptible([&](auto child_data) { + auto class_name = child_data->name(); + + RefPtr child; + if (auto* registration = Core::ObjectClassRegistration::find(class_name)) { + child = registration->construct(); + if (!child || !registration->is_derived_from(widget_class)) { + dbgln("Invalid widget class: '{}'", class_name); + return IterationDecision::Break; } - - RefPtr child; - if (auto* registration = Core::ObjectClassRegistration::find(class_name.as_string())) { - child = registration->construct(); - if (!child || !registration->is_derived_from(widget_class)) { - dbgln("Invalid widget class: '{}'", class_name.to_string()); - return false; - } - } else { - child = unregistered_child_handler(class_name.as_string()); - } - if (!child) - return false; - add_child(*child); - child->load_from_json(child_json, unregistered_child_handler); + } else { + child = unregistered_child_handler(class_name); } - } + if (!child) + return IterationDecision::Break; + add_child(*child); + // This is possible as we ensure that Widget is a base class above. + static_ptr_cast(child)->load_from_gml_ast(child_data, unregistered_child_handler); + return IterationDecision::Continue; + }); return true; } diff --git a/Userland/Libraries/LibGUI/Widget.h b/Userland/Libraries/LibGUI/Widget.h index b8c7979d2d..c6e1391269 100644 --- a/Userland/Libraries/LibGUI/Widget.h +++ b/Userland/Libraries/LibGUI/Widget.h @@ -8,12 +8,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -299,6 +301,9 @@ public: bool has_pending_drop() const; + // In order for others to be able to call this, it needs to be public. + virtual bool load_from_gml_ast(NonnullRefPtr ast, RefPtr (*unregistered_child_handler)(const String&)); + protected: Widget(); @@ -354,8 +359,6 @@ private: void focus_previous_widget(FocusSource, bool siblings_only); void focus_next_widget(FocusSource, bool siblings_only); - virtual bool load_from_json(const JsonObject&, RefPtr (*unregistered_child_handler)(const String&)) override; - // HACK: These are used as property getters for the fixed_* size property aliases. int dummy_fixed_width() { return 0; } int dummy_fixed_height() { return 0; } -- cgit v1.2.3