diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 420c0a5f..65a35886 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -9,16 +9,18 @@ Old `editor` replacement is explicitly out of scope for this phase. - Phase 1 sandbox batch committed and pushed as `67a28bd` (`Add XCUI new editor sandbox phase 1`). - Current work has moved into Phase 2: filling the three-layer XCUI structure instead of replacing the old editor. +- Phase 2 common/runtime batch is now buildable locally again after repairing the duplicated schema helper collision inside `UIDocumentCompiler.cpp`. ## Three-Layer Status ### 1. Common Core - `UI::DrawData`, input event types, focus routing, style/theme resolution are in active use. -- `UIDocumentCompiler` was restored to a stable buildable baseline after a broken parallel schema attempt corrupted the file. +- `UIDocumentCompiler` is buildable again after repairing the duplicated schema helper regression introduced by overlapping schema work. - Build-system hardening for MSVC/PDB output paths has started in root CMake, `engine/CMakeLists.txt`, `new_editor/CMakeLists.txt`, and `tests/NewEditor/CMakeLists.txt`. - Shared engine-side XCUI runtime scaffolding is now present under `engine/include/XCEngine/UI/Runtime` and `engine/src/UI/Runtime`. -- Core regression coverage now includes `UIContext`, layout, style, and runtime screen-player tests through `core_ui_tests`. +- Shared engine-side `UIDocumentScreenHost` now compiles `.xcui` / `.xctheme` screen documents into a runtime-facing document host path instead of leaving all document ownership in `new_editor`. +- Core regression coverage now includes `UIContext`, layout, style, runtime screen player/system, and real document-host tests through `core_ui_tests`. Current gap: @@ -28,21 +30,22 @@ Current gap: ### 2. Runtime/Game Layer -- Runtime-side XCUI is still shallow. - The main concrete progress here is that the retained-mode demo runtime now supports a real `TextField` input path with UTF-8 text entry and backspace handling. -- This proves that the runtime-facing layer is no longer limited to static cards/buttons. -- Engine-side runtime ownership is no longer zero: `UIScreenPlayer` and `UISystem` now define a minimal shared runtime contract for loading a screen document, ticking it with input, and collecting `UI::UIDrawData`. +- The demo runtime has moved past single-line input: multiline `TextArea` behavior is now covered in the sandbox testbed. +- Engine-side runtime ownership is no longer zero: `UIScreenPlayer`, `UIDocumentScreenHost`, and `UISystem` now define a shared runtime contract for loading a screen document, ticking it with input, and collecting `UI::UIDrawData`. +- `UISystem` now supports layered screen composition semantics: stacked screen players, top-interactive input routing, and modal layers that block lower screens. Current gap: -- No real game-facing screen host, menu stack, HUD stack, or shared runtime widget library yet. -- The new runtime layer still needs a real XCUI document host implementation instead of the current host-facing contract only. +- No production game-loop integration has been wired yet from scene/runtime systems into `UISystem`. +- The runtime widget library is still shallow and missing the editor-grade controls that will later be shared downward. ### 3. Editor Layer - `new_editor` remains the isolated XCUI sandbox. - Native hosted preview is working as `RHI offscreen surface -> ImGui shell texture embed`. - `XCUI Demo` remains the long-lived effect and behavior testbed. +- `XCUI Demo` now covers both single-line and multiline text authoring behavior. - `LayoutLab` now includes a `ScrollView` prototype and a more editor-like three-column authored layout. - Panel diagnostics were expanded to clearly separate preview/runtime/input state and native vs legacy paths. - `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`. @@ -54,18 +57,24 @@ Current gap: ## Validated This Phase -- `new_editor_xcui_demo_runtime_tests`: `6/6` +- `new_editor_xcui_demo_runtime_tests`: `7/7` - `new_editor_xcui_layout_lab_runtime_tests`: `5/5` - `new_editor_xcui_rhi_command_compiler_tests`: `6/6` - `new_editor_xcui_rhi_render_backend_tests`: `5/5` - `XCNewEditor` Debug target builds successfully -- `core_ui_tests`: `20/20` +- `core_ui_tests`: `24/24` ## Landed This Phase - Demo runtime `TextField` with UTF-8 text insertion, caret state, and backspace. +- Demo runtime multiline `TextArea` path in the sandbox and test coverage for caret movement / multiline input. - Demo authored resources updated to exercise the input field. - LayoutLab `ScrollView` prototype with clipping and hover rejection outside clipped content. +- Engine runtime layer added: + - `UIScreenPlayer` + - `UIDocumentScreenHost` + - `UISystem` + - layered screen composition and modal blocking semantics - RHI image path improvements: - clipped image UV adjustment - mirrored image UV preservation @@ -73,18 +82,19 @@ Current gap: - per-batch scissor application - `new_editor` panel/shell diagnostics improvements for hosted preview state. - XCUI asset document loading changed to prefer direct source compilation before `ResourceManager` fallback for the sandbox path, fixing the LayoutLab crash. +- `UIDocumentCompiler.cpp` repaired enough to restore full local builds after the duplicated schema-helper regression. ## Phase Risks Still Open -- Schema/validation work must be restarted cleanly after the corrupted parallel attempt. +- Schema/validation still needs a clean minimal landing instead of the current partially merged state in `UIDocumentCompiler.cpp`. - `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet. - `Image` widgets still do not have source-rect/atlas-subregion level API in the high-level draw command model. - Editor shell still depends on ImGui as host chrome. ## Next Phase -1. Re-open common-layer schema/validation on a clean baseline and land the smallest stable version. -2. Expand runtime/game-layer ownership from `UIScreenPlayer`/`UISystem` into a real XCUI document host plus menu/HUD stack patterns. -3. Add next editor-facing widgets: `TextArea`, list/tree, property-style sections. +1. Cleanly stabilize schema/validation in `UIDocumentCompiler.cpp` and add targeted schema regression tests. +2. Expand runtime/game-layer ownership from the current document host + layered `UISystem` into reusable menu/HUD stack patterns and engine runtime integration. +3. Add next editor-facing widgets: tree/list, property-style sections, toolbar/menu, and more native shell-owned chrome. 4. Move more diagnostics and shell affordances into XCUI-owned editor-layer surfaces instead of only ImGui HUDs. 5. Continue phased validation, commit, push, and plan refresh after each stable batch. diff --git a/engine/src/Resources/UI/UIDocumentCompiler.cpp b/engine/src/Resources/UI/UIDocumentCompiler.cpp index 388e5309..51acf154 100644 --- a/engine/src/Resources/UI/UIDocumentCompiler.cpp +++ b/engine/src/Resources/UI/UIDocumentCompiler.cpp @@ -24,6 +24,9 @@ Containers::String ToContainersString(const std::string& value) { } std::string ToStdString(const Containers::String& value) { + if (value.Empty() || value.CStr() == nullptr) { + return std::string(); + } return std::string(value.CStr()); } @@ -151,6 +154,199 @@ bool ReadNode(std::ifstream& stream, UIDocumentNode& outNode) { return true; } +struct UIDocumentArtifactSchemaDefinitionHeader { + Core::uint32 elementCount = 0; + Core::uint32 valid = 0; +}; + +struct UIDocumentArtifactSchemaElementHeader { + Core::uint32 attributeCount = 0; + Core::uint32 childCount = 0; + Core::uint32 line = 1; + Core::uint32 column = 1; + Core::uint32 allowUnknownAttributes = 0; + Core::uint32 allowUnknownChildren = 0; +}; + +struct UIDocumentArtifactSchemaAttributeHeader { + Core::uint32 valueType = 0; + Core::uint32 documentKind = 0; + Core::uint32 allowedValueCount = 0; + Core::uint32 line = 1; + Core::uint32 column = 1; + Core::uint32 required = 0; + Core::uint32 restrictDocumentKind = 0; +}; + +bool WriteSchemaAttribute(std::ofstream& stream, const UISchemaAttributeDefinition& attribute) { + WriteString(stream, attribute.name); + + UIDocumentArtifactSchemaAttributeHeader header; + header.valueType = static_cast(attribute.valueType); + header.documentKind = static_cast(attribute.documentKind); + header.allowedValueCount = static_cast(attribute.allowedValues.Size()); + header.line = attribute.location.line; + header.column = attribute.location.column; + header.required = attribute.required ? 1u : 0u; + header.restrictDocumentKind = attribute.restrictDocumentKind ? 1u : 0u; + stream.write(reinterpret_cast(&header), sizeof(header)); + if (!stream) { + return false; + } + + for (const Containers::String& value : attribute.allowedValues) { + WriteString(stream, value); + if (!stream) { + return false; + } + } + + return true; +} + +bool ReadSchemaAttribute(std::ifstream& stream, UISchemaAttributeDefinition& outAttribute) { + if (!ReadString(stream, outAttribute.name)) { + return false; + } + + UIDocumentArtifactSchemaAttributeHeader header; + stream.read(reinterpret_cast(&header), sizeof(header)); + if (!stream) { + return false; + } + + outAttribute.valueType = static_cast(header.valueType); + outAttribute.documentKind = static_cast(header.documentKind); + outAttribute.location.line = header.line; + outAttribute.location.column = header.column; + outAttribute.required = header.required != 0; + outAttribute.restrictDocumentKind = header.restrictDocumentKind != 0; + outAttribute.allowedValues.Clear(); + outAttribute.allowedValues.Reserve(header.allowedValueCount); + for (Core::uint32 index = 0; index < header.allowedValueCount; ++index) { + Containers::String value; + if (!ReadString(stream, value)) { + return false; + } + outAttribute.allowedValues.PushBack(std::move(value)); + } + + return true; +} + +bool WriteSchemaElement(std::ofstream& stream, const UISchemaElementDefinition& element) { + WriteString(stream, element.tagName); + + UIDocumentArtifactSchemaElementHeader header; + header.attributeCount = static_cast(element.attributes.Size()); + header.childCount = static_cast(element.children.Size()); + header.line = element.location.line; + header.column = element.location.column; + header.allowUnknownAttributes = element.allowUnknownAttributes ? 1u : 0u; + header.allowUnknownChildren = element.allowUnknownChildren ? 1u : 0u; + stream.write(reinterpret_cast(&header), sizeof(header)); + if (!stream) { + return false; + } + + for (const UISchemaAttributeDefinition& attribute : element.attributes) { + if (!WriteSchemaAttribute(stream, attribute)) { + return false; + } + } + + for (const UISchemaElementDefinition& child : element.children) { + if (!WriteSchemaElement(stream, child)) { + return false; + } + } + + return true; +} + +bool ReadSchemaElement(std::ifstream& stream, UISchemaElementDefinition& outElement) { + if (!ReadString(stream, outElement.tagName)) { + return false; + } + + UIDocumentArtifactSchemaElementHeader header; + stream.read(reinterpret_cast(&header), sizeof(header)); + if (!stream) { + return false; + } + + outElement.location.line = header.line; + outElement.location.column = header.column; + outElement.allowUnknownAttributes = header.allowUnknownAttributes != 0; + outElement.allowUnknownChildren = header.allowUnknownChildren != 0; + outElement.attributes.Clear(); + outElement.children.Clear(); + outElement.attributes.Reserve(header.attributeCount); + outElement.children.Reserve(header.childCount); + for (Core::uint32 index = 0; index < header.attributeCount; ++index) { + UISchemaAttributeDefinition attribute; + if (!ReadSchemaAttribute(stream, attribute)) { + return false; + } + outElement.attributes.PushBack(std::move(attribute)); + } + + for (Core::uint32 index = 0; index < header.childCount; ++index) { + UISchemaElementDefinition child; + if (!ReadSchemaElement(stream, child)) { + return false; + } + outElement.children.PushBack(std::move(child)); + } + + return true; +} + +bool WriteSchemaDefinition(std::ofstream& stream, const UISchemaDefinition& schemaDefinition) { + WriteString(stream, schemaDefinition.name); + + UIDocumentArtifactSchemaDefinitionHeader header; + header.elementCount = static_cast(schemaDefinition.elements.Size()); + header.valid = schemaDefinition.valid ? 1u : 0u; + stream.write(reinterpret_cast(&header), sizeof(header)); + if (!stream) { + return false; + } + + for (const UISchemaElementDefinition& element : schemaDefinition.elements) { + if (!WriteSchemaElement(stream, element)) { + return false; + } + } + + return true; +} + +bool ReadSchemaDefinition(std::ifstream& stream, UISchemaDefinition& outSchemaDefinition) { + if (!ReadString(stream, outSchemaDefinition.name)) { + return false; + } + + UIDocumentArtifactSchemaDefinitionHeader header; + stream.read(reinterpret_cast(&header), sizeof(header)); + if (!stream) { + return false; + } + + outSchemaDefinition.valid = header.valid != 0; + outSchemaDefinition.elements.Clear(); + outSchemaDefinition.elements.Reserve(header.elementCount); + for (Core::uint32 index = 0; index < header.elementCount; ++index) { + UISchemaElementDefinition element; + if (!ReadSchemaElement(stream, element)) { + return false; + } + outSchemaDefinition.elements.PushBack(std::move(element)); + } + + return true; +} + bool IsNameStartChar(char ch) { return std::isalpha(static_cast(ch)) != 0 || ch == '_' || @@ -173,6 +369,560 @@ bool LooksLikeUIDocumentReference(const Containers::String& value) { trimmed.size() >= 9 && trimmed.rfind(".xcschema") == trimmed.size() - 9); } +bool ContainsErrorDiagnostics(const Containers::Array& diagnostics) { + for (const UIDocumentDiagnostic& diagnostic : diagnostics) { + if (diagnostic.severity == UIDocumentDiagnosticSeverity::Error) { + return true; + } + } + + return false; +} + +void AppendDiagnostic( + const Containers::String& sourcePath, + Containers::Array& diagnostics, + Containers::String& inOutErrorMessage, + UIDocumentDiagnosticSeverity severity, + const UIDocumentSourceLocation& location, + const Containers::String& message) { + UIDocumentDiagnostic diagnostic; + diagnostic.severity = severity; + diagnostic.location = location; + diagnostic.message = message; + diagnostics.PushBack(std::move(diagnostic)); + + if (severity == UIDocumentDiagnosticSeverity::Error && inOutErrorMessage.Empty()) { + inOutErrorMessage = FormatDiagnosticMessage(sourcePath, location, message); + } +} + +bool TryParseBooleanValueExtended(const Containers::String& value, bool& outValue) { + const std::string normalized = ToLowerCopy(ToStdString(value.Trim())); + if (normalized == "true" || normalized == "1" || normalized == "yes") { + outValue = true; + return true; + } + if (normalized == "false" || normalized == "0" || normalized == "no") { + outValue = false; + return true; + } + return false; +} + +bool TryParseSchemaValueTypeExtended(const Containers::String& value, UISchemaValueType& outValueType) { + const std::string normalized = ToLowerCopy(ToStdString(value.Trim())); + if (normalized == "string") { + outValueType = UISchemaValueType::String; + return true; + } + if (normalized == "boolean" || normalized == "bool") { + outValueType = UISchemaValueType::Boolean; + return true; + } + if (normalized == "integer" || normalized == "int") { + outValueType = UISchemaValueType::Integer; + return true; + } + if (normalized == "number" || normalized == "float") { + outValueType = UISchemaValueType::Number; + return true; + } + if (normalized == "document") { + outValueType = UISchemaValueType::Document; + return true; + } + if (normalized == "enum") { + outValueType = UISchemaValueType::Enum; + return true; + } + return false; +} + +bool TryParseDocumentKindValueExtended(const Containers::String& value, UIDocumentKind& outKind) { + const std::string normalized = ToLowerCopy(ToStdString(value.Trim())); + if (normalized == "view" || normalized == "xcui") { + outKind = UIDocumentKind::View; + return true; + } + if (normalized == "theme" || normalized == "xctheme") { + outKind = UIDocumentKind::Theme; + return true; + } + if (normalized == "schema" || normalized == "xcschema") { + outKind = UIDocumentKind::Schema; + return true; + } + return false; +} + +Containers::Array ParseAllowedValueList(const Containers::String& value) { + Containers::Array values; + std::string token; + const std::string rawValue = ToStdString(value); + + auto flushToken = [&]() { + Containers::String trimmed = ToContainersString(token).Trim(); + if (!trimmed.Empty()) { + values.PushBack(std::move(trimmed)); + } + token.clear(); + }; + + for (char ch : rawValue) { + if (ch == ',' || ch == '|' || ch == ';') { + flushToken(); + continue; + } + token.push_back(ch); + } + flushToken(); + return values; +} + +bool ValidateNodeTree( + const Containers::String& sourcePath, + const UIDocumentNode& node, + Containers::Array& diagnostics, + Containers::String& inOutErrorMessage) { + bool valid = true; + std::unordered_set seenAttributeNames; + + for (const UIDocumentAttribute& attribute : node.attributes) { + const std::string key = ToStdString(attribute.name); + if (!seenAttributeNames.insert(key).second) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Duplicate attribute '") + + attribute.name + + "' on <" + + node.tagName + + ">."); + valid = false; + } + } + + for (const UIDocumentNode& child : node.children) { + if (!ValidateNodeTree(sourcePath, child, diagnostics, inOutErrorMessage)) { + valid = false; + } + } + + return valid; +} + +const UIDocumentAttribute* FindAttributeByName( + const UIDocumentNode& node, + const char* name, + const char* alias = nullptr) { + if (const UIDocumentAttribute* attribute = node.FindAttribute(name)) { + return attribute; + } + if (alias != nullptr) { + return node.FindAttribute(alias); + } + return nullptr; +} + +bool ValidateAllowedAttributes( + const Containers::String& sourcePath, + const UIDocumentNode& node, + std::initializer_list allowedAttributeNames, + Containers::Array& diagnostics, + Containers::String& inOutErrorMessage) { + bool valid = true; + for (const UIDocumentAttribute& attribute : node.attributes) { + bool allowed = false; + for (const char* allowedName : allowedAttributeNames) { + if (attribute.name == allowedName) { + allowed = true; + break; + } + } + + if (!allowed) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Unsupported attribute '") + + attribute.name + + "' on schema node <" + + node.tagName + + ">."); + valid = false; + } + } + return valid; +} + +bool BuildSchemaAttributeDefinition( + const Containers::String& sourcePath, + const UIDocumentNode& node, + UISchemaAttributeDefinition& outDefinition, + Containers::Array& diagnostics, + Containers::String& inOutErrorMessage) { + bool valid = true; + if (!node.children.Empty()) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + "Schema nodes cannot contain child elements."); + valid = false; + } + + if (!ValidateAllowedAttributes( + sourcePath, + node, + {"name", "type", "required", "documentKind", "kind", "restrictDocumentKind", "allowedValues", "values"}, + diagnostics, + inOutErrorMessage)) { + valid = false; + } + + outDefinition.location = node.location; + + const UIDocumentAttribute* nameAttribute = FindAttributeByName(node, "name"); + if (nameAttribute == nullptr || nameAttribute->value.Trim().Empty()) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + "Schema attribute definition is missing required 'name'."); + valid = false; + } else { + outDefinition.name = nameAttribute->value.Trim(); + } + + if (const UIDocumentAttribute* typeAttribute = FindAttributeByName(node, "type"); + typeAttribute != nullptr && !typeAttribute->value.Trim().Empty()) { + if (!TryParseSchemaValueTypeExtended(typeAttribute->value, outDefinition.valueType)) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Unsupported schema value type '") + + typeAttribute->value + + "'."); + valid = false; + } + } + + if (const UIDocumentAttribute* requiredAttribute = FindAttributeByName(node, "required"); + requiredAttribute != nullptr && !requiredAttribute->value.Trim().Empty()) { + if (!TryParseBooleanValueExtended(requiredAttribute->value, outDefinition.required)) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Invalid boolean value '") + + requiredAttribute->value + + "' for schema attribute 'required'."); + valid = false; + } + } + + const UIDocumentAttribute* documentKindAttribute = + FindAttributeByName(node, "documentKind", "kind"); + if (documentKindAttribute != nullptr && !documentKindAttribute->value.Trim().Empty()) { + if (!TryParseDocumentKindValueExtended(documentKindAttribute->value, outDefinition.documentKind)) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Unsupported document kind '") + + documentKindAttribute->value + + "' in schema attribute definition."); + valid = false; + } else if (outDefinition.valueType == UISchemaValueType::Document) { + outDefinition.restrictDocumentKind = true; + } + } + + if (const UIDocumentAttribute* restrictDocumentKindAttribute = FindAttributeByName(node, "restrictDocumentKind"); + restrictDocumentKindAttribute != nullptr && !restrictDocumentKindAttribute->value.Trim().Empty()) { + if (!TryParseBooleanValueExtended( + restrictDocumentKindAttribute->value, + outDefinition.restrictDocumentKind)) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Invalid boolean value '") + + restrictDocumentKindAttribute->value + + "' for schema attribute 'restrictDocumentKind'."); + valid = false; + } + } + + if (const UIDocumentAttribute* allowedValuesAttribute = FindAttributeByName(node, "allowedValues", "values"); + allowedValuesAttribute != nullptr) { + outDefinition.allowedValues = ParseAllowedValueList(allowedValuesAttribute->value); + } + + if (outDefinition.valueType == UISchemaValueType::Enum && outDefinition.allowedValues.Empty()) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Enum schema attribute '") + + outDefinition.name + + "' must declare 'allowedValues'."); + valid = false; + } + + return valid; +} + +bool BuildSchemaElementDefinition( + const Containers::String& sourcePath, + const UIDocumentNode& node, + UISchemaElementDefinition& outDefinition, + Containers::Array& diagnostics, + Containers::String& inOutErrorMessage) { + bool valid = ValidateAllowedAttributes( + sourcePath, + node, + {"tag", "name", "allowUnknownAttributes", "allowUnknownChildren"}, + diagnostics, + inOutErrorMessage); + + outDefinition.location = node.location; + + const UIDocumentAttribute* tagAttribute = FindAttributeByName(node, "tag", "name"); + if (tagAttribute == nullptr || tagAttribute->value.Trim().Empty()) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + "Schema element definition is missing required 'tag'."); + valid = false; + } else { + outDefinition.tagName = tagAttribute->value.Trim(); + } + + if (const UIDocumentAttribute* allowUnknownAttributes = FindAttributeByName(node, "allowUnknownAttributes"); + allowUnknownAttributes != nullptr && !allowUnknownAttributes->value.Trim().Empty()) { + if (!TryParseBooleanValueExtended( + allowUnknownAttributes->value, + outDefinition.allowUnknownAttributes)) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Invalid boolean value '") + + allowUnknownAttributes->value + + "' for schema element 'allowUnknownAttributes'."); + valid = false; + } + } + + if (const UIDocumentAttribute* allowUnknownChildren = FindAttributeByName(node, "allowUnknownChildren"); + allowUnknownChildren != nullptr && !allowUnknownChildren->value.Trim().Empty()) { + if (!TryParseBooleanValueExtended( + allowUnknownChildren->value, + outDefinition.allowUnknownChildren)) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + node.location, + Containers::String("Invalid boolean value '") + + allowUnknownChildren->value + + "' for schema element 'allowUnknownChildren'."); + valid = false; + } + } + + std::unordered_set seenAttributeDefinitions; + std::unordered_set seenChildElementDefinitions; + for (const UIDocumentNode& child : node.children) { + if (child.tagName == "Attribute") { + UISchemaAttributeDefinition attributeDefinition; + if (!BuildSchemaAttributeDefinition( + sourcePath, + child, + attributeDefinition, + diagnostics, + inOutErrorMessage)) { + valid = false; + } + + const std::string attributeKey = ToStdString(attributeDefinition.name); + if (!attributeDefinition.name.Empty() && + !seenAttributeDefinitions.insert(attributeKey).second) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + child.location, + Containers::String("Duplicate schema attribute definition '") + + attributeDefinition.name + + "' on <" + + outDefinition.tagName + + ">."); + valid = false; + } + + outDefinition.attributes.PushBack(std::move(attributeDefinition)); + continue; + } + + if (child.tagName == "Element") { + UISchemaElementDefinition childElementDefinition; + if (!BuildSchemaElementDefinition( + sourcePath, + child, + childElementDefinition, + diagnostics, + inOutErrorMessage)) { + valid = false; + } + + const std::string childKey = ToStdString(childElementDefinition.tagName); + if (!childElementDefinition.tagName.Empty() && + !seenChildElementDefinitions.insert(childKey).second) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + child.location, + Containers::String("Duplicate child schema element definition '") + + childElementDefinition.tagName + + "' on <" + + outDefinition.tagName + + ">."); + valid = false; + } + + outDefinition.children.PushBack(std::move(childElementDefinition)); + continue; + } + + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + child.location, + Containers::String("Unsupported schema child <") + + child.tagName + + "> inside ."); + valid = false; + } + + return valid; +} + +bool BuildSchemaDefinition( + const Containers::String& sourcePath, + const UIDocumentNode& rootNode, + const Containers::String& defaultName, + UISchemaDefinition& outSchemaDefinition, + Containers::Array& diagnostics, + Containers::String& inOutErrorMessage) { + outSchemaDefinition.Clear(); + if (rootNode.tagName != "Schema") { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + rootNode.location, + "UI schema root element must be ."); + return false; + } + + bool valid = ValidateAllowedAttributes( + sourcePath, + rootNode, + {"name"}, + diagnostics, + inOutErrorMessage); + + if (const UIDocumentAttribute* nameAttribute = FindAttributeByName(rootNode, "name"); + nameAttribute != nullptr && !nameAttribute->value.Trim().Empty()) { + outSchemaDefinition.name = nameAttribute->value.Trim(); + } else { + outSchemaDefinition.name = defaultName; + } + + std::unordered_set seenRootElements; + for (const UIDocumentNode& child : rootNode.children) { + if (child.tagName != "Element") { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + child.location, + Containers::String("Unsupported schema child <") + + child.tagName + + "> inside ."); + valid = false; + continue; + } + + UISchemaElementDefinition elementDefinition; + if (!BuildSchemaElementDefinition( + sourcePath, + child, + elementDefinition, + diagnostics, + inOutErrorMessage)) { + valid = false; + } + + const std::string elementKey = ToStdString(elementDefinition.tagName); + if (!elementDefinition.tagName.Empty() && + !seenRootElements.insert(elementKey).second) { + AppendDiagnostic( + sourcePath, + diagnostics, + inOutErrorMessage, + UIDocumentDiagnosticSeverity::Error, + child.location, + Containers::String("Duplicate root schema element definition '") + + elementDefinition.tagName + + "'."); + valid = false; + } + + outSchemaDefinition.elements.PushBack(std::move(elementDefinition)); + } + + outSchemaDefinition.valid = valid && !ContainsErrorDiagnostics(diagnostics); + return outSchemaDefinition.valid; +} + void AppendUniqueDependency(const Containers::String& dependencyPath, std::unordered_set& seenDependencies, Containers::Array& outDependencies) { @@ -345,6 +1095,18 @@ public: outResult.document.displayName = nameAttribute->value; } + if (m_kind == UIDocumentKind::Schema) { + if (!BuildSchemaDefinition( + NormalizePathString(m_resolvedPath), + outResult.document.rootNode, + outResult.document.displayName, + outResult.document.schemaDefinition, + outResult.document.diagnostics, + m_errorMessage)) { + return Finalize(outResult, false); + } + } + std::unordered_set seenDependencies; if (!CollectUIDocumentDependencies( m_resolvedPath, @@ -356,6 +1118,18 @@ public: return Finalize(outResult, false); } + if (!ValidateNodeTree( + outResult.document.sourcePath, + outResult.document.rootNode, + outResult.document.diagnostics, + m_errorMessage)) { + return Finalize(outResult, false); + } + + if (ContainsErrorDiagnostics(outResult.document.diagnostics)) { + return Finalize(outResult, false); + } + outResult.document.valid = true; outResult.succeeded = true; return Finalize(outResult, true); @@ -752,6 +1526,12 @@ bool WriteUIDocumentArtifact(const Containers::String& artifactPath, } return false; } + if (!WriteSchemaDefinition(output, compileResult.document.schemaDefinition)) { + if (outErrorMessage != nullptr) { + *outErrorMessage = Containers::String("Failed to write UI document schema definition: ") + artifactPath; + } + return false; + } for (const Containers::String& dependency : compileResult.document.dependencies) { WriteString(output, dependency); @@ -805,7 +1585,8 @@ bool LoadUIDocumentArtifact(const Containers::String& artifactPath, return false; } - if (header.schemaVersion != kUIDocumentArtifactSchemaVersion) { + if (header.schemaVersion != 1u && + header.schemaVersion != kUIDocumentArtifactSchemaVersion) { outResult.errorMessage = Containers::String("Unsupported UI document artifact schema version: ") + artifactPath; return false; } @@ -822,6 +1603,13 @@ bool LoadUIDocumentArtifact(const Containers::String& artifactPath, outResult.errorMessage = Containers::String("Failed to read UI document artifact body: ") + artifactPath; return false; } + outResult.document.schemaDefinition.Clear(); + if (header.schemaVersion >= 2u) { + if (!ReadSchemaDefinition(input, outResult.document.schemaDefinition)) { + outResult.errorMessage = Containers::String("Failed to read UI document artifact schema definition: ") + artifactPath; + return false; + } + } outResult.document.dependencies.Clear(); outResult.document.dependencies.Reserve(header.dependencyCount); @@ -856,6 +1644,25 @@ bool LoadUIDocumentArtifact(const Containers::String& artifactPath, outResult.document.diagnostics.PushBack(std::move(diagnostic)); } + if (expectedKind == UIDocumentKind::Schema) { + const Containers::String sourcePath = + outResult.document.sourcePath.Empty() + ? artifactPath + : outResult.document.sourcePath; + if (!outResult.document.schemaDefinition.valid && + !BuildSchemaDefinition( + sourcePath, + outResult.document.rootNode, + outResult.document.displayName, + outResult.document.schemaDefinition, + outResult.document.diagnostics, + outResult.errorMessage)) { + outResult.succeeded = false; + outResult.document.valid = false; + return false; + } + } + outResult.document.valid = true; outResult.succeeded = true; return true;