Stabilize XCUI schema compiler and phase 2 checkpoint
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user