Преглед на файлове

refactor(component): replace spry-prop with expression-based attributes

Replace the legacy spry-prop attribute system with a new expression-based
attribute syntax using the ExpressionParser. The new system uses *-expr
suffix attributes (content-expr, style-*-expr, class-*-expr) for more
flexible property binding.

- Add transform_expression_attributes to evaluate *-expr attributes
- Refactor transform_per_attributes to handle spry-per-* prefixed loops
- Update transform pipeline order for proper nested expression handling
- Remove get_ultimate_value and get_string_from_value helper methods
- Add CONFLICTING_ATTRIBUTES error type for attribute validation
- Update ProgressExample to demonstrate new attribute syntax
Billy Barrow преди 1 седмица
родител
ревизия
5a4c8e283b
променени са 2 файла, в които са добавени 108 реда и са изтрити 173 реда
  1. 4 3
      examples/ProgressExample.vala
  2. 104 170
      src/Component.vala

+ 4 - 3
examples/ProgressExample.vala

@@ -80,18 +80,19 @@ class ProgressComponent : Component {
         
         <div spry-continuation>
             <div class="progress-container" sse-swap="progress">
-                <div class="progress-bar" id="progress-bar" sid="progress-bar" spry-prop="percent; style.width = percent:%i%%">
+                <div class="progress-bar" id="progress-bar" sid="progress-bar" 
+                content-expr='format("%i%%", this.percent)' style-width-expr='format("%i%%", this.percent)'>
                     0%
                 </div>
             </div>
             
             <div class="status" sse-swap="status" sid="status" hx-swap="outerHTML">
-                <strong>Status:</strong> <span spry-prop="status">Initializing...</span>
+                <strong>Status:</strong> <span content-expr="this.status">Initializing...</span>
             </div>
             
             <div class="log" id="log" sid="log" sse-swap="log">
+                <div spry-per-task="this.completed_tasks" content-expr="task"></div>
                 <div>Waiting for task to start...</div>
-                <div spry-per="completed_tasks" spry-prop="value"></div>
             </div>
         </div>
         </body>

+ 104 - 170
src/Component.vala

@@ -1,5 +1,6 @@
 using Invercargill;
 using Invercargill.DataStructures;
+using Invercargill.Expressions;
 using Inversion;
 using Astralis;
 
@@ -9,7 +10,8 @@ namespace Spry {
         INVALID_TYPE,
         ELEMENT_NOT_FOUND,
         TYPE_NOT_FOUND,
-        PROPERTY_NOT_FOUND;
+        PROPERTY_NOT_FOUND,
+        CONFLICTING_ATTRIBUTES;
     }
 
     public abstract class Component : Object, Renderable {
@@ -198,9 +200,10 @@ namespace Spry {
         }
 
         private async void transform_document(MarkupDocument doc) throws Error {
-            remove_hidden_blocks(doc);
-            yield transform_per_attributes(doc);
-            yield transform_property_attributes(doc);
+            transform_if_attributes(doc); // Outputs spry-hidden attributes
+            remove_hidden_blocks(doc); // Removes tags with spry-hidden attributes
+            yield transform_per_attributes(doc); // Executes spry-per-* loops, which handles nested expression attributes
+            yield transform_expression_attributes(doc); // Evaluares *-expr attributes
             yield transform_outlets(doc);
             yield transform_components(doc);
             transform_action_nodes(doc);
@@ -309,211 +312,142 @@ namespace Spry {
             }
         }
 
-        private void transform_if_attributes(MarkupDocument doc) throws Error {
-            var nodes = doc.select("//*[@spry-if]");
+        private void transform_if_attributes(MarkupDocument doc, EvaluationContext? context = null) throws Error {
+            var nodes = doc.select("//spry-if");
             foreach (var node in nodes) {
-                var expression = node.get_attribute("spry-per");
+                var expression_string = node.get_attribute("spry-if");
 
+                var root = new PropertyDictionary();
+                root["this"] = new NativeElement<Component>(this);
+                var evaluation_context = context ?? new EvaluationContext(root);
+                var expression = ExpressionParser.parse(expression_string);
+                var result = expression.evaluate(evaluation_context);
+
+                bool boolean_result;
+                if(result.is<bool>()) {
+                    boolean_result = result.as<bool>();
+                }
+                else if(result.is<int>()) {
+                    boolean_result = result.as<int>() != 0;
+                }
+                else {
+                    boolean_result = !result.is_null();
+                }
+
+                if(boolean_result) {
+                    node.set_attribute("spry-hidden", "");
+                }
+                else {
+                    node.remove_attribute("spry-hidden");
+                }
             }
         }
 
-        private async void transform_per_attributes(MarkupDocument doc) throws Error {
-            var nodes = doc.select("//*[@spry-per]");
-            foreach (var node in nodes) {
-                var prop_path = node.get_attribute("spry-per");
-                var prop_value = get_ultimate_value(prop_path);
+        private async void transform_per_attributes(MarkupDocument doc, EvaluationContext? context = null) throws Error {
+            MarkupNode node;
+            // Select one by one, so we don't have problems with nesting
+            while((node = doc.select("//*[@*]").first_or_default(n => n.get_attributes().any(a => a.key.has_prefix("spry-per-")))) != null) {
+                var attribute = node.get_attributes().first_or_default(s => s.key.has_prefix("spry-per-"));
+                if(attribute == null) {
+                    continue;
+                }
 
-                if(!prop_value.type().is_a(typeof(Enumerable))) {
-                    throw new ComponentError.INVALID_TYPE(@"The spry-per attribute must refer to a value of type Invercargill.Enumerable, $prop_path is a $(prop_value.type().name())");
+                var root = new PropertyDictionary();
+                root["this"] = new NativeElement<Component>(this);
+                var evaluation_context = context ?? new EvaluationContext(root);
+                var expression = ExpressionParser.parse(attribute.value);
+                var result = expression.evaluate(evaluation_context);
+
+                if(!result.assignable_to<Enumerable>()) {
+                    throw new ComponentError.INVALID_TYPE(@"The spry-per attribute must refer to a value of type Invercargill.Enumerable, '$(attribute.value)' evaluates to a $(result.type_name())");
                 }
 
-                node.remove_attribute("spry-per");
-                var values = ((Enumerable)prop_value).as_values();
+                node.remove_attribute(attribute.key);
+                var values = result.as<Enumerable>().to_elements();
                 var output_nodes = new Series<MarkupNode>();
                 foreach(var value in values) {
+                    evaluation_context.root_values[attribute.key[9:]] = value;
                     var fragment = new MarkupDocument();
                     fragment.body.append_node(node);
-                    yield transform_property_attributes(fragment, value);
+                    
+                    // Basic transform pipeline for fragment, the rest will get processed as part
+                    // of the wider component document later.
+                    transform_if_attributes(fragment, evaluation_context);
+                    remove_hidden_blocks(fragment);
+                    yield transform_per_attributes(fragment, evaluation_context);
+                    yield transform_expression_attributes(fragment, evaluation_context);
+                    
                     output_nodes.add_all(fragment.body.children);
                 }
                 node.replace_with_nodes(output_nodes);
             }
         }
 
-        private async void transform_property_attributes(MarkupDocument doc, Value? context = null) throws Error {
-            var nodes = doc.select("//*[@spry-prop]");
+        private async void transform_expression_attributes(MarkupDocument doc, EvaluationContext? context = null) throws Error {
+            var nodes = doc.select("//*[@*]"); // Can't check for suffixes with xpath so iterate all nodes with attributes
             foreach (var node in nodes) {
-                var attr_string = node.get_attribute("spry-prop");
-                var paths = attr_string.split(";");
-                foreach (var prop_path in paths) {
-                    string attribute = null;
-                    string formatter = null;
-                    if(prop_path.contains("=")) {
-                        var parts = prop_path.split("=", 2);
-                        attribute = parts[0].chomp().chug();
-                        prop_path = parts[1].chomp().chug();
-                    }
-                    if(prop_path.contains(":")) {
-                        var parts = prop_path.split(":", 2);
-                        prop_path = parts[0].chomp().chug();
-                        formatter = parts[1].chomp().chug();
+                var attributes = node.get_attributes();
+                foreach (var attribute in attributes) {
+                    if(!attribute.key.has_suffix("-expr")) {
+                        continue;
                     }
 
-                    // Resolve the property path
-                    var value = get_ultimate_value(prop_path, context);
-                    var type = value.type();
+                    var real_attribute = attribute.key.substring(0, attribute.key.length - 5);
+                    var root = new PropertyDictionary();
+                    root["this"] = new NativeElement<Component>(this);
+                    var evaluation_context = context ?? new EvaluationContext(root);
+                    var expression = ExpressionParser.parse(attribute.value);
+                    var result = expression.evaluate(evaluation_context);
 
-                    // Render renderables
-                    if(type.is_a(typeof(Renderable))) {
-                        var renderable = (Renderable)value.get_object();
-                        if(renderable != null) {
-                            var document = yield renderable.to_document();
-                            node.replace_with_nodes(document.body.children);
+                    node.remove_attribute(attribute.key);
+                    
+                    // class.* can be boolean
+                    if(result.type().is_a(typeof(bool)) && real_attribute.has_prefix("class-")) {
+                        var class_name = real_attribute.split("-", 2)[1];
+                        if(result.as<bool>()) {
+                            if(!node.has_class(class_name)) {
+                                node.add_class(class_name);
+                            }
                         }
                         else {
-                            node.remove();
+                            node.remove_class(class_name);
                         }
                         continue;
                     }
 
-                    if(attribute.has_prefix("class.")) {
-                        var class_name = attribute[6:];
-                        if(!value.type().is_a(typeof(bool))) {
-                            throw new ComponentError.INVALID_TYPE(@"Property \'$prop_path\' is not assignable to \'$attribute\', only bools are valid");
-                        }
-                        if(value.get_boolean()) {
-                            node.add_class(class_name);
+                    if(real_attribute == "content" && result.type().is_a(typeof(Renderable))) {
+                        var renderable = result.as<Renderable>();
+                        var document = yield renderable.to_document();
+                        if(node.tag_name == "spry-outlet") {
+                            if(node.get_attribute("sid") != null) {
+                                throw new ComponentError.CONFLICTING_ATTRIBUTES("Tag 'spry-outlet' cannot have both a 'content-expr' and 'sid' attribute");
+                            }
+                            node.replace_with_nodes(document.body.children);
                         }
                         else {
-                            node.remove_class(class_name);
+                            node.clear_children();
+                            node.append_nodes(document.body.children);
                         }
                         continue;
                     }
-                    
-                    // Set text for stringified properties
-                    var output = get_string_from_value(value, formatter);
-                    if(attribute != null) {
-                        if(attribute.has_prefix("style.")) {
-                            node.set_style(attribute[6:], output);
-                        }
-                        else {
-                            node.set_attribute(attribute, output);
-                        }
-                    }
-                    else {
-                        node.text_content = output;
-                    }
-                }
-
-                node.remove_attribute("spry-prop");
-            }
-
-        }
 
-        private Value get_ultimate_value(string property_path, Value? context = null) throws Error {
-            var path = property_path.split(".");
-            var value = context;
+                    // everything else read as string
+                    var str_value = result.as<string>();
 
-            if(context == null || path[0] == "this") {
-                value = Value(get_type());
-                value.set_object(this);
-                if(path[0] == "this") {
-                    path = path[1:];
-                }
-            }
-            else if(path[0] == "value") {
-                if(value.type().is_a(typeof(Object)) && value.get_object() == this) {
-                    throw new ComponentError.PROPERTY_NOT_FOUND("Identifier 'value' not valid in this context");
-                }
-                path = path[1:];
-            }
+                    if(real_attribute == "content") {
+                        node.text_content = str_value;
+                        continue;
+                    }
 
-            foreach(var prop_name in path) {
-                if(!value.type().is_a(typeof(Object))) {
-                    throw new ComponentError.PROPERTY_NOT_FOUND(@"Cannot navigate to property \"$prop_name\" on type $(value.type_name()): Only Object types are navigable");
-                }
-                var source = value.get_object();
-                var prop_spec = source.get_class().find_property(prop_name);
-                if(prop_spec == null) {
-                    throw new ComponentError.PROPERTY_NOT_FOUND(@"Could not find property \"$prop_name\" on object $(source.get_type().name())");
-                }
-    
-                var type = prop_spec.value_type;
-                value = Value(type);                
-                source.get_property(prop_name, ref value);
-            }
+                    if(real_attribute.has_prefix("style-")) {
+                        var style_name = real_attribute.split("-", 2)[1];
+                        node.set_style(style_name, str_value);
+                        continue;
+                    }
 
-            return value;
-        }
-
-        private string get_string_from_value(Value value, string? formatter) {
-            var type = value.type();
-            
-            if (type == typeof(bool)) {
-                return value.get_boolean() ? "true" : "false";
-            } else if (type == typeof(int)) {
-                var int_val = value.get_int();
-                return formatter != null ? int_val.to_string(formatter) : int_val.to_string();
-            } else if (type == typeof(uint)) {
-                var uint_val = value.get_uint();
-                return formatter != null ? uint_val.to_string(formatter) : uint_val.to_string();
-            } else if (type == typeof(long)) {
-                var long_val = value.get_long();
-                return formatter != null ? long_val.to_string(formatter) : long_val.to_string();
-            } else if (type == typeof(ulong)) {
-                var ulong_val = value.get_ulong();
-                return formatter != null ? ulong_val.to_string(formatter) : ulong_val.to_string();
-            } else if (type == typeof(int64)) {
-                var int64_val = value.get_int64();
-                return formatter != null ? int64_val.to_string(formatter) : int64_val.to_string();
-            } else if (type == typeof(uint64)) {
-                var uint64_val = value.get_uint64();
-                return formatter != null ? uint64_val.to_string(formatter) : uint64_val.to_string();
-            } else if (type == typeof(float)) {
-                var float_val = value.get_float();
-                return formatter != null ? float_val.to_string(formatter) : float_val.to_string();
-            } else if (type == typeof(double)) {
-                var double_val = value.get_double();
-                return formatter != null ? @"%$(formatter)".printf(double_val) : double_val.to_string();
-            } else if (type == typeof(string)) {
-                return value.get_string() ?? "";
-            } else if (type == typeof(char)) {
-                var char_val = value.get_schar();
-                return formatter != null ? char_val.to_string(formatter) : char_val.to_string();
-            } else if (type == typeof(uchar)) {
-                var uchar_val = value.get_uchar();
-                return formatter != null ? uchar_val.to_string(formatter) : uchar_val.to_string();
-            } else if (type == typeof(int8)) {
-                var int8_val = value.get_schar();
-                return formatter != null ? int8_val.to_string(formatter) : int8_val.to_string();
-            } else if (type.is_enum()) {
-                var enum_val = value.get_enum();
-                return formatter != null ? enum_val.to_string(formatter) : enum_val.to_string();
-            } else if (type.is_flags()) {
-                var flags_val = value.get_flags();
-                return formatter != null ? flags_val.to_string(formatter) : flags_val.to_string();
-            } else if (type == typeof(Type)) {
-                return value.get_gtype().name();
-            } else if (type.is_object()) {
-                var obj = value.get_object();
-                if (obj == null) {
-                    return "null";
+                    node.set_attribute(real_attribute, str_value);
                 }
-                return @"<$(type.name())>";
-            } else if (type == typeof(Variant)) {
-                var variant = value.get_variant();
-                return variant != null ? variant.print(true) : "null";
-            } else if (type == typeof(ParamSpec)) {
-                var param = value.get_param();
-                return param != null ? param.name : "null";
-            } else if (type.is_derived() && !type.is_object() && !type.is_interface()) {
-                // Handle boxed types - derived but not object or interface
-                return @"<boxed $(type.name())>";
-            } else if (type == typeof(void*)) {
-                return @"<pointer 0x$(((int)value).to_string("%x"))>";
             }
-            
-            return "";
         }
 
     }