Finalize library bootstrap status and stabilize async asset regressions
This commit is contained in:
878
engine/src/Resources/UI/UIDocumentCompiler.cpp
Normal file
878
engine/src/Resources/UI/UIDocumentCompiler.cpp
Normal file
@@ -0,0 +1,878 @@
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
Containers::String ToContainersString(const std::string& value) {
|
||||
return Containers::String(value.c_str());
|
||||
}
|
||||
|
||||
std::string ToStdString(const Containers::String& value) {
|
||||
return std::string(value.CStr());
|
||||
}
|
||||
|
||||
std::string ToLowerCopy(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
Containers::String NormalizePathString(const fs::path& path) {
|
||||
return Containers::String(path.lexically_normal().generic_string().c_str());
|
||||
}
|
||||
|
||||
Containers::String FormatDiagnosticMessage(const Containers::String& path,
|
||||
const UIDocumentSourceLocation& location,
|
||||
const Containers::String& message) {
|
||||
return path +
|
||||
":" +
|
||||
Containers::String(std::to_string(location.line).c_str()) +
|
||||
":" +
|
||||
Containers::String(std::to_string(location.column).c_str()) +
|
||||
": " +
|
||||
message;
|
||||
}
|
||||
|
||||
void WriteString(std::ofstream& stream, const Containers::String& value) {
|
||||
const Core::uint32 length = static_cast<Core::uint32>(value.Length());
|
||||
stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
|
||||
if (length > 0) {
|
||||
stream.write(value.CStr(), length);
|
||||
}
|
||||
}
|
||||
|
||||
bool ReadString(std::ifstream& stream, Containers::String& outValue) {
|
||||
Core::uint32 length = 0;
|
||||
stream.read(reinterpret_cast<char*>(&length), sizeof(length));
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
outValue.Clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string buffer(length, '\0');
|
||||
stream.read(buffer.data(), length);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outValue = ToContainersString(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteNode(std::ofstream& stream, const UIDocumentNode& node) {
|
||||
WriteString(stream, node.tagName);
|
||||
|
||||
UIDocumentArtifactNodeHeader header;
|
||||
header.attributeCount = static_cast<Core::uint32>(node.attributes.Size());
|
||||
header.childCount = static_cast<Core::uint32>(node.children.Size());
|
||||
header.line = node.location.line;
|
||||
header.column = node.location.column;
|
||||
header.selfClosing = node.selfClosing ? 1u : 0u;
|
||||
stream.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const UIDocumentAttribute& attribute : node.attributes) {
|
||||
WriteString(stream, attribute.name);
|
||||
WriteString(stream, attribute.value);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const UIDocumentNode& child : node.children) {
|
||||
if (!WriteNode(stream, child)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<bool>(stream);
|
||||
}
|
||||
|
||||
bool ReadNode(std::ifstream& stream, UIDocumentNode& outNode) {
|
||||
if (!ReadString(stream, outNode.tagName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentArtifactNodeHeader header;
|
||||
stream.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outNode.location.line = header.line;
|
||||
outNode.location.column = header.column;
|
||||
outNode.selfClosing = header.selfClosing != 0;
|
||||
|
||||
outNode.attributes.Clear();
|
||||
outNode.children.Clear();
|
||||
outNode.attributes.Reserve(header.attributeCount);
|
||||
outNode.children.Reserve(header.childCount);
|
||||
|
||||
for (Core::uint32 index = 0; index < header.attributeCount; ++index) {
|
||||
UIDocumentAttribute attribute;
|
||||
if (!ReadString(stream, attribute.name) ||
|
||||
!ReadString(stream, attribute.value)) {
|
||||
return false;
|
||||
}
|
||||
outNode.attributes.PushBack(std::move(attribute));
|
||||
}
|
||||
|
||||
for (Core::uint32 index = 0; index < header.childCount; ++index) {
|
||||
UIDocumentNode child;
|
||||
if (!ReadNode(stream, child)) {
|
||||
return false;
|
||||
}
|
||||
outNode.children.PushBack(std::move(child));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsNameStartChar(char ch) {
|
||||
return std::isalpha(static_cast<unsigned char>(ch)) != 0 ||
|
||||
ch == '_' ||
|
||||
ch == ':';
|
||||
}
|
||||
|
||||
bool IsNameChar(char ch) {
|
||||
return std::isalnum(static_cast<unsigned char>(ch)) != 0 ||
|
||||
ch == '_' ||
|
||||
ch == '-' ||
|
||||
ch == ':' ||
|
||||
ch == '.';
|
||||
}
|
||||
|
||||
bool LooksLikeUIDocumentReference(const Containers::String& value) {
|
||||
const std::string trimmed = ToLowerCopy(ToStdString(value.Trim()));
|
||||
return trimmed.size() > 5 &&
|
||||
(trimmed.size() >= 5 && trimmed.rfind(".xcui") == trimmed.size() - 5 ||
|
||||
trimmed.size() >= 8 && trimmed.rfind(".xctheme") == trimmed.size() - 8 ||
|
||||
trimmed.size() >= 9 && trimmed.rfind(".xcschema") == trimmed.size() - 9);
|
||||
}
|
||||
|
||||
void AppendUniqueDependency(const Containers::String& dependencyPath,
|
||||
std::unordered_set<std::string>& seenDependencies,
|
||||
Containers::Array<Containers::String>& outDependencies) {
|
||||
const std::string key = ToLowerCopy(ToStdString(dependencyPath));
|
||||
if (seenDependencies.insert(key).second) {
|
||||
outDependencies.PushBack(dependencyPath);
|
||||
}
|
||||
}
|
||||
|
||||
bool CollectUIDocumentDependencies(const fs::path& sourcePath,
|
||||
const UIDocumentNode& node,
|
||||
Containers::Array<Containers::String>& outDependencies,
|
||||
Containers::Array<UIDocumentDiagnostic>& inOutDiagnostics,
|
||||
Containers::String& outErrorMessage,
|
||||
std::unordered_set<std::string>& seenDependencies) {
|
||||
const fs::path sourceDirectory = sourcePath.parent_path();
|
||||
|
||||
for (const UIDocumentAttribute& attribute : node.attributes) {
|
||||
if (!LooksLikeUIDocumentReference(attribute.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Containers::String trimmedValue = attribute.value.Trim();
|
||||
fs::path dependencyPath(trimmedValue.CStr());
|
||||
if (!dependencyPath.is_absolute()) {
|
||||
dependencyPath = sourceDirectory / dependencyPath;
|
||||
}
|
||||
|
||||
dependencyPath = dependencyPath.lexically_normal();
|
||||
if (!fs::exists(dependencyPath) || fs::is_directory(dependencyPath)) {
|
||||
UIDocumentDiagnostic diagnostic;
|
||||
diagnostic.severity = UIDocumentDiagnosticSeverity::Error;
|
||||
diagnostic.location = node.location;
|
||||
diagnostic.message =
|
||||
Containers::String("Referenced UI document was not found: ") + trimmedValue;
|
||||
inOutDiagnostics.PushBack(diagnostic);
|
||||
outErrorMessage = FormatDiagnosticMessage(
|
||||
NormalizePathString(sourcePath),
|
||||
diagnostic.location,
|
||||
diagnostic.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
AppendUniqueDependency(
|
||||
NormalizePathString(dependencyPath),
|
||||
seenDependencies,
|
||||
outDependencies);
|
||||
}
|
||||
|
||||
for (const UIDocumentNode& child : node.children) {
|
||||
if (!CollectUIDocumentDependencies(
|
||||
sourcePath,
|
||||
child,
|
||||
outDependencies,
|
||||
inOutDiagnostics,
|
||||
outErrorMessage,
|
||||
seenDependencies)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct SourceFileReadResult {
|
||||
bool succeeded = false;
|
||||
fs::path resolvedPath;
|
||||
std::string content;
|
||||
Containers::String errorMessage;
|
||||
};
|
||||
|
||||
bool ReadUIDocumentSourceFile(const Containers::String& path, SourceFileReadResult& outResult) {
|
||||
outResult = SourceFileReadResult();
|
||||
if (path.Empty()) {
|
||||
outResult.errorMessage = "UI document path is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto tryReadFile = [&](const fs::path& filePath) -> bool {
|
||||
std::ifstream input(filePath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
outResult.succeeded = static_cast<bool>(input) || input.eof();
|
||||
outResult.resolvedPath = filePath.lexically_normal();
|
||||
outResult.content = buffer.str();
|
||||
return outResult.succeeded;
|
||||
};
|
||||
|
||||
const fs::path requestedPath(path.CStr());
|
||||
if (tryReadFile(requestedPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!requestedPath.is_absolute()) {
|
||||
const Containers::String resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
if (!resourceRoot.Empty() &&
|
||||
tryReadFile(fs::path(resourceRoot.CStr()) / requestedPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
outResult.errorMessage = Containers::String("Unable to read UI document source file: ") + path;
|
||||
return false;
|
||||
}
|
||||
|
||||
class UIDocumentParser {
|
||||
public:
|
||||
UIDocumentParser(const std::string& source,
|
||||
const fs::path& resolvedPath,
|
||||
const Containers::String& expectedRootTag,
|
||||
UIDocumentKind kind)
|
||||
: m_source(source)
|
||||
, m_resolvedPath(resolvedPath)
|
||||
, m_expectedRootTag(expectedRootTag)
|
||||
, m_kind(kind) {
|
||||
}
|
||||
|
||||
bool Parse(UIDocumentCompileResult& outResult) {
|
||||
outResult = UIDocumentCompileResult();
|
||||
outResult.document.kind = m_kind;
|
||||
outResult.document.sourcePath = NormalizePathString(m_resolvedPath);
|
||||
|
||||
if (HasUtf8Bom()) {
|
||||
Advance();
|
||||
Advance();
|
||||
Advance();
|
||||
}
|
||||
|
||||
SkipWhitespaceAndTrivia();
|
||||
if (AtEnd()) {
|
||||
AddError(CurrentLocation(), "UI document is empty.");
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
if (Peek() != '<') {
|
||||
AddError(CurrentLocation(), "Expected '<' at the beginning of the UI document.");
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
UIDocumentNode rootNode;
|
||||
if (!ParseElement(rootNode)) {
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
SkipWhitespaceAndTrivia();
|
||||
if (!AtEnd()) {
|
||||
AddError(CurrentLocation(), "Unexpected trailing content after the root element.");
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
if (!m_expectedRootTag.Empty() &&
|
||||
rootNode.tagName != m_expectedRootTag) {
|
||||
AddError(
|
||||
rootNode.location,
|
||||
Containers::String("Expected root element <") +
|
||||
m_expectedRootTag +
|
||||
">, found <" +
|
||||
rootNode.tagName +
|
||||
">.");
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
outResult.document.rootNode = std::move(rootNode);
|
||||
if (const UIDocumentAttribute* nameAttribute = outResult.document.rootNode.FindAttribute("name");
|
||||
nameAttribute != nullptr && !nameAttribute->value.Empty()) {
|
||||
outResult.document.displayName = nameAttribute->value;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> seenDependencies;
|
||||
if (!CollectUIDocumentDependencies(
|
||||
m_resolvedPath,
|
||||
outResult.document.rootNode,
|
||||
outResult.document.dependencies,
|
||||
outResult.document.diagnostics,
|
||||
m_errorMessage,
|
||||
seenDependencies)) {
|
||||
return Finalize(outResult, false);
|
||||
}
|
||||
|
||||
outResult.document.valid = true;
|
||||
outResult.succeeded = true;
|
||||
return Finalize(outResult, true);
|
||||
}
|
||||
|
||||
private:
|
||||
bool ParseElement(UIDocumentNode& outNode) {
|
||||
const UIDocumentSourceLocation elementLocation = CurrentLocation();
|
||||
if (!Consume('<', "Expected '<' to start an element.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(elementLocation, "Unexpected end of file after '<'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Peek() == '/' || Peek() == '!' || Peek() == '?') {
|
||||
AddError(elementLocation, "Unexpected token while parsing an element.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Containers::String tagName;
|
||||
if (!ParseName(tagName, "Expected an element tag name.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outNode.location = elementLocation;
|
||||
outNode.tagName = tagName;
|
||||
|
||||
while (!AtEnd()) {
|
||||
SkipWhitespace();
|
||||
if (StartsWith("/>")) {
|
||||
Advance();
|
||||
Advance();
|
||||
outNode.selfClosing = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Peek() == '>') {
|
||||
Advance();
|
||||
break;
|
||||
}
|
||||
|
||||
UIDocumentAttribute attribute;
|
||||
if (!ParseAttribute(attribute)) {
|
||||
return false;
|
||||
}
|
||||
outNode.attributes.PushBack(std::move(attribute));
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(elementLocation, "Unexpected end of file before element start tag was closed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
SkipWhitespaceAndTrivia();
|
||||
if (AtEnd()) {
|
||||
AddError(
|
||||
elementLocation,
|
||||
Containers::String("Unexpected end of file while parsing <") +
|
||||
outNode.tagName +
|
||||
">.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StartsWith("</")) {
|
||||
Advance();
|
||||
Advance();
|
||||
|
||||
Containers::String closeName;
|
||||
if (!ParseName(closeName, "Expected a closing element tag name.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SkipWhitespace();
|
||||
if (!Consume('>', "Expected '>' to finish the closing tag.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (closeName != outNode.tagName) {
|
||||
AddError(
|
||||
CurrentLocation(),
|
||||
Containers::String("Closing tag </") +
|
||||
closeName +
|
||||
"> does not match <" +
|
||||
outNode.tagName +
|
||||
">.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Peek() != '<') {
|
||||
AddError(CurrentLocation(), "Text nodes are not supported in XCUI documents.");
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentNode childNode;
|
||||
if (!ParseElement(childNode)) {
|
||||
return false;
|
||||
}
|
||||
outNode.children.PushBack(std::move(childNode));
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseAttribute(UIDocumentAttribute& outAttribute) {
|
||||
if (!ParseName(outAttribute.name, "Expected an attribute name.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SkipWhitespace();
|
||||
if (!Consume('=', "Expected '=' after the attribute name.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SkipWhitespace();
|
||||
return ParseQuotedString(outAttribute.value);
|
||||
}
|
||||
|
||||
bool ParseQuotedString(Containers::String& outValue) {
|
||||
if (AtEnd()) {
|
||||
AddError(CurrentLocation(), "Expected a quoted attribute value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const char quote = Peek();
|
||||
if (quote != '"' && quote != '\'') {
|
||||
AddError(CurrentLocation(), "Attribute values must use single or double quotes.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
std::string value;
|
||||
while (!AtEnd() && Peek() != quote) {
|
||||
value.push_back(Peek());
|
||||
Advance();
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(CurrentLocation(), "Unterminated attribute value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
outValue = ToContainersString(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseName(Containers::String& outName, const char* errorMessage) {
|
||||
if (AtEnd() || !IsNameStartChar(Peek())) {
|
||||
AddError(CurrentLocation(), errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
name.push_back(Peek());
|
||||
Advance();
|
||||
while (!AtEnd() && IsNameChar(Peek())) {
|
||||
name.push_back(Peek());
|
||||
Advance();
|
||||
}
|
||||
|
||||
outName = ToContainersString(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkipWhitespaceAndTrivia() {
|
||||
while (!AtEnd()) {
|
||||
SkipWhitespace();
|
||||
if (StartsWith("<!--")) {
|
||||
if (!SkipComment()) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (StartsWith("<?")) {
|
||||
if (!SkipProcessingInstruction()) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SkipWhitespace() {
|
||||
while (!AtEnd() &&
|
||||
std::isspace(static_cast<unsigned char>(Peek())) != 0) {
|
||||
Advance();
|
||||
}
|
||||
}
|
||||
|
||||
bool SkipComment() {
|
||||
const UIDocumentSourceLocation location = CurrentLocation();
|
||||
Advance();
|
||||
Advance();
|
||||
Advance();
|
||||
Advance();
|
||||
|
||||
while (!AtEnd() && !StartsWith("-->")) {
|
||||
Advance();
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(location, "Unterminated XML comment.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
Advance();
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SkipProcessingInstruction() {
|
||||
const UIDocumentSourceLocation location = CurrentLocation();
|
||||
Advance();
|
||||
Advance();
|
||||
|
||||
while (!AtEnd() && !StartsWith("?>")) {
|
||||
Advance();
|
||||
}
|
||||
|
||||
if (AtEnd()) {
|
||||
AddError(location, "Unterminated processing instruction.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Consume(char expected, const char* errorMessage) {
|
||||
if (AtEnd() || Peek() != expected) {
|
||||
AddError(CurrentLocation(), errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AddError(const UIDocumentSourceLocation& location, const Containers::String& message) {
|
||||
UIDocumentDiagnostic diagnostic;
|
||||
diagnostic.severity = UIDocumentDiagnosticSeverity::Error;
|
||||
diagnostic.location = location;
|
||||
diagnostic.message = message;
|
||||
m_diagnostics.PushBack(diagnostic);
|
||||
|
||||
if (m_errorMessage.Empty()) {
|
||||
m_errorMessage = FormatDiagnosticMessage(NormalizePathString(m_resolvedPath), location, message);
|
||||
}
|
||||
}
|
||||
|
||||
bool Finalize(UIDocumentCompileResult& outResult, bool succeeded) {
|
||||
Containers::Array<UIDocumentDiagnostic> combinedDiagnostics;
|
||||
combinedDiagnostics.Reserve(
|
||||
m_diagnostics.Size() +
|
||||
outResult.document.diagnostics.Size());
|
||||
for (const UIDocumentDiagnostic& diagnostic : m_diagnostics) {
|
||||
combinedDiagnostics.PushBack(diagnostic);
|
||||
}
|
||||
for (const UIDocumentDiagnostic& diagnostic : outResult.document.diagnostics) {
|
||||
combinedDiagnostics.PushBack(diagnostic);
|
||||
}
|
||||
outResult.document.diagnostics = std::move(combinedDiagnostics);
|
||||
outResult.succeeded = succeeded;
|
||||
outResult.errorMessage = succeeded ? Containers::String() : m_errorMessage;
|
||||
if (!succeeded) {
|
||||
outResult.document.valid = false;
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
bool HasUtf8Bom() const {
|
||||
return m_source.size() >= 3 &&
|
||||
static_cast<unsigned char>(m_source[0]) == 0xEF &&
|
||||
static_cast<unsigned char>(m_source[1]) == 0xBB &&
|
||||
static_cast<unsigned char>(m_source[2]) == 0xBF;
|
||||
}
|
||||
|
||||
bool StartsWith(const char* text) const {
|
||||
const size_t length = std::strlen(text);
|
||||
return m_offset + length <= m_source.size() &&
|
||||
m_source.compare(m_offset, length, text) == 0;
|
||||
}
|
||||
|
||||
bool AtEnd() const {
|
||||
return m_offset >= m_source.size();
|
||||
}
|
||||
|
||||
char Peek() const {
|
||||
return AtEnd() ? '\0' : m_source[m_offset];
|
||||
}
|
||||
|
||||
void Advance() {
|
||||
if (AtEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_source[m_offset] == '\n') {
|
||||
++m_line;
|
||||
m_column = 1;
|
||||
} else {
|
||||
++m_column;
|
||||
}
|
||||
++m_offset;
|
||||
}
|
||||
|
||||
UIDocumentSourceLocation CurrentLocation() const {
|
||||
UIDocumentSourceLocation location;
|
||||
location.line = m_line;
|
||||
location.column = m_column;
|
||||
return location;
|
||||
}
|
||||
|
||||
const std::string& m_source;
|
||||
fs::path m_resolvedPath;
|
||||
Containers::String m_expectedRootTag;
|
||||
UIDocumentKind m_kind = UIDocumentKind::View;
|
||||
size_t m_offset = 0;
|
||||
Core::uint32 m_line = 1;
|
||||
Core::uint32 m_column = 1;
|
||||
Containers::Array<UIDocumentDiagnostic> m_diagnostics;
|
||||
Containers::String m_errorMessage;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CompileUIDocument(const UIDocumentCompileRequest& request,
|
||||
UIDocumentCompileResult& outResult) {
|
||||
outResult = UIDocumentCompileResult();
|
||||
|
||||
SourceFileReadResult readResult;
|
||||
if (!ReadUIDocumentSourceFile(request.path, readResult)) {
|
||||
outResult.errorMessage = readResult.errorMessage;
|
||||
outResult.succeeded = false;
|
||||
outResult.document.kind = request.kind;
|
||||
return false;
|
||||
}
|
||||
|
||||
const Containers::String expectedRootTag =
|
||||
request.expectedRootTag.Empty()
|
||||
? GetUIDocumentDefaultRootTag(request.kind)
|
||||
: request.expectedRootTag;
|
||||
|
||||
UIDocumentParser parser(
|
||||
readResult.content,
|
||||
readResult.resolvedPath,
|
||||
expectedRootTag,
|
||||
request.kind);
|
||||
return parser.Parse(outResult);
|
||||
}
|
||||
|
||||
bool WriteUIDocumentArtifact(const Containers::String& artifactPath,
|
||||
const UIDocumentCompileResult& compileResult,
|
||||
Containers::String* outErrorMessage) {
|
||||
if (!compileResult.succeeded || !compileResult.document.valid) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = "UI document compile result is invalid.";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream output(artifactPath.CStr(), std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Unable to write UI document artifact: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentArtifactFileHeader header;
|
||||
header.kind = static_cast<Core::uint32>(compileResult.document.kind);
|
||||
header.dependencyCount = static_cast<Core::uint32>(compileResult.document.dependencies.Size());
|
||||
header.diagnosticCount = static_cast<Core::uint32>(compileResult.document.diagnostics.Size());
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
if (!output) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write UI document artifact header: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteString(output, compileResult.document.sourcePath);
|
||||
WriteString(output, compileResult.document.displayName);
|
||||
if (!WriteNode(output, compileResult.document.rootNode)) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write UI document node tree: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const Containers::String& dependency : compileResult.document.dependencies) {
|
||||
WriteString(output, dependency);
|
||||
if (!output) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write UI document dependencies: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const UIDocumentDiagnostic& diagnostic : compileResult.document.diagnostics) {
|
||||
UIDocumentArtifactDiagnosticHeader diagnosticHeader;
|
||||
diagnosticHeader.severity = static_cast<Core::uint32>(diagnostic.severity);
|
||||
diagnosticHeader.line = diagnostic.location.line;
|
||||
diagnosticHeader.column = diagnostic.location.column;
|
||||
output.write(reinterpret_cast<const char*>(&diagnosticHeader), sizeof(diagnosticHeader));
|
||||
WriteString(output, diagnostic.message);
|
||||
if (!output) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write UI document diagnostics: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadUIDocumentArtifact(const Containers::String& artifactPath,
|
||||
UIDocumentKind expectedKind,
|
||||
UIDocumentCompileResult& outResult) {
|
||||
outResult = UIDocumentCompileResult();
|
||||
|
||||
std::ifstream input(artifactPath.CStr(), std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
outResult.errorMessage = Containers::String("Unable to open UI document artifact: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentArtifactFileHeader header;
|
||||
input.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!input) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact header: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIDocumentArtifactFileHeader expectedHeader;
|
||||
if (std::memcmp(header.magic, expectedHeader.magic, sizeof(header.magic)) != 0) {
|
||||
outResult.errorMessage = Containers::String("Invalid UI document artifact magic: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header.schemaVersion != kUIDocumentArtifactSchemaVersion) {
|
||||
outResult.errorMessage = Containers::String("Unsupported UI document artifact schema version: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header.kind != static_cast<Core::uint32>(expectedKind)) {
|
||||
outResult.errorMessage = Containers::String("UI document artifact kind mismatch: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
outResult.document.kind = expectedKind;
|
||||
if (!ReadString(input, outResult.document.sourcePath) ||
|
||||
!ReadString(input, outResult.document.displayName) ||
|
||||
!ReadNode(input, outResult.document.rootNode)) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact body: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
outResult.document.dependencies.Clear();
|
||||
outResult.document.dependencies.Reserve(header.dependencyCount);
|
||||
for (Core::uint32 index = 0; index < header.dependencyCount; ++index) {
|
||||
Containers::String dependency;
|
||||
if (!ReadString(input, dependency)) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact dependencies: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
outResult.document.dependencies.PushBack(std::move(dependency));
|
||||
}
|
||||
|
||||
outResult.document.diagnostics.Clear();
|
||||
outResult.document.diagnostics.Reserve(header.diagnosticCount);
|
||||
for (Core::uint32 index = 0; index < header.diagnosticCount; ++index) {
|
||||
UIDocumentArtifactDiagnosticHeader diagnosticHeader;
|
||||
input.read(reinterpret_cast<char*>(&diagnosticHeader), sizeof(diagnosticHeader));
|
||||
if (!input) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact diagnostic header: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
UIDocumentDiagnostic diagnostic;
|
||||
diagnostic.severity = static_cast<UIDocumentDiagnosticSeverity>(diagnosticHeader.severity);
|
||||
diagnostic.location.line = diagnosticHeader.line;
|
||||
diagnostic.location.column = diagnosticHeader.column;
|
||||
if (!ReadString(input, diagnostic.message)) {
|
||||
outResult.errorMessage = Containers::String("Failed to read UI document artifact diagnostic message: ") + artifactPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
outResult.document.diagnostics.PushBack(std::move(diagnostic));
|
||||
}
|
||||
|
||||
outResult.document.valid = true;
|
||||
outResult.succeeded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
Containers::String GetUIDocumentDefaultRootTag(UIDocumentKind kind) {
|
||||
switch (kind) {
|
||||
case UIDocumentKind::View:
|
||||
return "View";
|
||||
case UIDocumentKind::Theme:
|
||||
return "Theme";
|
||||
case UIDocumentKind::Schema:
|
||||
return "Schema";
|
||||
default:
|
||||
return "View";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
61
engine/src/Resources/UI/UIDocuments.cpp
Normal file
61
engine/src/Resources/UI/UIDocuments.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#include <XCEngine/Resources/UI/UIDocuments.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
size_t MeasureNodeMemorySize(const UIDocumentNode& node) {
|
||||
size_t size = sizeof(UIDocumentNode) + node.tagName.Length();
|
||||
for (const UIDocumentAttribute& attribute : node.attributes) {
|
||||
size += sizeof(UIDocumentAttribute);
|
||||
size += attribute.name.Length();
|
||||
size += attribute.value.Length();
|
||||
}
|
||||
|
||||
for (const UIDocumentNode& child : node.children) {
|
||||
size += MeasureNodeMemorySize(child);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t MeasureDiagnosticMemorySize(const UIDocumentDiagnostic& diagnostic) {
|
||||
return sizeof(UIDocumentDiagnostic) + diagnostic.message.Length();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void UIDocumentResource::Release() {
|
||||
m_document.Clear();
|
||||
SetInvalid();
|
||||
m_memorySize = 0;
|
||||
}
|
||||
|
||||
void UIDocumentResource::SetDocumentModel(const UIDocumentModel& document) {
|
||||
m_document = document;
|
||||
RecalculateMemorySize();
|
||||
}
|
||||
|
||||
void UIDocumentResource::SetDocumentModel(UIDocumentModel&& document) {
|
||||
m_document = std::move(document);
|
||||
RecalculateMemorySize();
|
||||
}
|
||||
|
||||
void UIDocumentResource::RecalculateMemorySize() {
|
||||
size_t size = sizeof(*this);
|
||||
size += m_document.sourcePath.Length();
|
||||
size += m_document.displayName.Length();
|
||||
size += MeasureNodeMemorySize(m_document.rootNode);
|
||||
for (const Containers::String& dependency : m_document.dependencies) {
|
||||
size += sizeof(Containers::String) + dependency.Length();
|
||||
}
|
||||
for (const UIDocumentDiagnostic& diagnostic : m_document.diagnostics) {
|
||||
size += MeasureDiagnosticMemorySize(diagnostic);
|
||||
}
|
||||
|
||||
m_memorySize = size;
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
Reference in New Issue
Block a user