Component.vala 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Invercargill.Expressions;
  4. using Inversion;
  5. using Astralis;
  6. namespace Spry {
  7. public errordomain ComponentError {
  8. INVALID_TYPE,
  9. ELEMENT_NOT_FOUND,
  10. TYPE_NOT_FOUND,
  11. PROPERTY_NOT_FOUND,
  12. CONFLICTING_ATTRIBUTES;
  13. }
  14. public abstract class Component : Object, Renderable {
  15. private static Dictionary<Type, ComponentTemplate> templates;
  16. private static Mutex templates_lock = Mutex();
  17. public abstract string markup { get; }
  18. public virtual StatusCode get_status() {
  19. return StatusCode.OK;
  20. }
  21. public virtual async void prepare() throws Error {
  22. // No-op default
  23. }
  24. public virtual async void prepare_once() throws Error {
  25. // No-op default
  26. }
  27. public virtual async void handle_action(string action) throws Error {
  28. // No-op default
  29. }
  30. public virtual async void continuation(ContinuationContext continuation_context) throws Error {
  31. // No-op default
  32. }
  33. public virtual async void continuation_canceled() throws Error {
  34. // No-op default
  35. }
  36. private PathProvider _path_provider = inject<PathProvider>();
  37. private ContinuationProvider _continuation_provider = inject<ContinuationProvider>();
  38. private ComponentFactory _component_factory = inject<ComponentFactory>();
  39. private Catalogue<string, Renderable> _children = new Catalogue<string, Renderable>();
  40. private Dictionary<string, Component> _child_components = new Dictionary<string, Component>();
  41. private HashSet<Component> _global_sources = new HashSet<Component>();
  42. private MarkupDocument _instance;
  43. private bool _prepare_once_called;
  44. private MarkupDocument instance { get {
  45. if(_instance == null) {
  46. try {
  47. lock(_instance) {
  48. if(_instance == null) {
  49. templates_lock.lock ();
  50. if(templates == null) {
  51. templates = new Dictionary<Type, ComponentTemplate>();
  52. }
  53. var type = this.get_type();
  54. ComponentTemplate template;
  55. if(!templates.try_get(type, out template)) {
  56. template = new ComponentTemplate(markup);
  57. templates[type] = template;
  58. }
  59. templates_lock.unlock();
  60. _instance = template.new_instance();
  61. }
  62. }
  63. }
  64. catch (Error e) {
  65. error(e.message);
  66. }
  67. }
  68. return _instance;
  69. }}
  70. protected MarkupNodeList get_elements_by_class_name(string class_name) {
  71. return instance.get_elements_by_class_name(class_name);
  72. }
  73. protected MarkupNodeList get_elements_by_tag_name(string tag_name) {
  74. return instance.get_elements_by_tag_name(tag_name);
  75. }
  76. protected new MarkupNodeList query(string xpath) {
  77. return instance.select(xpath);
  78. }
  79. protected MarkupNode? query_one(string xpath) {
  80. return instance.select_one(xpath);
  81. }
  82. protected MarkupNode get_element_by_global_id(string global_id) {
  83. return instance.get_element_by_id(global_id);
  84. }
  85. protected new MarkupNode? @get(string spry_id) {
  86. return instance.select_one(@"//*[@sid='$(spry_id)']");
  87. }
  88. protected void add_outlet_child(string outlet_id, Renderable renderable) {
  89. _children.add(outlet_id, renderable);
  90. }
  91. protected void add_outlet_children(string outlet_id, Enumerable<Renderable> renderables) {
  92. _children.add_all(outlet_id, renderables);
  93. }
  94. protected void set_outlet_children(string outlet_id, Enumerable<Renderable> renderables) {
  95. _children[outlet_id] = renderables;
  96. }
  97. protected void set_outlet_child(string outlet_id, Renderable renderable) {
  98. _children[outlet_id] = Iterate.single(renderable);
  99. }
  100. protected void clear_outlet_children(string outlet_id) {
  101. _children.clear_key(outlet_id);
  102. }
  103. protected void add_globals_from(Component component) {
  104. _global_sources.add(component);
  105. }
  106. protected T get_component_child<T>(string sid) throws Error {
  107. var node = query_one(@"//spry-component[@sid='$sid']");
  108. if(node == null) {
  109. throw new ComponentError.ELEMENT_NOT_FOUND(@"No spry-component element with sid '$sid' found.");
  110. }
  111. var component = get_component_instance_from_component_node(node);
  112. if(!component.get_type().is_a(typeof(T))) {
  113. throw new ComponentError.INVALID_TYPE(@"Component type $(component.get_type().name()) is not a $(typeof(T).name())");
  114. }
  115. return component;
  116. }
  117. public async MarkupDocument to_document() throws Error {
  118. if(!_prepare_once_called) {
  119. yield prepare_once();
  120. _prepare_once_called = true;
  121. }
  122. yield prepare();
  123. var final_instance = instance.copy();
  124. yield transform_document(final_instance);
  125. return final_instance;
  126. }
  127. public async MarkupDocument to_fragment(string sid) throws Error {
  128. if(!_prepare_once_called) {
  129. yield prepare_once();
  130. _prepare_once_called = true;
  131. }
  132. yield prepare();
  133. // Extract the fragment
  134. var final_instance = instance.copy();
  135. var template_fragment = final_instance.select_one(@"//*[@sid='$sid']")?.outer_html;
  136. if(template_fragment == null) {
  137. throw new ComponentError.ELEMENT_NOT_FOUND(@"No spry-component with sid '$sid' found.");
  138. }
  139. final_instance.body.inner_html = template_fragment;
  140. // Do regular transform
  141. yield transform_document(final_instance);
  142. return final_instance;
  143. }
  144. public async HttpResult to_result() throws Error {
  145. var document = yield to_document();
  146. return document.to_result(get_status());
  147. }
  148. private class ComponentTemplate : MarkupTemplate {
  149. private string _markup;
  150. protected override string markup { get { return _markup; } }
  151. public ComponentTemplate(string markup) {
  152. this._markup = markup;
  153. }
  154. }
  155. private Component get_component_instance_from_component_node(MarkupNode node) throws Error {
  156. Component component;
  157. // If no SID, create one to keep track of the instance
  158. var sid = node.get_attribute("sid");
  159. if(sid == null) {
  160. sid = Uuid.string_random();
  161. node.set_attribute("sid", sid);
  162. }
  163. if(!_child_components.try_get(sid, out component)) {
  164. component = _component_factory.create_by_name(node.get_attribute("name"));
  165. _child_components[sid] = component;
  166. }
  167. return component;
  168. }
  169. private async void transform_document(MarkupDocument doc) throws Error {
  170. transform_if_attributes(doc); // Outputs spry-hidden attributes
  171. remove_hidden_blocks(doc); // Removes tags with spry-hidden attributes
  172. yield transform_per_attributes(doc); // Executes spry-per-* loops, which handles nested expression attributes
  173. yield transform_expression_attributes(doc); // Evaluares *-expr attributes
  174. yield transform_outlets(doc);
  175. yield transform_components(doc);
  176. transform_action_nodes(doc);
  177. transform_target_nodes(doc);
  178. transform_global_nodes(doc);
  179. transform_script_nodes(doc);
  180. transform_continuation_nodes(doc);
  181. remove_internal_sids(doc);
  182. yield append_globals(doc);
  183. }
  184. private async void transform_outlets(MarkupDocument doc) throws Error {
  185. var outlets = doc.select("//spry-outlet");
  186. foreach (var outlet in outlets) {
  187. var nodes = new Series<MarkupNode>();
  188. foreach(var renderable in _children.get_or_empty(outlet.get_attribute("sid"))) {
  189. var document = yield renderable.to_document();
  190. nodes.add_all(document.body.children);
  191. }
  192. outlet.replace_with_nodes(nodes);
  193. }
  194. }
  195. private void remove_hidden_blocks(MarkupDocument doc) {
  196. doc.select("//*[@spry-hidden]")
  197. .iterate(n => n.remove());
  198. }
  199. private void transform_action_nodes(MarkupDocument doc) throws Error {
  200. var action_nodes = doc.select("//*[@spry-action]");
  201. foreach(var node in action_nodes) {
  202. var action = node.get_attribute("spry-action").split(":", 2);
  203. var component_name = action[0].replace(".", "");
  204. if(component_name == "") {
  205. component_name = this.get_type().name();
  206. }
  207. var component_action = action[1];
  208. node.remove_attribute("spry-action");
  209. node.set_attribute("hx-get", _path_provider.get_action_path(component_name, component_action));
  210. }
  211. }
  212. private void transform_target_nodes(MarkupDocument doc) {
  213. var target_nodes = doc.select("//*[@spry-target]");
  214. foreach(var node in target_nodes) {
  215. var target_node = doc.select_one(@"//*[@sid='$(node.get_attribute("spry-target"))']");
  216. if(target_node.id == null) {
  217. target_node.id = "_spry-" + Uuid.string_random();
  218. }
  219. node.set_attribute("hx-target", @"#$(target_node.id)");
  220. node.remove_attribute("spry-target");
  221. }
  222. }
  223. private void transform_global_nodes(MarkupDocument doc) {
  224. var global_nodes = doc.select("//*[@spry-global]");
  225. foreach(var node in global_nodes) {
  226. var key = node.get_attribute("spry-global");
  227. node.set_attribute("hx-swap-oob", @"[spry-global=\"$key\"]");
  228. }
  229. }
  230. private void transform_script_nodes(MarkupDocument doc) {
  231. var script_nodes = doc.select("//script[@spry-res]");
  232. foreach(var node in script_nodes) {
  233. var res = node.get_attribute("spry-res");
  234. if(res != null) {
  235. node.set_attribute("src", "/_spry/res/" + res);
  236. }
  237. node.remove_attribute("spry-res");
  238. }
  239. }
  240. private void transform_continuation_nodes(MarkupDocument doc) {
  241. var continuation_nodes = doc.select("//*[@spry-continuation]");
  242. foreach(var node in continuation_nodes) {
  243. var path = _continuation_provider.get_continuation_path(this);
  244. node.set_attribute("hx-ext", "sse");
  245. node.set_attribute("sse-connect", path);
  246. node.set_attribute("sse-close", "_spry-close");
  247. node.remove_attribute("spry-continuation");
  248. }
  249. }
  250. private void remove_internal_sids(MarkupDocument doc) {
  251. doc.select("//*[@sid]")
  252. .iterate(n => n.remove_attribute("sid"));
  253. }
  254. private async void append_globals(MarkupDocument doc) throws Error {
  255. foreach(var source in _global_sources) {
  256. var document = yield source.to_document();
  257. var globals = document.select("//*[@spry-global]");
  258. doc.body.append_nodes(globals);
  259. }
  260. }
  261. private async void transform_components(MarkupDocument doc) throws Error {
  262. var components = doc.select("//spry-component");
  263. foreach (var component_node in components) {
  264. var component = get_component_instance_from_component_node(component_node);
  265. var document = yield component.to_document();
  266. component_node.replace_with_nodes(document.body.children);
  267. }
  268. }
  269. private void transform_if_attributes(MarkupDocument doc, EvaluationContext? context = null) throws Error {
  270. var nodes = doc.select("//spry-if");
  271. foreach (var node in nodes) {
  272. var expression_string = node.get_attribute("spry-if");
  273. var root = new PropertyDictionary();
  274. root["this"] = new NativeElement<Component>(this);
  275. var evaluation_context = context ?? new EvaluationContext(root);
  276. var expression = ExpressionParser.parse(expression_string);
  277. var result = expression.evaluate(evaluation_context);
  278. bool boolean_result;
  279. if(result.is<bool>()) {
  280. boolean_result = result.as<bool>();
  281. }
  282. else if(result.is<int>()) {
  283. boolean_result = result.as<int>() != 0;
  284. }
  285. else {
  286. boolean_result = !result.is_null();
  287. }
  288. if(boolean_result) {
  289. node.set_attribute("spry-hidden", "");
  290. }
  291. else {
  292. node.remove_attribute("spry-hidden");
  293. }
  294. }
  295. }
  296. private async void transform_per_attributes(MarkupDocument doc, EvaluationContext? context = null) throws Error {
  297. MarkupNode node;
  298. // Select one by one, so we don't have problems with nesting
  299. while((node = doc.select("//*[@*]").first_or_default(n => n.get_attributes().any(a => a.key.has_prefix("spry-per-")))) != null) {
  300. var attribute = node.get_attributes().first_or_default(s => s.key.has_prefix("spry-per-"));
  301. if(attribute == null) {
  302. continue;
  303. }
  304. var root = new PropertyDictionary();
  305. root["this"] = new NativeElement<Component>(this);
  306. var evaluation_context = context ?? new EvaluationContext(root);
  307. var expression = ExpressionParser.parse(attribute.value);
  308. var result = expression.evaluate(evaluation_context);
  309. if(!result.assignable_to<Enumerable>()) {
  310. 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())");
  311. }
  312. node.remove_attribute(attribute.key);
  313. var values = result.as<Enumerable>().to_elements();
  314. var output_nodes = new Series<MarkupNode>();
  315. foreach(var value in values) {
  316. evaluation_context.root_values[attribute.key[9:]] = value;
  317. var fragment = new MarkupDocument();
  318. fragment.body.append_node(node);
  319. // Basic transform pipeline for fragment, the rest will get processed as part
  320. // of the wider component document later.
  321. transform_if_attributes(fragment, evaluation_context);
  322. remove_hidden_blocks(fragment);
  323. yield transform_per_attributes(fragment, evaluation_context);
  324. yield transform_expression_attributes(fragment, evaluation_context);
  325. output_nodes.add_all(fragment.body.children);
  326. }
  327. node.replace_with_nodes(output_nodes);
  328. }
  329. }
  330. private async void transform_expression_attributes(MarkupDocument doc, EvaluationContext? context = null) throws Error {
  331. var nodes = doc.select("//*[@*]"); // Can't check for suffixes with xpath so iterate all nodes with attributes
  332. foreach (var node in nodes) {
  333. var attributes = node.get_attributes();
  334. foreach (var attribute in attributes) {
  335. if(!attribute.key.has_suffix("-expr")) {
  336. continue;
  337. }
  338. var real_attribute = attribute.key.substring(0, attribute.key.length - 5);
  339. var root = new PropertyDictionary();
  340. root["this"] = new NativeElement<Component>(this);
  341. var evaluation_context = context ?? new EvaluationContext(root);
  342. var expression = ExpressionParser.parse(attribute.value);
  343. var result = expression.evaluate(evaluation_context);
  344. node.remove_attribute(attribute.key);
  345. // class.* can be boolean
  346. if(result.type().is_a(typeof(bool)) && real_attribute.has_prefix("class-")) {
  347. var class_name = real_attribute.split("-", 2)[1];
  348. if(result.as<bool>()) {
  349. if(!node.has_class(class_name)) {
  350. node.add_class(class_name);
  351. }
  352. }
  353. else {
  354. node.remove_class(class_name);
  355. }
  356. continue;
  357. }
  358. if(real_attribute == "content" && result.type().is_a(typeof(Renderable))) {
  359. var renderable = result.as<Renderable>();
  360. var document = yield renderable.to_document();
  361. if(node.tag_name == "spry-outlet") {
  362. if(node.get_attribute("sid") != null) {
  363. throw new ComponentError.CONFLICTING_ATTRIBUTES("Tag 'spry-outlet' cannot have both a 'content-expr' and 'sid' attribute");
  364. }
  365. node.replace_with_nodes(document.body.children);
  366. }
  367. else {
  368. node.clear_children();
  369. node.append_nodes(document.body.children);
  370. }
  371. continue;
  372. }
  373. // everything else read as string
  374. var str_value = result.as<string>();
  375. if(real_attribute == "content") {
  376. node.text_content = str_value;
  377. continue;
  378. }
  379. if(real_attribute.has_prefix("style-")) {
  380. var style_name = real_attribute.split("-", 2)[1];
  381. node.set_style(style_name, str_value);
  382. continue;
  383. }
  384. node.set_attribute(real_attribute, str_value);
  385. }
  386. }
  387. }
  388. }
  389. }