/* * Copyright (c) 2022, kleines Filmröllchen . * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include namespace GUI::GML { class Comment; class JsonValueNode; // Base of the GML Abstract Syntax Tree (AST). class Node : public RefCounted { public: virtual ~Node() = default; template requires(IsBaseOf) static ErrorOr> from_token(Token token) { return try_make_ref_counted(token.m_view); } DeprecatedString to_deprecated_string() const { StringBuilder builder; format(builder, 0, false); return builder.to_deprecated_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(DeprecatedString 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: DeprecatedString m_text {}; }; // Any JSON-like key: value pair. class KeyValuePair : public Node { public: KeyValuePair(DeprecatedString key, NonnullRefPtr 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'); } DeprecatedString key() const { return m_key; } NonnullRefPtr value() const { return m_value; } private: DeprecatedString m_key; NonnullRefPtr 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(DeprecatedString name, Vector> properties, Vector> 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(DeprecatedString name) { m_name = move(name); } ErrorOr add_sub_object_child(NonnullRefPtr child) { VERIFY(is(child.ptr()) || is(child.ptr())); return m_sub_objects.try_append(move(child)); } ErrorOr add_property_child(NonnullRefPtr child) { VERIFY(is(child.ptr()) || is(child.ptr())); return m_properties.try_append(move(child)); } // Does not return key-value pair `layout: ...`! template void for_each_property(Callback callback) const { for (auto const& child : m_properties) { if (is(child)) { auto const& property = static_cast(*child); if (property.key() != "layout" && is(property.value().ptr())) callback(property.key(), static_ptr_cast(property.value())); } } } template void for_each_child_object(Callback callback) const { for (NonnullRefPtr child : m_sub_objects) { // doesn't capture layout as intended, as that's behind a kv-pair if (is(child.ptr())) { auto object = static_ptr_cast(child); callback(object); } } } template> Callback> ErrorOr try_for_each_child_object(Callback callback) const { for (auto const& child : m_sub_objects) { // doesn't capture layout as intended, as that's behind a kv-pair if (is(child)) { TRY(callback(static_cast(*child))); } } return {}; } RefPtr layout_object() const { for (auto const& child : m_properties) { if (is(child)) { auto const& property = static_cast(*child); if (property.key() == "layout") { VERIFY(is(property.value().ptr())); return static_cast(*property.value()); } } } return nullptr; } RefPtr get_property(StringView property_name) const { for (auto const& child : m_properties) { if (is(child)) { auto const& property = static_cast(*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(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 Vector> m_properties; // Sub objects and comments Vector> m_sub_objects; DeprecatedString m_name {}; }; class GMLFile : public Node { public: virtual ~GMLFile() override = default; ErrorOr add_child(NonnullRefPtr child) { if (!has_main_class()) { if (is(child.ptr())) { return m_leading_comments.try_append(*static_ptr_cast(child)); } if (is(child.ptr())) { m_main_class = static_ptr_cast(child); return {}; } return Error::from_string_literal("Unexpected data before main class"); } // After the main class, only comments are allowed. if (!is(child.ptr())) return Error::from_string_literal("Data not allowed after main class"); return m_trailing_comments.try_append(*static_ptr_cast(child)); } bool has_main_class() const { return m_main_class != nullptr; } Vector> leading_comments() const { return m_leading_comments; } Object const& main_class() const { VERIFY(!m_main_class.is_null()); return *m_main_class.ptr(); } Vector> 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: Vector> m_leading_comments; RefPtr m_main_class; Vector> m_trailing_comments; }; }