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`). - 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. - 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 ## Three-Layer Status
### 1. Common Core ### 1. Common Core
- `UI::DrawData`, input event types, focus routing, style/theme resolution are in active use. - `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`. - 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`. - 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: Current gap:
@@ -28,21 +30,22 @@ Current gap:
### 2. Runtime/Game Layer ### 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. - 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. - 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` and `UISystem` now define a minimal shared runtime contract for loading a screen document, ticking it with input, and collecting `UI::UIDrawData`. - 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: Current gap:
- No real game-facing screen host, menu stack, HUD stack, or shared runtime widget library yet. - No production game-loop integration has been wired yet from scene/runtime systems into `UISystem`.
- The new runtime layer still needs a real XCUI document host implementation instead of the current host-facing contract only. - The runtime widget library is still shallow and missing the editor-grade controls that will later be shared downward.
### 3. Editor Layer ### 3. Editor Layer
- `new_editor` remains the isolated XCUI sandbox. - `new_editor` remains the isolated XCUI sandbox.
- Native hosted preview is working as `RHI offscreen surface -> ImGui shell texture embed`. - 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` 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. - `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. - 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`. - `XCNewEditor` builds successfully to `build/new_editor/bin/Debug/XCNewEditor.exe`.
@@ -54,18 +57,24 @@ Current gap:
## Validated This Phase ## 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_layout_lab_runtime_tests`: `5/5`
- `new_editor_xcui_rhi_command_compiler_tests`: `6/6` - `new_editor_xcui_rhi_command_compiler_tests`: `6/6`
- `new_editor_xcui_rhi_render_backend_tests`: `5/5` - `new_editor_xcui_rhi_render_backend_tests`: `5/5`
- `XCNewEditor` Debug target builds successfully - `XCNewEditor` Debug target builds successfully
- `core_ui_tests`: `20/20` - `core_ui_tests`: `24/24`
## Landed This Phase ## Landed This Phase
- Demo runtime `TextField` with UTF-8 text insertion, caret state, and backspace. - 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. - Demo authored resources updated to exercise the input field.
- LayoutLab `ScrollView` prototype with clipping and hover rejection outside clipped content. - 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: - RHI image path improvements:
- clipped image UV adjustment - clipped image UV adjustment
- mirrored image UV preservation - mirrored image UV preservation
@@ -73,18 +82,19 @@ Current gap:
- per-batch scissor application - per-batch scissor application
- `new_editor` panel/shell diagnostics improvements for hosted preview state. - `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. - 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 ## 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. - `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. - `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. - Editor shell still depends on ImGui as host chrome.
## Next Phase ## Next Phase
1. Re-open common-layer schema/validation on a clean baseline and land the smallest stable version. 1. Cleanly stabilize schema/validation in `UIDocumentCompiler.cpp` and add targeted schema regression tests.
2. Expand runtime/game-layer ownership from `UIScreenPlayer`/`UISystem` into a real XCUI document host plus menu/HUD stack patterns. 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: `TextArea`, list/tree, property-style sections. 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. 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. 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) { std::string ToStdString(const Containers::String& value) {
if (value.Empty() || value.CStr() == nullptr) {
return std::string();
}
return std::string(value.CStr()); return std::string(value.CStr());
} }
@@ -151,6 +154,199 @@ bool ReadNode(std::ifstream& stream, UIDocumentNode& outNode) {
return true; 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) { bool IsNameStartChar(char ch) {
return std::isalpha(static_cast<unsigned char>(ch)) != 0 || return std::isalpha(static_cast<unsigned char>(ch)) != 0 ||
ch == '_' || ch == '_' ||
@@ -173,6 +369,560 @@ bool LooksLikeUIDocumentReference(const Containers::String& value) {
trimmed.size() >= 9 && trimmed.rfind(".xcschema") == trimmed.size() - 9); 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, void AppendUniqueDependency(const Containers::String& dependencyPath,
std::unordered_set<std::string>& seenDependencies, std::unordered_set<std::string>& seenDependencies,
Containers::Array<Containers::String>& outDependencies) { Containers::Array<Containers::String>& outDependencies) {
@@ -345,6 +1095,18 @@ public:
outResult.document.displayName = nameAttribute->value; 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; std::unordered_set<std::string> seenDependencies;
if (!CollectUIDocumentDependencies( if (!CollectUIDocumentDependencies(
m_resolvedPath, m_resolvedPath,
@@ -356,6 +1118,18 @@ public:
return Finalize(outResult, false); 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.document.valid = true;
outResult.succeeded = true; outResult.succeeded = true;
return Finalize(outResult, true); return Finalize(outResult, true);
@@ -752,6 +1526,12 @@ bool WriteUIDocumentArtifact(const Containers::String& artifactPath,
} }
return false; 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) { for (const Containers::String& dependency : compileResult.document.dependencies) {
WriteString(output, dependency); WriteString(output, dependency);
@@ -805,7 +1585,8 @@ bool LoadUIDocumentArtifact(const Containers::String& artifactPath,
return false; return false;
} }
if (header.schemaVersion != kUIDocumentArtifactSchemaVersion) { if (header.schemaVersion != 1u &&
header.schemaVersion != kUIDocumentArtifactSchemaVersion) {
outResult.errorMessage = Containers::String("Unsupported UI document artifact schema version: ") + artifactPath; outResult.errorMessage = Containers::String("Unsupported UI document artifact schema version: ") + artifactPath;
return false; return false;
} }
@@ -822,6 +1603,13 @@ bool LoadUIDocumentArtifact(const Containers::String& artifactPath,
outResult.errorMessage = Containers::String("Failed to read UI document artifact body: ") + artifactPath; outResult.errorMessage = Containers::String("Failed to read UI document artifact body: ") + artifactPath;
return false; 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.Clear();
outResult.document.dependencies.Reserve(header.dependencyCount); outResult.document.dependencies.Reserve(header.dependencyCount);
@@ -856,6 +1644,25 @@ bool LoadUIDocumentArtifact(const Containers::String& artifactPath,
outResult.document.diagnostics.PushBack(std::move(diagnostic)); 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.document.valid = true;
outResult.succeeded = true; outResult.succeeded = true;
return true; return true;