Component.vala 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Inversion;
  4. using Astralis;
  5. namespace Spry {
  6. public errordomain ComponentError {
  7. INVALID_TYPE,
  8. ELEMENT_NOT_FOUND,
  9. TYPE_NOT_FOUND;
  10. }
  11. public abstract class Component : Object, Renderable {
  12. private static Dictionary<Type, ComponentTemplate> templates;
  13. private static Mutex templates_lock = Mutex();
  14. public abstract string markup { get; }
  15. public virtual StatusCode get_status() {
  16. return StatusCode.OK;
  17. }
  18. public virtual async void prepare() throws Error {
  19. // No-op default
  20. }
  21. public virtual async void handle_action(string action) throws Error {
  22. // No-op default
  23. }
  24. public virtual async void continuation(SseStream stream) throws Error {
  25. // No-op default
  26. }
  27. public virtual async void continuation_canceled() throws Error {
  28. // No-op default
  29. }
  30. private PathProvider _path_provider = inject<PathProvider>();
  31. private ContinuationProvider _continuation_provider = inject<ContinuationProvider>();
  32. private ComponentFactory _component_factory = inject<ComponentFactory>();
  33. private Catalogue<string, Renderable> _children = new Catalogue<string, Renderable>();
  34. private Dictionary<string, Component> _child_components = new Dictionary<string, Component>();
  35. private HashSet<Component> _global_sources = new HashSet<Component>();
  36. private MarkupDocument _instance;
  37. private MarkupDocument instance { get {
  38. if(_instance == null) {
  39. try {
  40. lock(_instance) {
  41. if(_instance == null) {
  42. templates_lock.lock ();
  43. if(templates == null) {
  44. templates = new Dictionary<Type, ComponentTemplate>();
  45. }
  46. var type = this.get_type();
  47. ComponentTemplate template;
  48. if(!templates.try_get(type, out template)) {
  49. template = new ComponentTemplate(markup);
  50. templates[type] = template;
  51. }
  52. templates_lock.unlock();
  53. _instance = template.new_instance();
  54. }
  55. }
  56. }
  57. catch (Error e) {
  58. error(e.message);
  59. }
  60. }
  61. return _instance;
  62. }}
  63. protected MarkupNodeList get_elements_by_class_name(string class_name) {
  64. return instance.get_elements_by_class_name(class_name);
  65. }
  66. protected MarkupNodeList get_elements_by_tag_name(string tag_name) {
  67. return instance.get_elements_by_tag_name(tag_name);
  68. }
  69. protected new MarkupNodeList query(string xpath) {
  70. return instance.select(xpath);
  71. }
  72. protected MarkupNode? query_one(string xpath) {
  73. return instance.select_one(xpath);
  74. }
  75. protected MarkupNode get_element_by_global_id(string global_id) {
  76. return instance.get_element_by_id(global_id);
  77. }
  78. protected new MarkupNode? @get(string spry_id) {
  79. return instance.select_one(@"//*[@sid='$(spry_id)']");
  80. }
  81. protected void add_outlet_child(string outlet_id, Renderable renderable) {
  82. _children.add(outlet_id, renderable);
  83. }
  84. protected void add_outlet_children(string outlet_id, Enumerable<Renderable> renderables) {
  85. _children.add_all(outlet_id, renderables);
  86. }
  87. protected void set_outlet_children(string outlet_id, Enumerable<Renderable> renderables) {
  88. _children[outlet_id] = renderables;
  89. }
  90. protected void set_outlet_child(string outlet_id, Renderable renderable) {
  91. _children[outlet_id] = Iterate.single(renderable);
  92. }
  93. protected void clear_outlet_children(string outlet_id) {
  94. _children.clear_key(outlet_id);
  95. }
  96. protected void add_globals_from(Component component) {
  97. _global_sources.add(component);
  98. }
  99. protected T get_component_child<T>(string sid) throws Error {
  100. var node = query_one(@"//spry-component[@sid='$sid']");
  101. if(node == null) {
  102. throw new ComponentError.ELEMENT_NOT_FOUND(@"No spry-component element with sid '$sid' found.");
  103. }
  104. var component = get_component_instance_from_component_node(node);
  105. if(!component.get_type().is_a(typeof(T))) {
  106. throw new ComponentError.INVALID_TYPE(@"Component type $(component.get_type().name()) is not a $(typeof(T).name())");
  107. }
  108. return component;
  109. }
  110. public async MarkupDocument to_document() throws Error {
  111. yield prepare();
  112. var final_instance = instance.copy();
  113. remove_hidden_blocks(final_instance);
  114. yield transform_outlets(final_instance);
  115. yield transform_components(final_instance);
  116. replace_control_blocks(final_instance);
  117. transform_action_nodes(final_instance);
  118. transform_target_nodes(final_instance);
  119. transform_global_nodes(final_instance);
  120. transform_script_nodes(final_instance);
  121. transform_continuation_nodes(final_instance);
  122. remove_internal_sids(final_instance);
  123. yield append_globals(final_instance);
  124. return final_instance;
  125. }
  126. public async HttpResult to_result() throws Error {
  127. var document = yield to_document();
  128. return document.to_result(get_status());
  129. }
  130. private class ComponentTemplate : MarkupTemplate {
  131. private string _markup;
  132. protected override string markup { get { return _markup; } }
  133. public ComponentTemplate(string markup) {
  134. this._markup = markup;
  135. }
  136. }
  137. private Component get_component_instance_from_component_node(MarkupNode node) throws Error {
  138. Component component;
  139. // If no SID, create one to keep track of the instance
  140. var sid = node.get_attribute("sid");
  141. if(sid == null) {
  142. sid = Uuid.string_random();
  143. node.set_attribute("sid", sid);
  144. }
  145. if(!_child_components.try_get(sid, out component)) {
  146. component = _component_factory.create_by_name(node.get_attribute("name"));
  147. _child_components[sid] = component;
  148. }
  149. return component;
  150. }
  151. private async void transform_outlets(MarkupDocument doc) throws Error {
  152. var outlets = doc.select("//spry-outlet");
  153. foreach (var outlet in outlets) {
  154. var nodes = new Series<MarkupNode>();
  155. foreach(var renderable in _children.get_or_empty(outlet.get_attribute("sid"))) {
  156. var document = yield renderable.to_document();
  157. nodes.add_all(document.body.children);
  158. }
  159. outlet.replace_with_nodes(nodes);
  160. }
  161. }
  162. private void remove_hidden_blocks(MarkupDocument doc) {
  163. doc.select("//*[@spry-hidden]")
  164. .iterate(n => n.remove());
  165. }
  166. private void replace_control_blocks(MarkupDocument doc) {
  167. doc.select("//spry-control")
  168. .iterate(n => n.replace_with_nodes(n.children));
  169. }
  170. private void transform_action_nodes(MarkupDocument doc) throws Error {
  171. var action_nodes = doc.select("//*[@spry-action]");
  172. foreach(var node in action_nodes) {
  173. var action = node.get_attribute("spry-action").split(":", 2);
  174. var component_name = action[0].replace(".", "");
  175. if(component_name == "") {
  176. component_name = this.get_type().name();
  177. }
  178. var component_action = action[1];
  179. node.remove_attribute("spry-action");
  180. node.set_attribute("hx-get", _path_provider.get_action_path(component_name, component_action));
  181. }
  182. }
  183. private void transform_target_nodes(MarkupDocument doc) {
  184. var target_nodes = doc.select("//*[@spry-target]");
  185. foreach(var node in target_nodes) {
  186. var target_node = doc.select_one(@"//*[@sid='$(node.get_attribute("spry-target"))']");
  187. if(target_node.id == null) {
  188. target_node.id = "_spry-" + Uuid.string_random();
  189. }
  190. node.set_attribute("hx-target", @"#$(target_node.id)");
  191. node.remove_attribute("spry-target");
  192. }
  193. }
  194. private void transform_global_nodes(MarkupDocument doc) {
  195. var global_nodes = doc.select("//*[@spry-global]");
  196. foreach(var node in global_nodes) {
  197. var key = node.get_attribute("spry-global");
  198. node.set_attribute("hx-swap-oob", @"[spry-global=\"$key\"]");
  199. }
  200. }
  201. private void transform_script_nodes(MarkupDocument doc) {
  202. var script_nodes = doc.select("//script[@spry-res]");
  203. foreach(var node in script_nodes) {
  204. var res = node.get_attribute("spry-res");
  205. if(res != null) {
  206. node.set_attribute("src", "/_spry/res/" + res);
  207. }
  208. node.remove_attribute("spry-res");
  209. }
  210. }
  211. private void transform_continuation_nodes(MarkupDocument doc) {
  212. var continuation_nodes = doc.select("//*[@spry-continuation]");
  213. foreach(var node in continuation_nodes) {
  214. var path = _continuation_provider.get_continuation_path(this);
  215. node.set_attribute("hx-ext", "sse");
  216. node.set_attribute("sse-connect", path);
  217. node.set_attribute("sse-close", "_spry-close");
  218. node.remove_attribute("spry-continuation");
  219. }
  220. }
  221. private void remove_internal_sids(MarkupDocument doc) {
  222. doc.select("//*[@sid]")
  223. .iterate(n => n.remove_attribute("sid"));
  224. }
  225. private async void append_globals(MarkupDocument doc) throws Error {
  226. foreach(var source in _global_sources) {
  227. var document = yield source.to_document();
  228. var globals = document.select("//*[@spry-global]");
  229. doc.body.append_nodes(globals);
  230. }
  231. }
  232. private async void transform_components(MarkupDocument doc) throws Error {
  233. var components = doc.select("//spry-component");
  234. foreach (var component_node in components) {
  235. var component = get_component_instance_from_component_node(component_node);
  236. var document = yield component.to_document();
  237. component_node.replace_with_nodes(document.body.children);
  238. }
  239. }
  240. }
  241. }