mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 19:02:44 +00:00 
			
		
		
		
	 c8585b77d2
			
		
	
	
		c8585b77d2
		
	
	
	
	
		
			
			This prevents us from needing a sv suffix, and potentially reduces the need to run generic code for a single character (as contains, starts_with, ends_with etc. for a char will be just a length and equality check). No functional changes.
		
			
				
	
	
		
			338 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			338 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
|  * Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>.
 | |
|  *
 | |
|  * SPDX-License-Identifier: BSD-2-Clause
 | |
|  */
 | |
| 
 | |
| #pragma once
 | |
| 
 | |
| #include <AK/Concepts.h>
 | |
| #include <AK/Error.h>
 | |
| #include <AK/Forward.h>
 | |
| #include <AK/HashMap.h>
 | |
| #include <AK/IterationDecision.h>
 | |
| #include <AK/JsonArray.h>
 | |
| #include <AK/JsonValue.h>
 | |
| #include <AK/NonnullRefPtr.h>
 | |
| #include <AK/NonnullRefPtrVector.h>
 | |
| #include <AK/RefCounted.h>
 | |
| #include <AK/RefPtr.h>
 | |
| #include <AK/String.h>
 | |
| #include <AK/StringBuilder.h>
 | |
| #include <AK/TypeCasts.h>
 | |
| #include <LibGUI/GML/Lexer.h>
 | |
| 
 | |
| namespace GUI::GML {
 | |
| 
 | |
| class Comment;
 | |
| class JsonValueNode;
 | |
| 
 | |
| // Base of the GML Abstract Syntax Tree (AST).
 | |
| class Node : public RefCounted<Node> {
 | |
| public:
 | |
|     virtual ~Node() = default;
 | |
|     template<typename NodeT>
 | |
|     requires(IsBaseOf<Node, NodeT>) static ErrorOr<NonnullRefPtr<NodeT>> from_token(Token token)
 | |
|     {
 | |
|         return try_make_ref_counted<NodeT>(token.m_view);
 | |
|     }
 | |
| 
 | |
|     String to_string() const
 | |
|     {
 | |
|         StringBuilder builder;
 | |
|         format(builder, 0, false);
 | |
|         return builder.to_string();
 | |
|     }
 | |
| 
 | |
|     // Format this AST node with the builder at the given indentation level.
 | |
|     // is_inline controls whether we are starting on a new line.
 | |
|     virtual void format(StringBuilder& builder, size_t indentation, bool is_inline) const = 0;
 | |
| 
 | |
|     // FIXME: We can't change the kind of indentation right now.
 | |
|     static void indent(StringBuilder& builder, size_t indentation)
 | |
|     {
 | |
|         for (size_t i = 0; i < indentation; ++i)
 | |
|             builder.append("    "sv);
 | |
|     }
 | |
| };
 | |
| 
 | |
| // AST nodes that actually hold data and can be in a KeyValuePair.
 | |
| class ValueNode
 | |
|     : public Node {
 | |
| public:
 | |
|     virtual ~ValueNode() = default;
 | |
| };
 | |
| 
 | |
| // Single line comments with //.
 | |
| class Comment : public Node {
 | |
| public:
 | |
|     Comment(String text)
 | |
|         : m_text(move(text))
 | |
|     {
 | |
|     }
 | |
| 
 | |
|     virtual void format(StringBuilder& builder, size_t indentation, bool is_inline) const override
 | |
|     {
 | |
|         if (is_inline) {
 | |
|             builder.append(m_text);
 | |
|         } else {
 | |
|             indent(builder, indentation);
 | |
|             builder.append(m_text);
 | |
|         }
 | |
|         builder.append('\n');
 | |
|     }
 | |
|     virtual ~Comment() override = default;
 | |
| 
 | |
| private:
 | |
|     String m_text {};
 | |
| };
 | |
| 
 | |
| // Any JSON-like key: value pair.
 | |
| class KeyValuePair : public Node {
 | |
| public:
 | |
|     KeyValuePair(String key, NonnullRefPtr<ValueNode> value)
 | |
|         : m_key(move(key))
 | |
|         , m_value(move(value))
 | |
|     {
 | |
|     }
 | |
|     virtual ~KeyValuePair() override = default;
 | |
|     virtual void format(StringBuilder& builder, size_t indentation, bool is_inline) const override
 | |
|     {
 | |
|         if (!is_inline)
 | |
|             indent(builder, indentation);
 | |
|         builder.appendff("{}: ", m_key);
 | |
|         m_value->format(builder, indentation, true);
 | |
|         if (!is_inline)
 | |
|             builder.append('\n');
 | |
|     }
 | |
| 
 | |
|     String key() const { return m_key; }
 | |
|     NonnullRefPtr<ValueNode> value() const { return m_value; }
 | |
| 
 | |
| private:
 | |
|     String m_key;
 | |
|     NonnullRefPtr<ValueNode> m_value;
 | |
| };
 | |
| 
 | |
| // Just a mixin so that we can use JSON values in the AST
 | |
| // FIXME: Use a specialized value type for all the possible GML property values. Right now that's all possible JSON values (?)
 | |
| class JsonValueNode : public ValueNode
 | |
|     , public JsonValue {
 | |
| 
 | |
| public:
 | |
|     JsonValueNode(JsonValue const& value)
 | |
|         : JsonValue(value)
 | |
|     {
 | |
|     }
 | |
| 
 | |
|     virtual void format(StringBuilder& builder, size_t indentation, bool is_inline) const override
 | |
|     {
 | |
|         if (!is_inline)
 | |
|             indent(builder, indentation);
 | |
|         if (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;
 | |
|             as_array().for_each([&](auto& value) {
 | |
|                 if (!first)
 | |
|                     builder.append(", "sv);
 | |
|                 first = false;
 | |
|                 value.serialize(builder);
 | |
|             });
 | |
|             builder.append(']');
 | |
|         } else {
 | |
|             serialize(builder);
 | |
|         }
 | |
|         if (!is_inline)
 | |
|             builder.append('\n');
 | |
|     }
 | |
| };
 | |
| 
 | |
| // GML class declaration, starting with '@'
 | |
| class Object : public ValueNode {
 | |
| public:
 | |
|     Object() = default;
 | |
|     Object(String name, NonnullRefPtrVector<Node> properties, NonnullRefPtrVector<Node> sub_objects)
 | |
|         : m_properties(move(properties))
 | |
|         , m_sub_objects(move(sub_objects))
 | |
|         , m_name(move(name))
 | |
|     {
 | |
|     }
 | |
| 
 | |
|     virtual ~Object() override = default;
 | |
| 
 | |
|     StringView name() const { return m_name; }
 | |
|     void set_name(String name) { m_name = move(name); }
 | |
| 
 | |
|     ErrorOr<void> add_sub_object_child(NonnullRefPtr<Node> child)
 | |
|     {
 | |
|         VERIFY(is<Object>(child.ptr()) || is<Comment>(child.ptr()));
 | |
|         return m_sub_objects.try_append(move(child));
 | |
|     }
 | |
| 
 | |
|     ErrorOr<void> add_property_child(NonnullRefPtr<Node> child)
 | |
|     {
 | |
|         VERIFY(is<KeyValuePair>(child.ptr()) || is<Comment>(child.ptr()));
 | |
|         return m_properties.try_append(move(child));
 | |
|     }
 | |
| 
 | |
|     // Does not return key-value pair `layout: ...`!
 | |
|     template<typename Callback>
 | |
|     void for_each_property(Callback callback)
 | |
|     {
 | |
|         for (auto const& child : m_properties) {
 | |
|             if (is<KeyValuePair>(child)) {
 | |
|                 auto const& property = static_cast<KeyValuePair const&>(child);
 | |
|                 if (property.key() != "layout" && is<JsonValueNode>(property.value().ptr()))
 | |
|                     callback(property.key(), static_ptr_cast<JsonValueNode>(property.value()));
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     template<typename Callback>
 | |
|     void for_each_child_object(Callback callback)
 | |
|     {
 | |
|         for (NonnullRefPtr<Node> child : m_sub_objects) {
 | |
|             // doesn't capture layout as intended, as that's behind a kv-pair
 | |
|             if (is<Object>(child.ptr())) {
 | |
|                 auto object = static_ptr_cast<Object>(child);
 | |
|                 callback(object);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // Uses IterationDecision to allow the callback to interrupt the iteration, like a for-loop break.
 | |
|     template<IteratorFunction<NonnullRefPtr<Object>> Callback>
 | |
|     void for_each_child_object_interruptible(Callback callback)
 | |
|     {
 | |
|         for (NonnullRefPtr<Node> child : m_sub_objects) {
 | |
|             // doesn't capture layout as intended, as that's behind a kv-pair
 | |
|             if (is<Object>(child.ptr())) {
 | |
|                 auto object = static_ptr_cast<Object>(child);
 | |
|                 if (callback(object) == IterationDecision::Break)
 | |
|                     return;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     RefPtr<Object> layout_object() const
 | |
|     {
 | |
|         for (NonnullRefPtr<Node> child : m_properties) {
 | |
|             if (is<KeyValuePair>(child.ptr())) {
 | |
|                 auto property = static_ptr_cast<KeyValuePair>(child);
 | |
|                 if (property->key() == "layout") {
 | |
|                     VERIFY(is<Object>(property->value().ptr()));
 | |
|                     return static_ptr_cast<Object>(property->value());
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return nullptr;
 | |
|     }
 | |
| 
 | |
|     RefPtr<ValueNode> get_property(StringView property_name)
 | |
|     {
 | |
|         for (NonnullRefPtr<Node> child : m_properties) {
 | |
|             if (is<KeyValuePair>(child.ptr())) {
 | |
|                 auto property = static_ptr_cast<KeyValuePair>(child);
 | |
|                 if (property->key() == property_name)
 | |
|                     return property->value();
 | |
|             }
 | |
|         }
 | |
|         return nullptr;
 | |
|     }
 | |
| 
 | |
|     virtual void format(StringBuilder& builder, size_t indentation, bool is_inline) const override
 | |
|     {
 | |
|         if (!is_inline)
 | |
|             indent(builder, indentation);
 | |
|         builder.append('@');
 | |
|         builder.append(m_name);
 | |
|         builder.append(" {"sv);
 | |
|         if (!m_properties.is_empty() || !m_sub_objects.is_empty()) {
 | |
|             builder.append('\n');
 | |
| 
 | |
|             for (auto const& property : m_properties)
 | |
|                 property.format(builder, indentation + 1, false);
 | |
| 
 | |
|             if (!m_properties.is_empty() && !m_sub_objects.is_empty())
 | |
|                 builder.append('\n');
 | |
| 
 | |
|             // This loop is necessary as we need to know what the last child is.
 | |
|             for (size_t i = 0; i < m_sub_objects.size(); ++i) {
 | |
|                 auto const& child = m_sub_objects[i];
 | |
|                 child.format(builder, indentation + 1, false);
 | |
| 
 | |
|                 if (is<Object>(child) && i != m_sub_objects.size() - 1)
 | |
|                     builder.append('\n');
 | |
|             }
 | |
| 
 | |
|             indent(builder, indentation);
 | |
|         }
 | |
|         builder.append('}');
 | |
|         if (!is_inline)
 | |
|             builder.append('\n');
 | |
|     }
 | |
| 
 | |
| private:
 | |
|     // Properties and comments
 | |
|     NonnullRefPtrVector<Node> m_properties;
 | |
|     // Sub objects and comments
 | |
|     NonnullRefPtrVector<Node> m_sub_objects;
 | |
|     String m_name {};
 | |
| };
 | |
| 
 | |
| class GMLFile : public Node {
 | |
| public:
 | |
|     virtual ~GMLFile() override = default;
 | |
| 
 | |
|     ErrorOr<void> add_child(NonnullRefPtr<Node> child)
 | |
|     {
 | |
|         if (!has_main_class()) {
 | |
|             if (is<Comment>(child.ptr())) {
 | |
|                 return m_leading_comments.try_append(*static_ptr_cast<Comment>(child));
 | |
|             }
 | |
|             if (is<Object>(child.ptr())) {
 | |
|                 m_main_class = static_ptr_cast<Object>(child);
 | |
|                 return {};
 | |
|             }
 | |
|             return Error::from_string_literal("Unexpected data before main class");
 | |
|         }
 | |
|         // After the main class, only comments are allowed.
 | |
|         if (!is<Comment>(child.ptr()))
 | |
|             return Error::from_string_literal("Data not allowed after main class");
 | |
|         return m_trailing_comments.try_append(*static_ptr_cast<Comment>(child));
 | |
|     }
 | |
| 
 | |
|     bool has_main_class() const { return m_main_class != nullptr; }
 | |
| 
 | |
|     NonnullRefPtrVector<Comment> leading_comments() const { return m_leading_comments; }
 | |
|     Object& main_class()
 | |
|     {
 | |
|         VERIFY(!m_main_class.is_null());
 | |
|         return *m_main_class.ptr();
 | |
|     }
 | |
|     NonnullRefPtrVector<Comment> trailing_comments() const { return m_trailing_comments; }
 | |
| 
 | |
|     virtual void format(StringBuilder& builder, size_t indentation, [[maybe_unused]] bool is_inline) const override
 | |
|     {
 | |
|         for (auto const& comment : m_leading_comments)
 | |
|             comment.format(builder, indentation, false);
 | |
| 
 | |
|         if (!m_leading_comments.is_empty())
 | |
|             builder.append('\n');
 | |
|         m_main_class->format(builder, indentation, false);
 | |
|         if (!m_trailing_comments.is_empty())
 | |
|             builder.append('\n');
 | |
| 
 | |
|         for (auto const& comment : m_trailing_comments)
 | |
|             comment.format(builder, indentation, false);
 | |
|     }
 | |
| 
 | |
| private:
 | |
|     NonnullRefPtrVector<Comment> m_leading_comments;
 | |
|     RefPtr<Object> m_main_class;
 | |
|     NonnullRefPtrVector<Comment> m_trailing_comments;
 | |
| };
 | |
| 
 | |
| }
 |