|
|
@@ -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 "";
|
|
|
}
|
|
|
|
|
|
}
|