| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Invercargill.Expressions;
- using Inversion;
- using Astralis;
- namespace Spry {
- public errordomain ComponentError {
- INVALID_TYPE,
- ELEMENT_NOT_FOUND,
- TYPE_NOT_FOUND,
- PROPERTY_NOT_FOUND,
- CONFLICTING_ATTRIBUTES;
- }
- public abstract class Component : Object, Renderable {
-
- private static Dictionary<Type, ComponentTemplate> templates;
- private static Mutex templates_lock = Mutex();
-
- public abstract string markup { get; }
- public virtual StatusCode get_status() {
- return StatusCode.OK;
- }
- public virtual async void prepare() throws Error {
- // No-op default
- }
- public virtual async void prepare_once() throws Error {
- // No-op default
- }
- public virtual async void handle_action(string action) throws Error {
- // No-op default
- }
- public virtual async void continuation(ContinuationContext continuation_context) throws Error {
- // No-op default
- }
- public virtual async void continuation_canceled() throws Error {
- // No-op default
- }
-
- private PathProvider _path_provider = inject<PathProvider>();
- private ContinuationProvider _continuation_provider = inject<ContinuationProvider>();
- private ComponentFactory _component_factory = inject<ComponentFactory>();
- private Catalogue<string, Renderable> _children = new Catalogue<string, Renderable>();
- private Dictionary<string, Component> _child_components = new Dictionary<string, Component>();
- private HashSet<Component> _global_sources = new HashSet<Component>();
- private MarkupDocument _instance;
- private bool _prepare_once_called;
- private MarkupDocument instance { get {
- if(_instance == null) {
- try {
- lock(_instance) {
- if(_instance == null) {
- templates_lock.lock ();
- if(templates == null) {
- templates = new Dictionary<Type, ComponentTemplate>();
- }
- var type = this.get_type();
- ComponentTemplate template;
- if(!templates.try_get(type, out template)) {
- template = new ComponentTemplate(markup);
- templates[type] = template;
- }
- templates_lock.unlock();
- _instance = template.new_instance();
- }
- }
- }
- catch (Error e) {
- error(e.message);
- }
- }
- return _instance;
- }}
- protected MarkupNodeList get_elements_by_class_name(string class_name) {
- return instance.get_elements_by_class_name(class_name);
- }
- protected MarkupNodeList get_elements_by_tag_name(string tag_name) {
- return instance.get_elements_by_tag_name(tag_name);
- }
- protected new MarkupNodeList query(string xpath) {
- return instance.select(xpath);
- }
- protected MarkupNode? query_one(string xpath) {
- return instance.select_one(xpath);
- }
- protected MarkupNode get_element_by_global_id(string global_id) {
- return instance.get_element_by_id(global_id);
- }
- protected new MarkupNode? @get(string spry_id) {
- return instance.select_one(@"//*[@sid='$(spry_id)']");
- }
- protected void add_outlet_child(string outlet_id, Renderable renderable) {
- _children.add(outlet_id, renderable);
- }
- protected void add_outlet_children(string outlet_id, Enumerable<Renderable> renderables) {
- _children.add_all(outlet_id, renderables);
- }
- protected void set_outlet_children(string outlet_id, Enumerable<Renderable> renderables) {
- _children[outlet_id] = renderables;
- }
- protected void set_outlet_child(string outlet_id, Renderable renderable) {
- _children[outlet_id] = Iterate.single(renderable);
- }
- protected void clear_outlet_children(string outlet_id) {
- _children.clear_key(outlet_id);
- }
- protected void add_globals_from(Component component) {
- _global_sources.add(component);
- }
- protected T get_component_child<T>(string sid) throws Error {
- var node = query_one(@"//spry-component[@sid='$sid']");
- if(node == null) {
- throw new ComponentError.ELEMENT_NOT_FOUND(@"No spry-component element with sid '$sid' found.");
- }
- var component = get_component_instance_from_component_node(node);
- if(!component.get_type().is_a(typeof(T))) {
- throw new ComponentError.INVALID_TYPE(@"Component type $(component.get_type().name()) is not a $(typeof(T).name())");
- }
- return component;
- }
- public async MarkupDocument to_document() throws Error {
- if(!_prepare_once_called) {
- yield prepare_once();
- _prepare_once_called = true;
- }
- yield prepare();
- var final_instance = instance.copy();
- yield transform_document(final_instance);
- return final_instance;
- }
- public async MarkupDocument to_fragment(string sid) throws Error {
- if(!_prepare_once_called) {
- yield prepare_once();
- _prepare_once_called = true;
- }
- yield prepare();
- // Extract the fragment
- var final_instance = instance.copy();
- var template_fragment = final_instance.select_one(@"//*[@sid='$sid']")?.outer_html;
- if(template_fragment == null) {
- throw new ComponentError.ELEMENT_NOT_FOUND(@"No spry-component with sid '$sid' found.");
- }
- final_instance.body.inner_html = template_fragment;
- // Do regular transform
- yield transform_document(final_instance);
- return final_instance;
- }
- public async HttpResult to_result() throws Error {
- var document = yield to_document();
- return document.to_result(get_status());
- }
- private class ComponentTemplate : MarkupTemplate {
- private string _markup;
- protected override string markup { get { return _markup; } }
- public ComponentTemplate(string markup) {
- this._markup = markup;
- }
- }
- private Component get_component_instance_from_component_node(MarkupNode node) throws Error {
- Component component;
- // If no SID, create one to keep track of the instance
- var sid = node.get_attribute("sid");
- if(sid == null) {
- sid = Uuid.string_random();
- node.set_attribute("sid", sid);
- }
- if(!_child_components.try_get(sid, out component)) {
- component = _component_factory.create_by_name(node.get_attribute("name"));
- _child_components[sid] = component;
- }
- return component;
- }
- private async void transform_document(MarkupDocument doc) throws Error {
- 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);
- transform_target_nodes(doc);
- transform_global_nodes(doc);
- transform_script_nodes(doc);
- transform_continuation_nodes(doc);
- remove_internal_sids(doc);
- yield append_globals(doc);
- }
- private async void transform_outlets(MarkupDocument doc) throws Error {
- var outlets = doc.select("//spry-outlet");
- foreach (var outlet in outlets) {
- var nodes = new Series<MarkupNode>();
- foreach(var renderable in _children.get_or_empty(outlet.get_attribute("sid"))) {
- var document = yield renderable.to_document();
- nodes.add_all(document.body.children);
- }
- outlet.replace_with_nodes(nodes);
- }
- }
- private void remove_hidden_blocks(MarkupDocument doc) {
- doc.select("//*[@spry-hidden]")
- .iterate(n => n.remove());
- }
- private void transform_action_nodes(MarkupDocument doc) throws Error {
- var action_nodes = doc.select("//*[@spry-action]");
- foreach(var node in action_nodes) {
- var action = node.get_attribute("spry-action").split(":", 2);
- var component_name = action[0].replace(".", "");
- if(component_name == "") {
- component_name = this.get_type().name();
- }
- var component_action = action[1];
- node.remove_attribute("spry-action");
- node.set_attribute("hx-get", _path_provider.get_action_path(component_name, component_action));
- }
- }
- private void transform_target_nodes(MarkupDocument doc) {
- var target_nodes = doc.select("//*[@spry-target]");
- foreach(var node in target_nodes) {
- var target_node = doc.select_one(@"//*[@sid='$(node.get_attribute("spry-target"))']");
- if(target_node.id == null) {
- target_node.id = "_spry-" + Uuid.string_random();
- }
- node.set_attribute("hx-target", @"#$(target_node.id)");
- node.remove_attribute("spry-target");
- }
- }
- private void transform_global_nodes(MarkupDocument doc) {
- var global_nodes = doc.select("//*[@spry-global]");
- foreach(var node in global_nodes) {
- var key = node.get_attribute("spry-global");
- node.set_attribute("hx-swap-oob", @"[spry-global=\"$key\"]");
- }
- }
- private void transform_script_nodes(MarkupDocument doc) {
- var script_nodes = doc.select("//script[@spry-res]");
- foreach(var node in script_nodes) {
- var res = node.get_attribute("spry-res");
- if(res != null) {
- node.set_attribute("src", "/_spry/res/" + res);
- }
- node.remove_attribute("spry-res");
- }
- }
- private void transform_continuation_nodes(MarkupDocument doc) {
- var continuation_nodes = doc.select("//*[@spry-continuation]");
- foreach(var node in continuation_nodes) {
- var path = _continuation_provider.get_continuation_path(this);
- node.set_attribute("hx-ext", "sse");
- node.set_attribute("sse-connect", path);
- node.set_attribute("sse-close", "_spry-close");
- node.remove_attribute("spry-continuation");
- }
- }
- private void remove_internal_sids(MarkupDocument doc) {
- doc.select("//*[@sid]")
- .iterate(n => n.remove_attribute("sid"));
- }
- private async void append_globals(MarkupDocument doc) throws Error {
- foreach(var source in _global_sources) {
- var document = yield source.to_document();
- var globals = document.select("//*[@spry-global]");
- doc.body.append_nodes(globals);
- }
- }
- private async void transform_components(MarkupDocument doc) throws Error {
- var components = doc.select("//spry-component");
- foreach (var component_node in components) {
- var component = get_component_instance_from_component_node(component_node);
- var document = yield component.to_document();
- component_node.replace_with_nodes(document.body.children);
- }
- }
- 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_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, 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;
- }
- 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(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);
-
- // 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_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 attributes = node.get_attributes();
- foreach (var attribute in attributes) {
- if(!attribute.key.has_suffix("-expr")) {
- continue;
- }
- 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);
- 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_class(class_name);
- }
- continue;
- }
- 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.clear_children();
- node.append_nodes(document.body.children);
- }
- continue;
- }
- // everything else read as string
- var str_value = result.as<string>();
- if(real_attribute == "content") {
- node.text_content = str_value;
- continue;
- }
- if(real_attribute.has_prefix("style-")) {
- var style_name = real_attribute.split("-", 2)[1];
- node.set_style(style_name, str_value);
- continue;
- }
- node.set_attribute(real_attribute, str_value);
- }
- }
- }
- }
- }
|