DocumentBuilder.vala 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. using Astralis;
  2. using Invercargill;
  3. using Invercargill.DataStructures;
  4. /**
  5. * DocumentBuilder Example
  6. *
  7. * Demonstrates using the DocumentModel classes (MarkupDocument, MarkupNode) to
  8. * programmatically build and manipulate HTML documents. Shows how to:
  9. * - Create HTML documents from scratch or from templates
  10. * - Use XPath selectors to find elements
  11. * - Manipulate DOM elements (add/remove classes, attributes, content)
  12. * - Handle form POST submissions with dynamic document updates
  13. *
  14. * This example references patterns from:
  15. * - SimpleApi.vala: Basic endpoint structure
  16. * - FastResources.vala: Content type handling and response building
  17. *
  18. * Usage: document-builder [port]
  19. *
  20. * Examples:
  21. * document-builder
  22. * document-builder 8080
  23. */
  24. // Simple task model for our todo list
  25. class TodoItem : Object {
  26. public int id { get; set; }
  27. public string title { get; set; }
  28. public bool completed { get; set; }
  29. public TodoItem(int id, string title, bool completed = false) {
  30. this.id = id;
  31. this.title = title;
  32. this.completed = completed;
  33. }
  34. }
  35. // In-memory todo store
  36. class TodoStore : Object {
  37. private Series<TodoItem> items = new Series<TodoItem>();
  38. private int next_id = 1;
  39. public TodoStore() {
  40. // Add some initial items
  41. add("Learn Astralis DocumentModel");
  42. add("Build dynamic HTML pages");
  43. add("Handle form submissions");
  44. }
  45. public void add(string title) {
  46. items.add(new TodoItem(next_id++, title));
  47. }
  48. public void toggle(int id) {
  49. items.to_immutable_buffer().iterate((item) => {
  50. if (item.id == id) {
  51. item.completed = !item.completed;
  52. }
  53. });
  54. }
  55. public void remove(int id) {
  56. var new_items = items.to_immutable_buffer()
  57. .where(item => item.id != id)
  58. .to_series();
  59. items = new_items;
  60. }
  61. public ImmutableBuffer<TodoItem> all() {
  62. return items.to_immutable_buffer();
  63. }
  64. public int count() {
  65. return (int)items.to_immutable_buffer().count();
  66. }
  67. public int completed_count() {
  68. return (int)items.to_immutable_buffer()
  69. .count(item => item.completed);
  70. }
  71. }
  72. // Main application with todo store
  73. TodoStore todo_store;
  74. // Home page endpoint - builds HTML document programmatically
  75. class HomePageEndpoint : Object, Endpoint {
  76. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  77. var doc = create_base_document("Document Builder Example");
  78. // Get the body element
  79. var body = doc.body;
  80. // Add a header section
  81. add_header_section(doc, body);
  82. // Add the todo list section
  83. add_todo_list_section(doc, body);
  84. // Add the add todo form
  85. add_form_section(doc, body);
  86. // Add a section showing DocumentModel features
  87. add_features_section(doc, body);
  88. // Add footer
  89. add_footer(doc, body);
  90. return doc.to_result();
  91. }
  92. private MarkupDocument create_base_document(string title) throws Error {
  93. // Create document from string template
  94. var doc = new MarkupDocument.from_string("""
  95. <!DOCTYPE html>
  96. <html>
  97. <head>
  98. <meta charset="UTF-8"/>
  99. <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  100. <title></title>
  101. <style>
  102. body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
  103. .card { background: white; border-radius: 8px; padding: 20px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
  104. h1 { color: #333; margin-top: 0; }
  105. h2 { color: #555; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
  106. .todo-item { display: flex; align-items: center; padding: 10px; margin: 5px 0; background: #fafafa; border-radius: 4px; border-left: 4px solid #4CAF50; }
  107. .todo-item.completed { border-left-color: #ccc; opacity: 0.7; }
  108. .todo-item.completed .title { text-decoration: line-through; color: #888; }
  109. .todo-item .title { flex: 1; margin: 0 10px; }
  110. .todo-item form { margin: 0; }
  111. .stats { display: flex; gap: 20px; margin: 10px 0; }
  112. .stat { background: #e8f5e9; padding: 10px 20px; border-radius: 4px; }
  113. .stat-value { font-size: 24px; font-weight: bold; color: #4CAF50; }
  114. .stat-label { font-size: 12px; color: #666; }
  115. input[type="text"] { padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1; }
  116. button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
  117. .btn-primary { background: #4CAF50; color: white; }
  118. .btn-primary:hover { background: #45a049; }
  119. .btn-toggle { background: #2196F3; color: white; padding: 5px 10px; font-size: 12px; }
  120. .btn-delete { background: #f44336; color: white; padding: 5px 10px; font-size: 12px; }
  121. code { background: #e8e8e8; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
  122. pre { background: #263238; color: #aed581; padding: 15px; border-radius: 4px; overflow-x: auto; }
  123. .feature { margin: 15px 0; }
  124. .feature code { display: block; background: #f5f5f5; padding: 10px; margin: 5px 0; }
  125. a { color: #2196F3; text-decoration: none; }
  126. a:hover { text-decoration: underline; }
  127. .empty { color: #999; font-style: italic; padding: 20px; text-align: center; }
  128. </style>
  129. </head>
  130. <body></body>
  131. </html>
  132. """);
  133. // Set the title using the document's title property
  134. doc.title = title;
  135. return doc;
  136. }
  137. private void add_header_section(MarkupDocument doc, MarkupNode body) {
  138. // Create header card
  139. var header_card = body.append_child_element("div");
  140. header_card.add_class("card");
  141. var h1 = header_card.append_child_with_text("h1", "๐Ÿ“„ Document Builder Example");
  142. // Add description paragraph
  143. var desc = header_card.append_child_element("p");
  144. desc.append_text("This page demonstrates the ");
  145. var code = desc.append_child_element("code");
  146. code.append_text("MarkupDocument");
  147. desc.append_text(" and ");
  148. code = desc.append_child_element("code");
  149. code.append_text("MarkupNode");
  150. desc.append_text(" classes from the DocumentModel. The entire page is built programmatically!");
  151. // Add stats
  152. var stats_div = header_card.append_child_element("div");
  153. stats_div.add_class("stats");
  154. var total_stat = stats_div.append_child_element("div");
  155. total_stat.add_class("stat");
  156. var total_value = total_stat.append_child_with_text("div", todo_store.count().to_string());
  157. total_value.add_class("stat-value");
  158. var total_label = total_stat.append_child_with_text("div", "Total Tasks");
  159. total_label.add_class("stat-label");
  160. var completed_stat = stats_div.append_child_element("div");
  161. completed_stat.add_class("stat");
  162. var completed_value = completed_stat.append_child_with_text("div", todo_store.completed_count().to_string());
  163. completed_value.add_class("stat-value");
  164. var completed_label = completed_stat.append_child_with_text("div", "Completed");
  165. completed_label.add_class("stat-label");
  166. }
  167. private void add_todo_list_section(MarkupDocument doc, MarkupNode body) {
  168. var list_card = body.append_child_element("div");
  169. list_card.add_class("card");
  170. var h2 = list_card.append_child_with_text("h2", "Todo List");
  171. var todos = todo_store.all();
  172. var count = todo_store.count();
  173. if (count == 0) {
  174. var empty = list_card.append_child_element("div");
  175. empty.add_class("empty");
  176. empty.append_text("No tasks yet! Add one below.");
  177. } else {
  178. todos.iterate((item) => {
  179. var item_div = list_card.append_child_element("div");
  180. item_div.add_class("todo-item");
  181. if (item.completed) {
  182. item_div.add_class("completed");
  183. }
  184. // Toggle form
  185. var toggle_form = item_div.append_child_element("form");
  186. toggle_form.set_attribute("method", "POST");
  187. toggle_form.set_attribute("action", "/toggle");
  188. toggle_form.set_attribute("style", "display: inline;");
  189. var hidden_id = toggle_form.append_child_element("input");
  190. hidden_id.set_attribute("type", "hidden");
  191. hidden_id.set_attribute("name", "id");
  192. hidden_id.set_attribute("value", item.id.to_string());
  193. var toggle_btn = toggle_form.append_child_element("button");
  194. toggle_btn.add_class("btn-toggle");
  195. toggle_btn.set_attribute("type", "submit");
  196. toggle_btn.append_text(item.completed ? "โ†ฉ๏ธ Undo" : "โœ“ Done");
  197. // Title span
  198. var title_span = item_div.append_child_with_text("span", item.title);
  199. title_span.add_class("title");
  200. // Delete form
  201. var delete_form = item_div.append_child_element("form");
  202. delete_form.set_attribute("method", "POST");
  203. delete_form.set_attribute("action", "/delete");
  204. delete_form.set_attribute("style", "display: inline;");
  205. var delete_hidden = delete_form.append_child_element("input");
  206. delete_hidden.set_attribute("type", "hidden");
  207. delete_hidden.set_attribute("name", "id");
  208. delete_hidden.set_attribute("value", item.id.to_string());
  209. var delete_btn = delete_form.append_child_element("button");
  210. delete_btn.add_class("btn-delete");
  211. delete_btn.set_attribute("type", "submit");
  212. delete_btn.append_text("๐Ÿ—‘๏ธ Delete");
  213. });
  214. }
  215. }
  216. private void add_form_section(MarkupDocument doc, MarkupNode body) {
  217. var form_card = body.append_child_element("div");
  218. form_card.add_class("card");
  219. var h2 = form_card.append_child_with_text("h2", "Add New Task");
  220. // Create form
  221. var form = form_card.append_child_element("form");
  222. form.set_attribute("method", "POST");
  223. form.set_attribute("action", "/add");
  224. form.set_attribute("style", "display: flex; gap: 10px;");
  225. // Text input
  226. var input = form.append_child_element("input");
  227. input.set_attribute("type", "text");
  228. input.set_attribute("name", "title");
  229. input.set_attribute("placeholder", "Enter a new task...");
  230. input.set_attribute("required", "required");
  231. // Submit button
  232. var submit = form.append_child_element("button");
  233. submit.add_class("btn-primary");
  234. submit.set_attribute("type", "submit");
  235. submit.append_text("Add Task");
  236. }
  237. private void add_features_section(MarkupDocument doc, MarkupNode body) {
  238. var features_card = body.append_child_element("div");
  239. features_card.add_class("card");
  240. var h2 = features_card.append_child_with_text("h2", "DocumentModel Features Used");
  241. // Feature 1: Creating documents
  242. var f1 = features_card.append_child_element("div");
  243. f1.add_class("feature");
  244. f1.append_child_with_text("strong", "Creating Documents:");
  245. var code1 = f1.append_child_element("code");
  246. code1.append_text("var doc = new MarkupDocument.from_string(html_template);");
  247. // Feature 2: XPath selectors
  248. var f2 = features_card.append_child_element("div");
  249. f2.add_class("feature");
  250. f2.append_child_with_text("strong", "XPath Selectors:");
  251. var code2 = f2.append_child_element("code");
  252. code2.append_text("var element = doc.select_one(\"//div[@id='content']\");");
  253. // Feature 3: DOM manipulation
  254. var f3 = features_card.append_child_element("div");
  255. f3.add_class("feature");
  256. f3.append_child_with_text("strong", "DOM Manipulation:");
  257. var code3 = f3.append_child_element("code");
  258. code3.append_text("element.add_class(\"highlight\"); element.set_attribute(\"data-id\", \"123\");");
  259. // Feature 4: Building elements
  260. var f4 = features_card.append_child_element("div");
  261. f4.add_class("feature");
  262. f4.append_child_with_text("strong", "Building Elements:");
  263. var code4 = f4.append_child_element("code");
  264. code4.append_text("var child = parent.append_child_element(\"div\"); child.append_text(\"Hello!\");");
  265. // Feature 5: Returning HTML
  266. var f5 = features_card.append_child_element("div");
  267. f5.add_class("feature");
  268. f5.append_child_with_text("strong", "Returning HTML Response:");
  269. var code5 = f5.append_child_element("code");
  270. code5.append_text("return doc.to_result(); // Returns HtmlResult with correct Content-Type");
  271. }
  272. private void add_footer(MarkupDocument doc, MarkupNode body) {
  273. var footer_card = body.append_child_element("div");
  274. footer_card.add_class("card");
  275. footer_card.set_attribute("style", "text-align: center; color: #666;");
  276. var p = footer_card.append_child_element("p");
  277. p.append_text("Built with ");
  278. var code = p.append_child_element("code");
  279. code.append_text("Astralis.Document");
  280. p.append_text(" - See ");
  281. var link = p.append_child_element("a");
  282. link.set_attribute("href", "https://github.com/example/astralis");
  283. link.append_text("GitHub");
  284. p.append_text(" for more examples.");
  285. }
  286. }
  287. // Add todo endpoint
  288. class AddTodoEndpoint : Object, Endpoint {
  289. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  290. // Parse form data
  291. FormData form_data = yield FormDataParser.parse(
  292. context.request.request_body,
  293. context.request.content_type
  294. );
  295. var title = form_data.get_field("title");
  296. if (title != null && title.strip() != "") {
  297. todo_store.add(title.strip());
  298. }
  299. // Redirect back to home (302 Found)
  300. return new HttpStringResult("", (StatusCode)302)
  301. .set_header("Location", "/");
  302. }
  303. }
  304. // Toggle todo endpoint
  305. class ToggleTodoEndpoint : Object, Endpoint {
  306. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  307. FormData form_data = yield FormDataParser.parse(
  308. context.request.request_body,
  309. context.request.content_type
  310. );
  311. var id_str = form_data.get_field("id");
  312. if (id_str != null) {
  313. var id = int.parse(id_str);
  314. todo_store.toggle(id);
  315. }
  316. return new HttpStringResult("", (StatusCode)302)
  317. .set_header("Location", "/");
  318. }
  319. }
  320. // Delete todo endpoint
  321. class DeleteTodoEndpoint : Object, Endpoint {
  322. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  323. FormData form_data = yield FormDataParser.parse(
  324. context.request.request_body,
  325. context.request.content_type
  326. );
  327. var id_str = form_data.get_field("id");
  328. if (id_str != null) {
  329. var id = int.parse(id_str);
  330. todo_store.remove(id);
  331. }
  332. return new HttpStringResult("", (StatusCode)302)
  333. .set_header("Location", "/");
  334. }
  335. }
  336. // API endpoint that returns JSON representation of todos
  337. class TodoJsonEndpoint : Object, Endpoint {
  338. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  339. var json_parts = new Series<string>();
  340. json_parts.add("{\"todos\": [");
  341. bool first = true;
  342. todo_store.all().iterate((item) => {
  343. if (!first) json_parts.add(",");
  344. var completed_str = item.completed ? "true" : "false";
  345. var escaped_title = item.title.replace("\"", "\\\"");
  346. json_parts.add(@"{\"id\":$(item.id),\"title\":\"$escaped_title\",\"completed\":$completed_str}");
  347. first = false;
  348. });
  349. json_parts.add("]}");
  350. var json = json_parts.to_immutable_buffer()
  351. .aggregate<string>("", (acc, s) => acc + s);
  352. return new HttpStringResult(json)
  353. .set_header("Content-Type", "application/json");
  354. }
  355. }
  356. // XPath demo endpoint - shows how to use XPath selectors
  357. class XPathDemoEndpoint : Object, Endpoint {
  358. public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
  359. // Create a sample document
  360. var doc = new MarkupDocument.from_string("""
  361. <!DOCTYPE html>
  362. <html>
  363. <body>
  364. <div id="header">
  365. <h1>Welcome</h1>
  366. <nav class="menu">
  367. <a href="/" class="link active">Home</a>
  368. <a href="/about" class="link">About</a>
  369. </nav>
  370. </div>
  371. <div id="content">
  372. <article class="post featured">
  373. <h2>First Post</h2>
  374. <p class="intro">This is the introduction.</p>
  375. </article>
  376. <article class="post">
  377. <h2>Second Post</h2>
  378. <p class="intro">Another introduction.</p>
  379. </article>
  380. </div>
  381. </body>
  382. </html>
  383. """);
  384. // Build a response showing various XPath queries
  385. var result_doc = new MarkupDocument.from_string("""
  386. <!DOCTYPE html>
  387. <html>
  388. <head>
  389. <meta charset="UTF-8"/>
  390. <title>XPath Demo</title>
  391. <style>
  392. body { font-family: monospace; max-width: 900px; margin: 20px auto; padding: 20px; }
  393. .query { background: #f5f5f5; padding: 15px; margin: 10px 0; border-radius: 4px; }
  394. .xpath { color: #d73a49; font-weight: bold; }
  395. .result { background: #e8f5e9; padding: 10px; margin-top: 10px; border-radius: 4px; }
  396. pre { margin: 0; white-space: pre-wrap; }
  397. a { color: #2196F3; }
  398. </style>
  399. </head>
  400. <body></body>
  401. </html>
  402. """);
  403. var body = result_doc.body;
  404. var h1 = body.append_child_with_text("h1", "XPath Selector Demo");
  405. var back = body.append_child_element("p");
  406. var back_link = back.append_child_element("a");
  407. back_link.set_attribute("href", "/");
  408. back_link.append_text("โ† Back to Todo List");
  409. // Query 1: Select by ID
  410. add_xpath_demo(result_doc, body,
  411. "Select element by ID",
  412. "//*[@id='header']",
  413. doc.select("//*[@id='header']"));
  414. // Query 2: Select by class
  415. add_xpath_demo(result_doc, body,
  416. "Select elements by class",
  417. "//*[contains(@class, 'post')]",
  418. doc.select("//*[contains(@class, 'post')]"));
  419. // Query 3: Select nested elements
  420. add_xpath_demo(result_doc, body,
  421. "Select all links in nav",
  422. "//nav/a",
  423. doc.select("//nav/a"));
  424. // Query 4: Select by multiple classes
  425. add_xpath_demo(result_doc, body,
  426. "Select featured posts",
  427. "//*[contains(@class, 'featured')]",
  428. doc.select("//*[contains(@class, 'featured')]"));
  429. // Query 5: Select h2 elements
  430. add_xpath_demo(result_doc, body,
  431. "Select all h2 headings",
  432. "//h2",
  433. doc.select("//h2"));
  434. return result_doc.to_result();
  435. }
  436. private void add_xpath_demo(MarkupDocument doc, MarkupNode body, string title, string xpath, Enumerable<MarkupNode> results) {
  437. var query_div = body.append_child_element("div");
  438. query_div.add_class("query");
  439. query_div.append_child_with_text("strong", title);
  440. var xpath_code = query_div.append_child_element("div");
  441. xpath_code.add_class("xpath");
  442. xpath_code.append_text(xpath);
  443. var result_div = query_div.append_child_element("div");
  444. result_div.add_class("result");
  445. var count = (int)results.to_immutable_buffer().count();
  446. result_div.append_text(@"Found $count element(s):");
  447. var pre = result_div.append_child_element("pre");
  448. if (count == 0) {
  449. pre.append_text("(no matches)");
  450. } else {
  451. var results_text = new Series<string>();
  452. foreach (var node in results) {
  453. results_text.add(@"<$(node.tag_name)>");
  454. }
  455. pre.append_text(results_text.to_immutable_buffer()
  456. .aggregate<string>("", (acc, s) => acc + (acc == "" ? "" : ", ") + s));
  457. }
  458. }
  459. }
  460. void main(string[] args) {
  461. int port = args.length > 1 ? int.parse(args[1]) : 8080;
  462. // Initialize the todo store
  463. todo_store = new TodoStore();
  464. print("โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—\n");
  465. print("โ•‘ Astralis DocumentBuilder Example โ•‘\n");
  466. print("โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ\n");
  467. print(@"โ•‘ Port: $port");
  468. for (int i = 0; i < 50 - port.to_string().length - 7; i++) print(" ");
  469. print(" โ•‘\n");
  470. print("โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ\n");
  471. print("โ•‘ Endpoints: โ•‘\n");
  472. print("โ•‘ / - Todo list (built with DocumentModel) โ•‘\n");
  473. print("โ•‘ /add - Add a todo item (POST) โ•‘\n");
  474. print("โ•‘ /toggle - Toggle todo completion (POST) โ•‘\n");
  475. print("โ•‘ /delete - Delete a todo item (POST) โ•‘\n");
  476. print("โ•‘ /api/todos - JSON API for todos โ•‘\n");
  477. print("โ•‘ /xpath - XPath selector demo โ•‘\n");
  478. print("โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n");
  479. print("\nPress Ctrl+C to stop the server\n\n");
  480. try {
  481. var application = new WebApplication(port);
  482. // Register compression components (optional, for better performance)
  483. application.container.register_singleton<GzipCompressor>(() => new GzipCompressor());
  484. application.container.register_singleton<ZstdCompressor>(() => new ZstdCompressor());
  485. application.container.register_singleton<BrotliCompressor>(() => new BrotliCompressor());
  486. // Register endpoints using the pattern from SimpleApi.vala
  487. application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
  488. application.add_endpoint<AddTodoEndpoint>(new EndpointRoute("/add"));
  489. application.add_endpoint<ToggleTodoEndpoint>(new EndpointRoute("/toggle"));
  490. application.add_endpoint<DeleteTodoEndpoint>(new EndpointRoute("/delete"));
  491. application.add_endpoint<TodoJsonEndpoint>(new EndpointRoute("/api/todos"));
  492. application.add_endpoint<XPathDemoEndpoint>(new EndpointRoute("/xpath"));
  493. application.run();
  494. } catch (Error e) {
  495. printerr("Error: %s\n", e.message);
  496. Process.exit(1);
  497. }
  498. }