using Invercargill; using Invercargill.DataStructures; using Inversion; using Astralis; namespace Spry { public abstract class Component : Object, Renderable { private static Dictionary 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 handle_action(string action) throws Error { // No-op default } public virtual async void continuation(SseStream stream) throws Error { // No-op default } private PathProvider _path_provider = inject(); private ContinuationProvider _continuation_provider = inject(); private Catalogue _children = new Catalogue(); private HashSet _global_sources = new HashSet(); private MarkupDocument _instance; private MarkupDocument instance { get { if(_instance == null) { try { lock(_instance) { if(_instance == null) { templates_lock.lock (); if(templates == null) { templates = new Dictionary(); } 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 renderables) { _children.add_all(outlet_id, renderables); } protected void set_outlet_children(string outlet_id, Enumerable 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); } public async MarkupDocument to_document() throws Error { yield prepare(); var final_instance = instance.copy(); // Replace outlets var outlets = final_instance.select("//spry-outlet"); foreach (var outlet in outlets) { var nodes = new Series(); 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); } // Remove hidden blocks final_instance.select("//*[@spry-hidden]") .iterate(n => n.remove()); // Replace control blocks with their children final_instance.select("//spry-control") .iterate(n => n.replace_with_nodes(n.children)); var action_nodes = final_instance.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)); } var target_nodes = final_instance.select("//*[@spry-target]"); foreach(var node in target_nodes) { var target_node = final_instance.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"); } var global_nodes = final_instance.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\"]"); } var script_nodes = final_instance.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"); } var continuation_nodes = final_instance.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.remove_attribute("spry-continuation"); } // Remove all internal SIDs final_instance.select("//*[@sid]") .iterate(n => n.remove_attribute("sid")); // Add globals foreach(var source in _global_sources) { var document = yield source.to_document(); var globals = document.select("//*[@spry-global]"); final_instance.body.append_nodes(globals); } 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; } } } }