mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 17:27:35 +00:00
LibGUI: Remove GML prefix in favor of proper namespace
Prefixes are very much a C thing which we don't need in C++. This commit moves all GML-related classes in LibGUI into the GUI::GML namespace, a change somewhat overdue.
This commit is contained in:
parent
89201b682b
commit
4931c88b13
17 changed files with 129 additions and 125 deletions
245
Userland/Libraries/LibGUI/GML/AutocompleteProvider.cpp
Normal file
245
Userland/Libraries/LibGUI/GML/AutocompleteProvider.cpp
Normal file
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2021, thislooksfun <tlf@thislooks.fun>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "AutocompleteProvider.h"
|
||||
#include "Lexer.h"
|
||||
#include <AK/QuickSort.h>
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
void AutocompleteProvider::provide_completions(Function<void(Vector<Entry>)> callback)
|
||||
{
|
||||
auto cursor = m_editor->cursor();
|
||||
auto text = m_editor->text();
|
||||
Lexer lexer(text);
|
||||
// FIXME: Provide a begin() and end() for lexers PLEASE!
|
||||
auto all_tokens = lexer.lex();
|
||||
enum State {
|
||||
Free,
|
||||
InClassName,
|
||||
AfterClassName,
|
||||
InIdentifier,
|
||||
AfterIdentifier, // Can we introspect this?
|
||||
} state { Free };
|
||||
String identifier_string;
|
||||
Vector<String> class_names;
|
||||
Vector<State> previous_states;
|
||||
bool should_push_state { true };
|
||||
Token* last_seen_token { nullptr };
|
||||
Token* last_identifier_token { nullptr };
|
||||
|
||||
for (auto& token : all_tokens) {
|
||||
auto handle_class_child = [&] {
|
||||
if (token.m_type == Token::Type::Identifier) {
|
||||
state = InIdentifier;
|
||||
identifier_string = token.m_view;
|
||||
last_identifier_token = &token;
|
||||
} else if (token.m_type == Token::Type::ClassMarker) {
|
||||
previous_states.append(AfterClassName);
|
||||
state = Free;
|
||||
should_push_state = false;
|
||||
}
|
||||
};
|
||||
|
||||
if (token.m_start.line > cursor.line() || (token.m_start.line == cursor.line() && token.m_start.column > cursor.column()))
|
||||
break;
|
||||
|
||||
last_seen_token = &token;
|
||||
switch (state) {
|
||||
case Free:
|
||||
if (token.m_type == Token::Type::ClassName) {
|
||||
if (should_push_state)
|
||||
previous_states.append(state);
|
||||
else
|
||||
should_push_state = true;
|
||||
state = InClassName;
|
||||
class_names.append(token.m_view);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case InClassName:
|
||||
if (token.m_type != Token::Type::LeftCurly) {
|
||||
// Close empty class and immediately handle our parent's next child
|
||||
class_names.take_last();
|
||||
state = previous_states.take_last();
|
||||
|
||||
if (state == AfterClassName)
|
||||
handle_class_child();
|
||||
|
||||
break;
|
||||
}
|
||||
state = AfterClassName;
|
||||
break;
|
||||
case AfterClassName:
|
||||
handle_class_child();
|
||||
|
||||
if (token.m_type == Token::Type::RightCurly) {
|
||||
class_names.take_last();
|
||||
state = previous_states.take_last();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case InIdentifier:
|
||||
if (token.m_type == Token::Type::Colon)
|
||||
state = AfterIdentifier;
|
||||
break;
|
||||
case AfterIdentifier:
|
||||
if (token.m_type == Token::Type::RightCurly || token.m_type == Token::Type::LeftCurly)
|
||||
break;
|
||||
if (token.m_type == Token::Type::ClassMarker) {
|
||||
previous_states.append(AfterClassName);
|
||||
state = Free;
|
||||
should_push_state = false;
|
||||
} else {
|
||||
state = AfterClassName;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (state == InClassName && last_seen_token && last_seen_token->m_end.line < cursor.line()) {
|
||||
// Close empty class
|
||||
class_names.take_last();
|
||||
state = previous_states.take_last();
|
||||
}
|
||||
|
||||
auto& widget_class = *Core::ObjectClassRegistration::find("GUI::Widget");
|
||||
auto& layout_class = *Core::ObjectClassRegistration::find("GUI::Layout");
|
||||
|
||||
// FIXME: Can this be done without a StringBuilder?
|
||||
auto make_fuzzy = [](StringView str) {
|
||||
auto fuzzy_str_builder = StringBuilder(str.length() * 2 + 1);
|
||||
fuzzy_str_builder.append('*');
|
||||
for (auto character : str) {
|
||||
fuzzy_str_builder.append(character);
|
||||
fuzzy_str_builder.append('*');
|
||||
}
|
||||
return fuzzy_str_builder.build();
|
||||
};
|
||||
|
||||
Vector<AutocompleteProvider::Entry> class_entries, identifier_entries;
|
||||
|
||||
auto register_layouts_matching_pattern = [&](String pattern, size_t partial_input_length) {
|
||||
Core::ObjectClassRegistration::for_each([&](const Core::ObjectClassRegistration& registration) {
|
||||
if (registration.is_derived_from(layout_class) && ®istration != &layout_class && registration.class_name().matches(pattern))
|
||||
class_entries.empend(String::formatted("@{}", registration.class_name()), partial_input_length);
|
||||
});
|
||||
};
|
||||
|
||||
auto register_widgets_matching_pattern = [&](String pattern, size_t partial_input_length) {
|
||||
Core::ObjectClassRegistration::for_each([&](const Core::ObjectClassRegistration& registration) {
|
||||
if (registration.is_derived_from(widget_class) && registration.class_name().matches(pattern))
|
||||
class_entries.empend(String::formatted("@{}", registration.class_name()), partial_input_length);
|
||||
});
|
||||
};
|
||||
|
||||
auto register_class_properties_matching_pattern = [&](String pattern, size_t partial_input_length) {
|
||||
auto class_name = class_names.last();
|
||||
|
||||
// FIXME: Don't show properties that are already specified in the scope.
|
||||
auto registration = Core::ObjectClassRegistration::find(class_name);
|
||||
if (registration && (registration->is_derived_from(widget_class) || registration->is_derived_from(layout_class))) {
|
||||
if (auto instance = registration->construct()) {
|
||||
for (auto& it : instance->properties()) {
|
||||
if (!it.value->is_readonly() && it.key.matches(pattern))
|
||||
identifier_entries.empend(String::formatted("{}: ", it.key), partial_input_length, Language::Unspecified, it.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (can_have_declared_layout(class_names.last()) && "layout"sv.matches(pattern))
|
||||
identifier_entries.empend("layout: ", partial_input_length, Language::Unspecified, "layout", AutocompleteProvider::Entry::HideAutocompleteAfterApplying::No);
|
||||
if (class_names.last() == "GUI::ScrollableContainerWidget" && "content_widget"sv.matches(pattern))
|
||||
identifier_entries.empend("content_widget: ", partial_input_length, Language::Unspecified, "content_widget", AutocompleteProvider::Entry::HideAutocompleteAfterApplying::No);
|
||||
};
|
||||
|
||||
auto register_properties_and_widgets_matching_pattern = [&](String pattern, size_t partial_input_length) {
|
||||
if (!class_names.is_empty()) {
|
||||
register_class_properties_matching_pattern(pattern, partial_input_length);
|
||||
|
||||
auto parent_registration = Core::ObjectClassRegistration::find(class_names.last());
|
||||
if (parent_registration && parent_registration->is_derived_from(layout_class)) {
|
||||
// Layouts can't have child classes, so why suggest them?
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
register_widgets_matching_pattern(pattern, partial_input_length);
|
||||
};
|
||||
|
||||
bool after_token_on_same_line = last_seen_token && last_seen_token->m_end.column != cursor.column() && last_seen_token->m_end.line == cursor.line();
|
||||
switch (state) {
|
||||
case Free:
|
||||
if (after_token_on_same_line) {
|
||||
// After some token, but with extra space, not on a new line.
|
||||
// Nothing to put here.
|
||||
break;
|
||||
}
|
||||
|
||||
register_widgets_matching_pattern("*", 0u);
|
||||
break;
|
||||
case InClassName: {
|
||||
if (class_names.is_empty())
|
||||
break;
|
||||
if (after_token_on_same_line) {
|
||||
// After a class name, but haven't seen braces.
|
||||
// TODO: Suggest braces?
|
||||
break;
|
||||
}
|
||||
|
||||
auto class_name = class_names.last();
|
||||
auto fuzzy_class = make_fuzzy(class_name);
|
||||
if (last_identifier_token && last_identifier_token->m_end.line == last_seen_token->m_end.line && identifier_string == "layout")
|
||||
register_layouts_matching_pattern(fuzzy_class, class_name.length() + 1);
|
||||
else
|
||||
register_widgets_matching_pattern(fuzzy_class, class_name.length() + 1);
|
||||
|
||||
break;
|
||||
}
|
||||
case InIdentifier:
|
||||
if (after_token_on_same_line) {
|
||||
// After an identifier, but with extra space
|
||||
// TODO: Maybe suggest a colon?
|
||||
break;
|
||||
}
|
||||
|
||||
register_properties_and_widgets_matching_pattern(make_fuzzy(identifier_string), identifier_string.length());
|
||||
break;
|
||||
case AfterClassName:
|
||||
if (last_seen_token && last_seen_token->m_end.line == cursor.line()) {
|
||||
if (last_seen_token->m_type != Token::Type::Identifier || last_seen_token->m_end.column != cursor.column()) {
|
||||
// Inside braces, but on the same line as some other stuff (and not the continuation of one!)
|
||||
// The user expects nothing here.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
register_properties_and_widgets_matching_pattern("*", 0u);
|
||||
break;
|
||||
case AfterIdentifier:
|
||||
if (last_seen_token && last_seen_token->m_end.line != cursor.line())
|
||||
break;
|
||||
if (identifier_string == "layout")
|
||||
register_layouts_matching_pattern("*", 0u);
|
||||
if (identifier_string == "content_widget")
|
||||
register_widgets_matching_pattern("*", 0u);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
quick_sort(class_entries, [](auto& a, auto& b) { return a.completion < b.completion; });
|
||||
quick_sort(identifier_entries, [](auto& a, auto& b) { return a.completion < b.completion; });
|
||||
|
||||
Vector<GUI::AutocompleteProvider::Entry> entries;
|
||||
entries.extend(move(identifier_entries));
|
||||
entries.extend(move(class_entries));
|
||||
|
||||
callback(move(entries));
|
||||
}
|
||||
|
||||
}
|
27
Userland/Libraries/LibGUI/GML/AutocompleteProvider.h
Normal file
27
Userland/Libraries/LibGUI/GML/AutocompleteProvider.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../AutocompleteProvider.h"
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
class AutocompleteProvider final : public virtual GUI::AutocompleteProvider {
|
||||
public:
|
||||
AutocompleteProvider() { }
|
||||
virtual ~AutocompleteProvider() override { }
|
||||
|
||||
private:
|
||||
static bool can_have_declared_layout(StringView class_name)
|
||||
{
|
||||
return class_name.is_one_of("GUI::Widget", "GUI::Frame");
|
||||
}
|
||||
|
||||
virtual void provide_completions(Function<void(Vector<Entry>)> callback) override;
|
||||
};
|
||||
|
||||
}
|
101
Userland/Libraries/LibGUI/GML/Formatter.cpp
Normal file
101
Userland/Libraries/LibGUI/GML/Formatter.cpp
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
15
Userland/Libraries/LibGUI/GML/Formatter.h
Normal file
15
Userland/Libraries/LibGUI/GML/Formatter.h
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
String format_gml(StringView);
|
||||
|
||||
}
|
154
Userland/Libraries/LibGUI/GML/Lexer.cpp
Normal file
154
Userland/Libraries/LibGUI/GML/Lexer.cpp
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "Lexer.h"
|
||||
#include <AK/CharacterTypes.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
Lexer::Lexer(StringView input)
|
||||
: m_input(input)
|
||||
{
|
||||
}
|
||||
|
||||
char Lexer::peek(size_t offset) const
|
||||
{
|
||||
if ((m_index + offset) >= m_input.length())
|
||||
return 0;
|
||||
return m_input[m_index + offset];
|
||||
}
|
||||
|
||||
char Lexer::consume()
|
||||
{
|
||||
VERIFY(m_index < m_input.length());
|
||||
char ch = m_input[m_index++];
|
||||
if (ch == '\n') {
|
||||
m_position.line++;
|
||||
m_position.column = 0;
|
||||
} else {
|
||||
m_position.column++;
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
constexpr bool is_valid_identifier_start(char ch)
|
||||
{
|
||||
return is_ascii_alpha(ch) || ch == '_';
|
||||
}
|
||||
|
||||
constexpr bool is_valid_identifier_character(char ch)
|
||||
{
|
||||
return is_ascii_alphanumeric(ch) || ch == '_';
|
||||
}
|
||||
|
||||
constexpr bool is_valid_class_character(char ch)
|
||||
{
|
||||
return is_ascii_alphanumeric(ch) || ch == '_' || ch == ':';
|
||||
}
|
||||
|
||||
Vector<Token> Lexer::lex()
|
||||
{
|
||||
Vector<Token> tokens;
|
||||
|
||||
size_t token_start_index = 0;
|
||||
Position token_start_position;
|
||||
|
||||
auto begin_token = [&] {
|
||||
token_start_index = m_index;
|
||||
token_start_position = m_position;
|
||||
};
|
||||
|
||||
auto commit_token = [&](auto type) {
|
||||
Token token;
|
||||
token.m_view = m_input.substring_view(token_start_index, m_index - token_start_index);
|
||||
token.m_type = type;
|
||||
token.m_start = token_start_position;
|
||||
token.m_end = m_position;
|
||||
tokens.append(token);
|
||||
};
|
||||
|
||||
auto consume_class = [&] {
|
||||
begin_token();
|
||||
consume();
|
||||
commit_token(Token::Type::ClassMarker);
|
||||
begin_token();
|
||||
while (is_valid_class_character(peek()))
|
||||
consume();
|
||||
commit_token(Token::Type::ClassName);
|
||||
};
|
||||
|
||||
while (m_index < m_input.length()) {
|
||||
if (is_ascii_space(peek(0))) {
|
||||
begin_token();
|
||||
while (is_ascii_space(peek()))
|
||||
consume();
|
||||
continue;
|
||||
}
|
||||
|
||||
// C++ style comments
|
||||
if (peek(0) && peek(0) == '/' && peek(1) == '/') {
|
||||
begin_token();
|
||||
while (peek() && peek() != '\n')
|
||||
consume();
|
||||
commit_token(Token::Type::Comment);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peek(0) == '{') {
|
||||
begin_token();
|
||||
consume();
|
||||
commit_token(Token::Type::LeftCurly);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peek(0) == '}') {
|
||||
begin_token();
|
||||
consume();
|
||||
commit_token(Token::Type::RightCurly);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peek(0) == '@') {
|
||||
consume_class();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_valid_identifier_start(peek(0))) {
|
||||
begin_token();
|
||||
consume();
|
||||
while (is_valid_identifier_character(peek(0)))
|
||||
consume();
|
||||
commit_token(Token::Type::Identifier);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peek(0) == ':') {
|
||||
begin_token();
|
||||
consume();
|
||||
commit_token(Token::Type::Colon);
|
||||
|
||||
while (is_ascii_space(peek()))
|
||||
consume();
|
||||
|
||||
if (peek(0) == '@') {
|
||||
consume_class();
|
||||
} else {
|
||||
begin_token();
|
||||
while (peek() && peek() != '\n')
|
||||
consume();
|
||||
commit_token(Token::Type::JsonValue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
consume();
|
||||
commit_token(Token::Type::Unknown);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
}
|
69
Userland/Libraries/LibGUI/GML/Lexer.h
Normal file
69
Userland/Libraries/LibGUI/GML/Lexer.h
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/StringView.h>
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
#define FOR_EACH_TOKEN_TYPE \
|
||||
__TOKEN(Unknown) \
|
||||
__TOKEN(Comment) \
|
||||
__TOKEN(ClassMarker) \
|
||||
__TOKEN(ClassName) \
|
||||
__TOKEN(LeftCurly) \
|
||||
__TOKEN(RightCurly) \
|
||||
__TOKEN(Identifier) \
|
||||
__TOKEN(Colon) \
|
||||
__TOKEN(JsonValue)
|
||||
|
||||
struct Position {
|
||||
size_t line;
|
||||
size_t column;
|
||||
};
|
||||
|
||||
struct Token {
|
||||
enum class Type {
|
||||
#define __TOKEN(x) x,
|
||||
FOR_EACH_TOKEN_TYPE
|
||||
#undef __TOKEN
|
||||
};
|
||||
|
||||
char const* to_string() const
|
||||
{
|
||||
switch (m_type) {
|
||||
#define __TOKEN(x) \
|
||||
case Type::x: \
|
||||
return #x;
|
||||
FOR_EACH_TOKEN_TYPE
|
||||
#undef __TOKEN
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
Type m_type { Type::Unknown };
|
||||
StringView m_view;
|
||||
Position m_start;
|
||||
Position m_end;
|
||||
};
|
||||
|
||||
class Lexer {
|
||||
public:
|
||||
Lexer(StringView);
|
||||
|
||||
Vector<Token> lex();
|
||||
|
||||
private:
|
||||
char peek(size_t offset = 0) const;
|
||||
char consume();
|
||||
|
||||
StringView m_input;
|
||||
size_t m_index { 0 };
|
||||
Position m_position { 0, 0 };
|
||||
};
|
||||
|
||||
}
|
141
Userland/Libraries/LibGUI/GML/Parser.cpp
Normal file
141
Userland/Libraries/LibGUI/GML/Parser.cpp
Normal file
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "Parser.h"
|
||||
#include "Lexer.h"
|
||||
#include <AK/GenericLexer.h>
|
||||
#include <AK/JsonObject.h>
|
||||
#include <AK/JsonValue.h>
|
||||
#include <AK/Queue.h>
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
static Optional<JsonValue> parse_core_object(Queue<Token>& tokens)
|
||||
{
|
||||
JsonObject object;
|
||||
JsonArray children;
|
||||
|
||||
auto peek = [&] {
|
||||
if (tokens.is_empty())
|
||||
return Token::Type::Unknown;
|
||||
return tokens.head().m_type;
|
||||
};
|
||||
|
||||
while (peek() == Token::Type::Comment)
|
||||
tokens.dequeue();
|
||||
|
||||
if (peek() != Token::Type::ClassMarker) {
|
||||
dbgln("Expected class marker");
|
||||
return {};
|
||||
}
|
||||
|
||||
tokens.dequeue();
|
||||
|
||||
if (peek() != Token::Type::ClassName) {
|
||||
dbgln("Expected class name");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto class_name = tokens.dequeue();
|
||||
object.set("class", JsonValue(class_name.m_view));
|
||||
|
||||
if (peek() != Token::Type::LeftCurly) {
|
||||
// Empty object
|
||||
return object;
|
||||
}
|
||||
tokens.dequeue();
|
||||
|
||||
for (;;) {
|
||||
if (peek() == Token::Type::RightCurly) {
|
||||
// End of object
|
||||
break;
|
||||
}
|
||||
|
||||
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());
|
||||
} 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 (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));
|
||||
} else if (peek() == Token::Type::Comment) {
|
||||
tokens.dequeue();
|
||||
} else {
|
||||
dbgln("Expected child, property, comment, or }}");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (peek() != Token::Type::RightCurly) {
|
||||
dbgln("Expected }}");
|
||||
return {};
|
||||
}
|
||||
tokens.dequeue();
|
||||
|
||||
if (!children.is_empty())
|
||||
object.set("children", move(children));
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
JsonValue parse_gml(StringView string)
|
||||
{
|
||||
auto lexer = Lexer(string);
|
||||
|
||||
Queue<Token> tokens;
|
||||
for (auto& token : lexer.lex())
|
||||
tokens.enqueue(token);
|
||||
|
||||
auto root = parse_core_object(tokens);
|
||||
|
||||
if (!root.has_value())
|
||||
return JsonValue();
|
||||
|
||||
return root.release_value();
|
||||
}
|
||||
|
||||
}
|
16
Userland/Libraries/LibGUI/GML/Parser.h
Normal file
16
Userland/Libraries/LibGUI/GML/Parser.h
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2022, kleines Filmröllchen <filmroellchen@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
JsonValue parse_gml(StringView);
|
||||
|
||||
}
|
84
Userland/Libraries/LibGUI/GML/SyntaxHighlighter.cpp
Normal file
84
Userland/Libraries/LibGUI/GML/SyntaxHighlighter.cpp
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2021, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "SyntaxHighlighter.h"
|
||||
#include "Lexer.h"
|
||||
#include <LibGfx/Palette.h>
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
static Syntax::TextStyle style_for_token_type(const Gfx::Palette& palette, Token::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Token::Type::LeftCurly:
|
||||
case Token::Type::RightCurly:
|
||||
return { palette.syntax_punctuation() };
|
||||
case Token::Type::ClassMarker:
|
||||
return { palette.syntax_keyword() };
|
||||
case Token::Type::ClassName:
|
||||
return { palette.syntax_identifier(), true };
|
||||
case Token::Type::Identifier:
|
||||
return { palette.syntax_identifier() };
|
||||
case Token::Type::JsonValue:
|
||||
return { palette.syntax_string() };
|
||||
case Token::Type::Comment:
|
||||
return { palette.syntax_comment() };
|
||||
default:
|
||||
return { palette.base_text() };
|
||||
}
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::is_identifier(u64 token) const
|
||||
{
|
||||
auto ini_token = static_cast<Token::Type>(token);
|
||||
return ini_token == Token::Type::Identifier;
|
||||
}
|
||||
|
||||
void SyntaxHighlighter::rehighlight(const Palette& palette)
|
||||
{
|
||||
auto text = m_client->get_text();
|
||||
Lexer lexer(text);
|
||||
auto tokens = lexer.lex();
|
||||
|
||||
Vector<GUI::TextDocumentSpan> spans;
|
||||
for (auto& token : tokens) {
|
||||
GUI::TextDocumentSpan span;
|
||||
span.range.set_start({ token.m_start.line, token.m_start.column });
|
||||
span.range.set_end({ token.m_end.line, token.m_end.column });
|
||||
auto style = style_for_token_type(palette, token.m_type);
|
||||
span.attributes.color = style.color;
|
||||
span.attributes.bold = style.bold;
|
||||
span.is_skippable = false;
|
||||
span.data = static_cast<u64>(token.m_type);
|
||||
spans.append(span);
|
||||
}
|
||||
m_client->do_set_spans(move(spans));
|
||||
|
||||
m_has_brace_buddies = false;
|
||||
highlight_matching_token_pair();
|
||||
|
||||
m_client->do_update();
|
||||
}
|
||||
|
||||
Vector<SyntaxHighlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs_impl() const
|
||||
{
|
||||
static Vector<MatchingTokenPair> pairs;
|
||||
if (pairs.is_empty()) {
|
||||
pairs.append({ static_cast<u64>(Token::Type::LeftCurly), static_cast<u64>(Token::Type::RightCurly) });
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
bool SyntaxHighlighter::token_types_equal(u64 token1, u64 token2) const
|
||||
{
|
||||
return static_cast<Token::Type>(token1) == static_cast<Token::Type>(token2);
|
||||
}
|
||||
|
||||
SyntaxHighlighter::~SyntaxHighlighter()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
28
Userland/Libraries/LibGUI/GML/SyntaxHighlighter.h
Normal file
28
Userland/Libraries/LibGUI/GML/SyntaxHighlighter.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2021, Andreas Kling <kling@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibSyntax/Highlighter.h>
|
||||
|
||||
namespace GUI::GML {
|
||||
|
||||
class SyntaxHighlighter final : public Syntax::Highlighter {
|
||||
public:
|
||||
SyntaxHighlighter() { }
|
||||
virtual ~SyntaxHighlighter() override;
|
||||
|
||||
virtual bool is_identifier(u64) const override;
|
||||
|
||||
virtual Syntax::Language language() const override { return Syntax::Language::GML; }
|
||||
virtual void rehighlight(const Palette&) override;
|
||||
|
||||
protected:
|
||||
virtual Vector<MatchingTokenPair> matching_token_pairs_impl() const override;
|
||||
virtual bool token_types_equal(u64, u64) const override;
|
||||
};
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue