DocumentBuilder.vala 25 KB

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