From 174313fe12fd710f0e23274f70dcc5c415175dc5 Mon Sep 17 00:00:00 2001 From: kayomn Date: Thu, 6 Jul 2023 00:51:15 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + .vscode/c_cpp_properties.json | 16 ++ .vscode/launch.json | 13 ++ .vscode/settings.json | 54 +++++ build.sh | 3 + lml.cpp | 386 ++++++++++++++++++++++++++++++++++ lml.hpp | 84 ++++++++ main.cpp | 31 +++ test.lml | 26 +++ 9 files changed, 614 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100755 build.sh create mode 100644 lml.cpp create mode 100644 lml.hpp create mode 100644 main.cpp create mode 100644 test.lml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9861886 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +lml_testbed diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..ffd6c8e --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/usr/sbin/clang", + "cStandard": "c17", + "cppStandard": "c++20", + "intelliSenseMode": "linux-clang-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a4a3958 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "LML", + "type": "gdb", + "request": "launch", + "target": "${workspaceRoot}/lml_testbed", + "cwd": "${workspaceRoot}/", + "valuesFormatting": "parseText" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0c9f4cc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,54 @@ +{ + "files.associations": { + "unordered_map": "cpp", + "variant": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "map": "cpp", + "string": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "initializer_list": "cpp", + "iosfwd": "cpp", + "limits": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "span": "cpp", + "iostream": "cpp", + "istream": "cpp", + "fstream": "cpp" + } +} \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..61c4a97 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +clang++ lml.cpp main.cpp -g --std=c++20 -o lml_testbed diff --git a/lml.cpp b/lml.cpp new file mode 100644 index 0000000..ce0ac0e --- /dev/null +++ b/lml.cpp @@ -0,0 +1,386 @@ +#include "lml.hpp" + +using namespace Lml; + +struct Token final +{ + enum class Kind + { + Unexpected, + Newline, + Identifier, + Colon, + Comma, + BraceLeft, + BraceRight, + }; + + Kind kind {Kind::Unexpected}; + + std::string_view view {""}; +}; + +static bool isLmlSymbol(char c) +{ + switch (c) + { + case ':': + case '[': + case ']': + case '{': + case '}': + case ' ': + case ';': + case ',': + case '`': + case '\'': + case '\t': + case '\n': + case '"': return false; + + default: return true; + } +} + +struct Tokenizer final +{ + std::string_view data {""}; + + std::optional Next() + { + while (!this->data.empty()) + { + switch (this->data[0]) + { + case ' ': + case '\t': + { + this->data = this->data.substr(1); + } + break; + + case '\n': + { + this->data = this->data.substr(1); + + return Token + { + .kind {Token::Kind::Newline}, + .view {"\n"}, + }; + } + break; + + case '{': + { + this->data = this->data.substr(1); + + return Token + { + .kind {Token::Kind::BraceLeft}, + .view {"{"}, + }; + } + break; + + case '}': + { + this->data = this->data.substr(1); + + return Token + { + .kind {Token::Kind::BraceRight}, + .view {"}"}, + }; + } + break; + + case ':': + { + this->data = this->data.substr(1); + + return Token + { + .kind {Token::Kind::Colon}, + .view {":"}, + }; + } + break; + + case ',': + { + this->data = this->data.substr(1); + + return Token + { + .kind {Token::Kind::Comma}, + .view {","}, + }; + } + break; + + case '"': + { + size_t const identifierEndIndex {this->data.find_first_of('"', 1)}; + + if (identifierEndIndex == std::string_view::npos) + { + this->data = this->data.substr(this->data.length() - 1, 0); + + return Token + { + .kind {Token::Kind::Unexpected}, + .view {""}, + }; + } + + std::string_view const identifier = this->data.substr(1, identifierEndIndex - 1); + + this->data = this->data.substr(identifierEndIndex + 1); + + return Token + { + .kind {Token::Kind::Identifier}, + .view {identifier}, + }; + } + break; + + default: + { + size_t identifierLength {0}; + + { + size_t dataLength {this->data.length()}; + + while (identifierLength < this->data.length() && isLmlSymbol(this->data[identifierLength])) + { + identifierLength += 1; + } + } + + if (identifierLength == 0) + { + return Token + { + .kind {Token::Kind::Unexpected}, + .view {""}, + }; + } + + std::string_view const identifier {this->data.substr(0, identifierLength)}; + + this->data = this->data.substr(identifierLength); + + return Token + { + .kind {Token::Kind::Identifier}, + .view {identifier}, + }; + }; + } + } + + return std::nullopt; + } +}; + +static std::optional ParseScalar(Tokenizer & tokenizer) +{ + Scalar scalar {}; + + while (true) + { + std::optional const elementToken {tokenizer.Next()}; + + if (elementToken == std::nullopt) + { + return scalar; + } + + switch (elementToken->kind) + { + case Token::Kind::Newline: return scalar; + + case Token::Kind::Identifier: + { + scalar.push_back(std::string{elementToken->view}); + } + break; + + default: return std::nullopt; + } + + std::optional const delimiterToken {tokenizer.Next()}; + + if (delimiterToken == std::nullopt) + { + return scalar; + } + + switch (delimiterToken->kind) + { + case Token::Kind::Comma: continue; + case Token::Kind::Newline: return scalar; + default: return std::nullopt; + } + } +} + +static bool ParseDocument(Tokenizer & tokenizer, Document & document, bool isRoot) +{ + while (true) + { + std::optional const initialToken {tokenizer.Next()}; + + if (initialToken == std::nullopt) + { + return isRoot; + } + + switch (initialToken->kind) + { + case Token::Kind::Newline: continue; + case Token::Kind::Identifier: break; + case Token::Kind::BraceRight: return !isRoot; + default: return false; + } + + std::optional kindToken {tokenizer.Next()}; + + if (kindToken == std::nullopt) + { + return false; + } + + if (kindToken->kind == Token::Kind::Colon) + { + std::optional const scalar {ParseScalar(tokenizer)}; + + if (scalar == std::nullopt) + { + return false; + } + + document.properties[std::string{initialToken->view}] = *scalar; + } + else + { + while (kindToken->kind != Token::Kind::BraceLeft) + { + kindToken = tokenizer.Next(); + + if (kindToken == std::nullopt) + { + return false; + } + + switch (kindToken->kind) + { + case Token::Kind::Newline: break; + case Token::Kind::BraceLeft: break; + default: return false; + } + } + + std::optional const beginSpaceToken {tokenizer.Next()}; + + if (beginSpaceToken == std::nullopt) + { + return false; + } + + switch (beginSpaceToken->kind) + { + case Token::Kind::Newline: + { + Document elementDocument {}; + + if (!ParseDocument(tokenizer, elementDocument, false)) + { + return false; + } + + document.elements.emplace_back(std::string{initialToken->view}, elementDocument); + } + break; + + case Token::Kind::BraceRight: + { + document.elements.emplace_back(std::string{initialToken->view}, Document{}); + + std::optional const endSpaceToken {tokenizer.Next()}; + + if (endSpaceToken == std::nullopt) + { + return true; + } + + switch (endSpaceToken->kind) + { + case Token::Kind::Newline: return true; + default: return false; + } + } + break; + + default: return false; + } + } + } +} + +bool Lml::Document::Parse(std::string_view const & data) +{ + Tokenizer tokenizer {data}; + + return ParseDocument(tokenizer, *this, true); +} + +std::optional Lml::Document::Property(std::string_view const & property) +{ + if (PropertyMap::iterator found {this->properties.find(property)}; found != this->properties.end()) + { + if (!found->second.empty()) + { + return found->second[0]; + } + } + + return std::nullopt; +} + +void Lml::Document::SerializeTo(std::string & string, size_t indentation) const +{ + for (auto const & [property, scalar] : this->properties) + { + string.append(indentation, '\t'); + string.append(property); + string.append(": "); + + if (!scalar.empty()) + { + string.append(scalar[0]); + + for (std::string const & scalarString : std::span{scalar.begin() + 1, scalar.end()}) + { + string.append(", "); + string.append(scalarString); + } + } + + string.push_back('\n'); + } + + for (auto const & [element, document] : this->elements) + { + string.append(indentation, '\t'); + string.append(element); + string.append(" {\n"); + document.SerializeTo(string, indentation + 1); + string.append(indentation, '\t'); + string.append("}\n"); + } + + string.push_back('\n'); +} diff --git a/lml.hpp b/lml.hpp new file mode 100644 index 0000000..0ab95ad --- /dev/null +++ b/lml.hpp @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Lml +{ + /// + /// Tree leaf composed of zero or more values. + /// + using Scalar = std::vector; + + /// + /// Content body capable of holding Scalars as properties and other child Documents as elements. + /// + struct Document final + { + /// + /// Custom hashing mechanism used by properties to support lookups with string types other than std::string. + /// + struct PropertyHash + { + using HashType = std::hash; + + using is_transparent = void; + + size_t operator()(const char* str) const + { + return HashType{}(str); + } + + size_t operator()(std::string_view str) const + { + return HashType{}(str); + } + + size_t operator()(std::string const& str) const + { + return HashType{}(str); + } + }; + + /// + /// Map of property names and their associated Scalar. + /// + using PropertyMap = std::unordered_map>; + + /// + /// List of associated element names and Documents. + /// + using ElementList = std::vector>; + + /// + /// All live property Scalars belonging to the Document. + /// + PropertyMap properties {}; + + /// + /// All live element Documents belonging to the Document. + /// + ElementList elements {}; + + /// + /// Attempts to parse the provided data into the live Document object model, returning true if it was + /// successful, otherwise false. + /// + bool Parse(std::string_view const & data); + + /// + /// Attempts to retrieve a single string value from a Scalar defined under the provided property name, + /// returning it or null if no such property exists. + /// + std::optional Property(std::string_view const & property); + + /// + /// Serializes the live Document object model to the provided string with the specified indentation level. + /// + void SerializeTo(std::string & string, size_t indentation) const; + }; +} \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..226c06c --- /dev/null +++ b/main.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include "lml.hpp" + +int main(int argc, char const * const * argv) +{ + Lml::Document document {}; + + { + std::ifstream testFile {"test.lml", std::ios::binary}; + std::string testData {}; + + testFile.seekg(0, std::ios::end); + testData.resize(testFile.tellg(), 0); + testFile.seekg(0, std::ios::beg); + testFile.read(testData.data(), testData.size()); + + if (!document.Parse(testData)) + { + std::cout << "Parse error"; + } + else + { + testData.clear(); + document.SerializeTo(testData, 0); + + std::cout << testData; + } + } +} \ No newline at end of file diff --git a/test.lml b/test.lml new file mode 100644 index 0000000..b6f1496 --- /dev/null +++ b/test.lml @@ -0,0 +1,26 @@ +Styles { + Readout { + fontFamily: "Comic Sans.ttf" + labelSize: 2em + color: #ffffffff + } +} + +Items { + Canvas { + rect: 0.85, 0.9, 0.95, 0.95 + + Sprite { + path: "hudtexture.dds" + position: 0, 0 + stretch: 1, 1 + } + + SpeedLabel { + text: "Current Speed: $Speed" + position: 0.5, 0.5 + alignment: mid, mid + styles: Readout + } + } +}