/* * 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 #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); } 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(" "); } }; // 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 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 value() const { return m_value; } private: String 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(", "); 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 properties, NonnullRefPtrVector 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 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) { 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) { 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); } } } // Uses IterationDecision to allow the callback to interrupt the iteration, like a for-loop break. template> Callback> void for_each_child_object_interruptible(Callback callback) { 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); if (callback(object) == IterationDecision::Break) return; } } } RefPtr layout_object() const { for (NonnullRefPtr child : m_properties) { if (is(child.ptr())) { auto property = static_ptr_cast(child); if (property->key() == "layout") { VERIFY(is(property->value().ptr())); return static_ptr_cast(property->value()); } } } return nullptr; } RefPtr get_property(StringView property_name) { for (NonnullRefPtr child : m_properties) { if (is(child.ptr())) { auto property = static_ptr_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(" {"); 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 NonnullRefPtrVector m_properties; // Sub objects and comments NonnullRefPtrVector m_sub_objects; String 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; } NonnullRefPtrVector leading_comments() const { return m_leading_comments; } Object& main_class() { VERIFY(!m_main_class.is_null()); return *m_main_class.ptr(); } NonnullRefPtrVector 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 m_leading_comments; RefPtr m_main_class; NonnullRefPtrVector m_trailing_comments; }; }