diff --git a/Userland/Libraries/LibGUI/GML/AST.h b/Userland/Libraries/LibGUI/GML/AST.h new file mode 100644 index 0000000000..665a4cd7a6 --- /dev/null +++ b/Userland/Libraries/LibGUI/GML/AST.h @@ -0,0 +1,322 @@ +/* + * 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 children) + : m_children(move(children)) + , m_name(move(name)) + { + } + + virtual ~Object() override = default; + + StringView name() const { return m_name; } + void set_name(String name) { m_name = move(name); } + NonnullRefPtrVector children() const { return m_children; } + + ErrorOr add_child(NonnullRefPtr child) + { + return m_children.try_append(move(child)); + } + + // Does not return key-value pair `layout: ...`! + template + void for_each_property(Callback callback) + { + for (auto const& child : m_children) { + 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_children) { + // 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_children) { + // 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_children) { + 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_children) { + 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); + + if (!m_children.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_children.size(); ++i) { + auto const& child = m_children[i]; + child.format(builder, indentation + 1, false); + + if (is(child) && i != m_children.size() - 1) + builder.append('\n'); + } + + indent(builder, indentation); + builder.append('}'); + } + builder.append('\n'); + } + +private: + // Any node contained in the object body, i.e. properties, comments and subobjects. + NonnullRefPtrVector m_children; + 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; +}; + +}