Explorar o código

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 :^)
kleines Filmröllchen %!s(int64=3) %!d(string=hai) anos
pai
achega
41ef4f11dc

+ 0 - 2
Userland/Libraries/LibCore/Object.h

@@ -181,8 +181,6 @@ public:
     void increment_inspector_count(Badge<InspectorServerConnection>);
     void decrement_inspector_count(Badge<InspectorServerConnection>);
 
-    virtual bool load_from_json(const JsonObject&, RefPtr<Core::Object> (*)(const String&)) { return false; }
-
 protected:
     explicit Object(Object* parent = nullptr);
 

+ 0 - 1
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

+ 0 - 101
Userland/Libraries/LibGUI/GML/Formatter.cpp

@@ -1,101 +0,0 @@
-/*
- * Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
- * Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
-
-#include "Formatter.h"
-#include "Parser.h"
-#include <AK/JsonObject.h>
-#include <AK/JsonValue.h>
-#include <AK/StringBuilder.h>
-
-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<Property> 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());
-}
-
-}

+ 10 - 1
Userland/Libraries/LibGUI/GML/Formatter.h

@@ -7,9 +7,18 @@
 #pragma once
 
 #include <AK/Forward.h>
+#include <AK/String.h>
+#include <LibGUI/GML/AST.h>
+#include <LibGUI/GML/Parser.h>
 
 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();
+}
 
 }

+ 50 - 67
Userland/Libraries/LibGUI/GML/Parser.cpp

@@ -6,18 +6,20 @@
  */
 
 #include "Parser.h"
+#include "AST.h"
 #include "Lexer.h"
+#include <AK/Error.h>
 #include <AK/GenericLexer.h>
 #include <AK/JsonObject.h>
 #include <AK/JsonValue.h>
 #include <AK/Queue.h>
+#include <AK/RefPtr.h>
 
 namespace GUI::GML {
 
-static Optional<JsonValue> parse_core_object(Queue<Token>& tokens)
+static ErrorOr<NonnullRefPtr<Object>> parse_gml_object(Queue<Token>& tokens)
 {
-    JsonObject object;
-    JsonArray children;
+    auto object = TRY(try_make_ref_counted<Object>());
 
     auto peek = [&] {
         if (tokens.is_empty())
@@ -25,23 +27,21 @@ static Optional<JsonValue> parse_core_object(Queue<Token>& 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<Comment>(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<JsonValue> parse_core_object(Queue<Token>& 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<ValueNode> value;
+            if (peek() == Token::Type::ClassMarker)
+                value = TRY(parse_gml_object(tokens));
+            else if (peek() == Token::Type::JsonValue)
+                value = TRY(try_make_ref_counted<JsonValueNode>(TRY(JsonValueNode::from_string(tokens.dequeue().m_view))));
+
+            auto property = TRY(try_make_ref_counted<KeyValuePair>(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<Comment>(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<NonnullRefPtr<GMLFile>> 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<GMLFile>());
+
+    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<Comment>(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<Comment>(tokens.dequeue()))));
 
-    return root.release_value();
+    return file;
 }
 
 }

+ 2 - 1
Userland/Libraries/LibGUI/GML/Parser.h

@@ -8,9 +8,10 @@
 #pragma once
 
 #include <AK/Forward.h>
+#include <LibGUI/GML/AST.h>
 
 namespace GUI::GML {
 
-JsonValue parse_gml(StringView);
+ErrorOr<NonnullRefPtr<GMLFile>> parse_gml(StringView);
 
 }

+ 20 - 21
Userland/Libraries/LibGUI/ScrollableContainerWidget.cpp

@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
+#include <AK/RefPtr.h>
 #include <LibGUI/Layout.h>
 #include <LibGUI/ScrollableContainerWidget.h>
 
@@ -85,48 +86,46 @@ void ScrollableContainerWidget::set_widget(GUI::Widget* widget)
     update_widget_position();
 }
 
-bool ScrollableContainerWidget::load_from_json(const JsonObject& json, RefPtr<Core::Object> (*unregistered_child_handler)(const String&))
+bool ScrollableContainerWidget::load_from_gml_ast(NonnullRefPtr<GUI::GML::Node> ast, RefPtr<Core::Object> (*unregistered_child_handler)(const String&))
 {
-    json.for_each_member([&](auto& key, auto& value) {
+    if (is<GUI::GML::GMLFile>(ast.ptr()))
+        return load_from_gml_ast(static_ptr_cast<GUI::GML::GMLFile>(ast)->main_class(), unregistered_child_handler);
+
+    VERIFY(is<GUI::GML::Object>(ast.ptr()));
+    auto object = static_ptr_cast<GUI::GML::Object>(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<Object>(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<Object>(content_widget_value.ptr())) {
+        auto content_widget = static_ptr_cast<GUI::GML::Object>(content_widget_value);
+        auto class_name = content_widget->name();
 
         RefPtr<Core::Object> 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<GUI::Widget>(child.ptr());
         set_widget(widget_ptr);
-        child->load_from_json(content_widget, unregistered_child_handler);
+        static_ptr_cast<Widget>(child)->load_from_gml_ast(content_widget.release_nonnull(), unregistered_child_handler);
         return true;
     }
 

+ 1 - 1
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<Core::Object> (*unregistered_child_handler)(const String&)) override;
+    virtual bool load_from_gml_ast(NonnullRefPtr<GUI::GML::Node> ast, RefPtr<Core::Object> (*unregistered_child_handler)(const String&)) override;
 
     ScrollableContainerWidget();
 

+ 40 - 40
Userland/Libraries/LibGUI/Widget.cpp

@@ -6,11 +6,15 @@
 
 #include <AK/Assertions.h>
 #include <AK/Debug.h>
+#include <AK/IterationDecision.h>
 #include <AK/JsonObject.h>
+#include <AK/NonnullRefPtr.h>
+#include <AK/RefPtr.h>
 #include <LibGUI/Action.h>
 #include <LibGUI/Application.h>
 #include <LibGUI/BoxLayout.h>
 #include <LibGUI/Event.h>
+#include <LibGUI/GML/AST.h>
 #include <LibGUI/GML/Parser.h>
 #include <LibGUI/Layout.h>
 #include <LibGUI/Menu.h>
@@ -1067,32 +1071,36 @@ bool Widget::load_from_gml(StringView gml_string)
 bool Widget::load_from_gml(StringView gml_string, RefPtr<Core::Object> (*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<Core::Object> (*unregistered_child_handler)(const String&))
+bool Widget::load_from_gml_ast(NonnullRefPtr<GUI::GML::Node> ast, RefPtr<Core::Object> (*unregistered_child_handler)(const String&))
 {
-    json.for_each_member([&](auto& key, auto& value) {
+    if (is<GUI::GML::GMLFile>(ast.ptr()))
+        return load_from_gml_ast(static_ptr_cast<GUI::GML::GMLFile>(ast)->main_class(), unregistered_child_handler);
+
+    VERIFY(is<GUI::GML::Object>(ast.ptr()));
+    auto object = static_ptr_cast<GUI::GML::Object>(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<Core::Object> (*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<Core::Object> 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<Core::Object> 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<Widget>(child)->load_from_gml_ast(child_data, unregistered_child_handler);
+        return IterationDecision::Continue;
+    });
 
     return true;
 }

+ 5 - 2
Userland/Libraries/LibGUI/Widget.h

@@ -8,12 +8,14 @@
 
 #include <AK/EnumBits.h>
 #include <AK/JsonObject.h>
+#include <AK/NonnullRefPtr.h>
 #include <AK/String.h>
 #include <AK/Variant.h>
 #include <LibCore/Object.h>
 #include <LibGUI/Event.h>
 #include <LibGUI/FocusPolicy.h>
 #include <LibGUI/Forward.h>
+#include <LibGUI/GML/AST.h>
 #include <LibGUI/Margins.h>
 #include <LibGfx/Color.h>
 #include <LibGfx/Forward.h>
@@ -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<GUI::GML::Node> ast, RefPtr<Core::Object> (*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<Core::Object> (*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; }