Stabilize XCUI schema compiler and phase 2 checkpoint

This commit is contained in:
2026-04-05 05:16:40 +08:00
parent ade5be31d6
commit acdbcdd35b
2 changed files with 831 additions and 14 deletions

View File

@@ -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.

View File

@@ -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<Core::uint32>(attribute.valueType);
header.documentKind = static_cast<Core::uint32>(attribute.documentKind);
header.allowedValueCount = static_cast<Core::uint32>(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<const char*>(&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<char*>(&header), sizeof(header));
if (!stream) {
return false;
}
outAttribute.valueType = static_cast<UISchemaValueType>(header.valueType);
outAttribute.documentKind = static_cast<UIDocumentKind>(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<Core::uint32>(element.attributes.Size());
header.childCount = static_cast<Core::uint32>(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<const char*>(&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<char*>(&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<Core::uint32>(schemaDefinition.elements.Size());
header.valid = schemaDefinition.valid ? 1u : 0u;
stream.write(reinterpret_cast<const char*>(&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<char*>(&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<unsigned char>(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<UIDocumentDiagnostic>& diagnostics) {
for (const UIDocumentDiagnostic& diagnostic : diagnostics) {
if (diagnostic.severity == UIDocumentDiagnosticSeverity::Error) {
return true;
}
}
return false;
}
void AppendDiagnostic(
const Containers::String& sourcePath,
Containers::Array<UIDocumentDiagnostic>& 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<Containers::String> ParseAllowedValueList(const Containers::String& value) {
Containers::Array<Containers::String> 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<UIDocumentDiagnostic>& diagnostics,
Containers::String& inOutErrorMessage) {
bool valid = true;
std::unordered_set<std::string> 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<const char*> allowedAttributeNames,
Containers::Array<UIDocumentDiagnostic>& 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<UIDocumentDiagnostic>& diagnostics,
Containers::String& inOutErrorMessage) {
bool valid = true;
if (!node.children.Empty()) {
AppendDiagnostic(
sourcePath,
diagnostics,
inOutErrorMessage,
UIDocumentDiagnosticSeverity::Error,
node.location,
"Schema <Attribute> 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<UIDocumentDiagnostic>& 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<std::string> seenAttributeDefinitions;
std::unordered_set<std::string> 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 <Element>.");
valid = false;
}
return valid;
}
bool BuildSchemaDefinition(
const Containers::String& sourcePath,
const UIDocumentNode& rootNode,
const Containers::String& defaultName,
UISchemaDefinition& outSchemaDefinition,
Containers::Array<UIDocumentDiagnostic>& diagnostics,
Containers::String& inOutErrorMessage) {
outSchemaDefinition.Clear();
if (rootNode.tagName != "Schema") {
AppendDiagnostic(
sourcePath,
diagnostics,
inOutErrorMessage,
UIDocumentDiagnosticSeverity::Error,
rootNode.location,
"UI schema root element must be <Schema>.");
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<std::string> 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 <Schema>.");
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<std::string>& seenDependencies,
Containers::Array<Containers::String>& 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<std::string> 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;