using Astralis; using Invercargill; using Invercargill.DataStructures; /** * DocumentBuilder Example * * Demonstrates using the DocumentModel classes (MarkupDocument, MarkupNode) to * programmatically build and manipulate HTML documents. Shows how to: * - Create HTML documents from scratch or from templates * - Use XPath selectors to find elements * - Manipulate DOM elements (add/remove classes, attributes, content) * - Handle form POST submissions with dynamic document updates * * This example references patterns from: * - SimpleApi.vala: Basic endpoint structure * - FastResources.vala: Content type handling and response building * * Usage: document-builder [port] * * Examples: * document-builder * document-builder 8080 */ // Simple task model for our todo list class TodoItem : Object { public int id { get; set; } public string title { get; set; } public bool completed { get; set; } public TodoItem(int id, string title, bool completed = false) { this.id = id; this.title = title; this.completed = completed; } } // In-memory todo store class TodoStore : Object { private Series items = new Series(); private int next_id = 1; public TodoStore() { // Add some initial items add("Learn Astralis DocumentModel"); add("Build dynamic HTML pages"); add("Handle form submissions"); } public void add(string title) { items.add(new TodoItem(next_id++, title)); } public void toggle(int id) { items.to_immutable_buffer().iterate((item) => { if (item.id == id) { item.completed = !item.completed; } }); } public void remove(int id) { var new_items = items.to_immutable_buffer() .where(item => item.id != id) .to_series(); items = new_items; } public ImmutableBuffer all() { return items.to_immutable_buffer(); } public int count() { return (int)items.to_immutable_buffer().count(); } public int completed_count() { return (int)items.to_immutable_buffer() .count(item => item.completed); } } // Main application with todo store TodoStore todo_store; // Home page endpoint - builds HTML document programmatically class HomePageEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { var doc = create_base_document("Document Builder Example"); // Get the body element var body = doc.body; // Add a header section add_header_section(doc, body); // Add the todo list section add_todo_list_section(doc, body); // Add the add todo form add_form_section(doc, body); // Add a section showing DocumentModel features add_features_section(doc, body); // Add footer add_footer(doc, body); return doc.to_result(); } private MarkupDocument create_base_document(string title) throws Error { // Create document from string template var doc = new MarkupDocument.from_string(""" """); // Set the title using the document's title property doc.title = title; return doc; } private void add_header_section(MarkupDocument doc, MarkupNode body) { // Create header card var header_card = body.append_child_element("div"); header_card.add_class("card"); var h1 = header_card.append_child_with_text("h1", "📄 Document Builder Example"); // Add description paragraph var desc = header_card.append_child_element("p"); desc.append_text("This page demonstrates the "); var code = desc.append_child_element("code"); code.append_text("MarkupDocument"); desc.append_text(" and "); code = desc.append_child_element("code"); code.append_text("MarkupNode"); desc.append_text(" classes from the DocumentModel. The entire page is built programmatically!"); // Add stats var stats_div = header_card.append_child_element("div"); stats_div.add_class("stats"); var total_stat = stats_div.append_child_element("div"); total_stat.add_class("stat"); var total_value = total_stat.append_child_with_text("div", todo_store.count().to_string()); total_value.add_class("stat-value"); var total_label = total_stat.append_child_with_text("div", "Total Tasks"); total_label.add_class("stat-label"); var completed_stat = stats_div.append_child_element("div"); completed_stat.add_class("stat"); var completed_value = completed_stat.append_child_with_text("div", todo_store.completed_count().to_string()); completed_value.add_class("stat-value"); var completed_label = completed_stat.append_child_with_text("div", "Completed"); completed_label.add_class("stat-label"); } private void add_todo_list_section(MarkupDocument doc, MarkupNode body) { var list_card = body.append_child_element("div"); list_card.add_class("card"); var h2 = list_card.append_child_with_text("h2", "Todo List"); var todos = todo_store.all(); var count = todo_store.count(); if (count == 0) { var empty = list_card.append_child_element("div"); empty.add_class("empty"); empty.append_text("No tasks yet! Add one below."); } else { todos.iterate((item) => { var item_div = list_card.append_child_element("div"); item_div.add_class("todo-item"); if (item.completed) { item_div.add_class("completed"); } // Toggle form var toggle_form = item_div.append_child_element("form"); toggle_form.set_attribute("method", "POST"); toggle_form.set_attribute("action", "/toggle"); toggle_form.set_attribute("style", "display: inline;"); var hidden_id = toggle_form.append_child_element("input"); hidden_id.set_attribute("type", "hidden"); hidden_id.set_attribute("name", "id"); hidden_id.set_attribute("value", item.id.to_string()); var toggle_btn = toggle_form.append_child_element("button"); toggle_btn.add_class("btn-toggle"); toggle_btn.set_attribute("type", "submit"); toggle_btn.append_text(item.completed ? "â†Šī¸ Undo" : "✓ Done"); // Title span var title_span = item_div.append_child_with_text("span", item.title); title_span.add_class("title"); // Delete form var delete_form = item_div.append_child_element("form"); delete_form.set_attribute("method", "POST"); delete_form.set_attribute("action", "/delete"); delete_form.set_attribute("style", "display: inline;"); var delete_hidden = delete_form.append_child_element("input"); delete_hidden.set_attribute("type", "hidden"); delete_hidden.set_attribute("name", "id"); delete_hidden.set_attribute("value", item.id.to_string()); var delete_btn = delete_form.append_child_element("button"); delete_btn.add_class("btn-delete"); delete_btn.set_attribute("type", "submit"); delete_btn.append_text("đŸ—‘ī¸ Delete"); }); } } private void add_form_section(MarkupDocument doc, MarkupNode body) { var form_card = body.append_child_element("div"); form_card.add_class("card"); var h2 = form_card.append_child_with_text("h2", "Add New Task"); // Create form var form = form_card.append_child_element("form"); form.set_attribute("method", "POST"); form.set_attribute("action", "/add"); form.set_attribute("style", "display: flex; gap: 10px;"); // Text input var input = form.append_child_element("input"); input.set_attribute("type", "text"); input.set_attribute("name", "title"); input.set_attribute("placeholder", "Enter a new task..."); input.set_attribute("required", "required"); // Submit button var submit = form.append_child_element("button"); submit.add_class("btn-primary"); submit.set_attribute("type", "submit"); submit.append_text("Add Task"); } private void add_features_section(MarkupDocument doc, MarkupNode body) { var features_card = body.append_child_element("div"); features_card.add_class("card"); var h2 = features_card.append_child_with_text("h2", "DocumentModel Features Used"); // Feature 1: Creating documents var f1 = features_card.append_child_element("div"); f1.add_class("feature"); f1.append_child_with_text("strong", "Creating Documents:"); var code1 = f1.append_child_element("code"); code1.append_text("var doc = new MarkupDocument.from_string(html_template);"); // Feature 2: XPath selectors var f2 = features_card.append_child_element("div"); f2.add_class("feature"); f2.append_child_with_text("strong", "XPath Selectors:"); var code2 = f2.append_child_element("code"); code2.append_text("var element = doc.select_one(\"//div[@id='content']\");"); // Feature 3: DOM manipulation var f3 = features_card.append_child_element("div"); f3.add_class("feature"); f3.append_child_with_text("strong", "DOM Manipulation:"); var code3 = f3.append_child_element("code"); code3.append_text("element.add_class(\"highlight\"); element.set_attribute(\"data-id\", \"123\");"); // Feature 4: Building elements var f4 = features_card.append_child_element("div"); f4.add_class("feature"); f4.append_child_with_text("strong", "Building Elements:"); var code4 = f4.append_child_element("code"); code4.append_text("var child = parent.append_child_element(\"div\"); child.append_text(\"Hello!\");"); // Feature 5: Returning HTML var f5 = features_card.append_child_element("div"); f5.add_class("feature"); f5.append_child_with_text("strong", "Returning HTML Response:"); var code5 = f5.append_child_element("code"); code5.append_text("return doc.to_result(); // Returns HtmlResult with correct Content-Type"); } private void add_footer(MarkupDocument doc, MarkupNode body) { var footer_card = body.append_child_element("div"); footer_card.add_class("card"); footer_card.set_attribute("style", "text-align: center; color: #666;"); var p = footer_card.append_child_element("p"); p.append_text("Built with "); var code = p.append_child_element("code"); code.append_text("Astralis.Document"); p.append_text(" - See "); var link = p.append_child_element("a"); link.set_attribute("href", "https://github.com/example/astralis"); link.append_text("GitHub"); p.append_text(" for more examples."); } } // Add todo endpoint class AddTodoEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { // Parse form data FormData form_data = yield FormDataParser.parse( context.request.request_body, context.request.content_type ); var title = form_data.get_field("title"); if (title != null && title.strip() != "") { todo_store.add(title.strip()); } // Redirect back to home (302 Found) return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // Toggle todo endpoint class ToggleTodoEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { FormData form_data = yield FormDataParser.parse( context.request.request_body, context.request.content_type ); var id_str = form_data.get_field("id"); if (id_str != null) { var id = int.parse(id_str); todo_store.toggle(id); } return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // Delete todo endpoint class DeleteTodoEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { FormData form_data = yield FormDataParser.parse( context.request.request_body, context.request.content_type ); var id_str = form_data.get_field("id"); if (id_str != null) { var id = int.parse(id_str); todo_store.remove(id); } return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // API endpoint that returns JSON representation of todos class TodoJsonEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { var json_parts = new Series(); json_parts.add("{\"todos\": ["); bool first = true; todo_store.all().iterate((item) => { if (!first) json_parts.add(","); var completed_str = item.completed ? "true" : "false"; var escaped_title = item.title.replace("\"", "\\\""); json_parts.add(@"{\"id\":$(item.id),\"title\":\"$escaped_title\",\"completed\":$completed_str}"); first = false; }); json_parts.add("]}"); var json = json_parts.to_immutable_buffer() .aggregate("", (acc, s) => acc + s); return new HttpStringResult(json) .set_header("Content-Type", "application/json"); } } // XPath demo endpoint - shows how to use XPath selectors class XPathDemoEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { // Create a sample document var doc = new MarkupDocument.from_string("""

First Post

This is the introduction.

Second Post

Another introduction.

"""); // Build a response showing various XPath queries var result_doc = new MarkupDocument.from_string(""" XPath Demo """); var body = result_doc.body; var h1 = body.append_child_with_text("h1", "XPath Selector Demo"); var back = body.append_child_element("p"); var back_link = back.append_child_element("a"); back_link.set_attribute("href", "/"); back_link.append_text("← Back to Todo List"); // Query 1: Select by ID add_xpath_demo(result_doc, body, "Select element by ID", "//*[@id='header']", doc.select("//*[@id='header']")); // Query 2: Select by class add_xpath_demo(result_doc, body, "Select elements by class", "//*[contains(@class, 'post')]", doc.select("//*[contains(@class, 'post')]")); // Query 3: Select nested elements add_xpath_demo(result_doc, body, "Select all links in nav", "//nav/a", doc.select("//nav/a")); // Query 4: Select by multiple classes add_xpath_demo(result_doc, body, "Select featured posts", "//*[contains(@class, 'featured')]", doc.select("//*[contains(@class, 'featured')]")); // Query 5: Select h2 elements add_xpath_demo(result_doc, body, "Select all h2 headings", "//h2", doc.select("//h2")); return result_doc.to_result(); } private void add_xpath_demo(MarkupDocument doc, MarkupNode body, string title, string xpath, MarkupNodeList results) { var query_div = body.append_child_element("div"); query_div.add_class("query"); query_div.append_child_with_text("strong", title); var xpath_code = query_div.append_child_element("div"); xpath_code.add_class("xpath"); xpath_code.append_text(xpath); var result_div = query_div.append_child_element("div"); result_div.add_class("result"); var count = results.length; result_div.append_text(@"Found $count element(s):"); var pre = result_div.append_child_element("pre"); if (count == 0) { pre.append_text("(no matches)"); } else { var results_text = new Series(); var enumerator = results.iterator(); while (enumerator.move_next()) { var node = enumerator.get(); if (node != null) { results_text.add(@"<$(node.tag_name)>"); } } pre.append_text(results_text.to_immutable_buffer() .aggregate("", (acc, s) => acc + (acc == "" ? "" : ", ") + s)); } } } void main(string[] args) { int port = args.length > 1 ? int.parse(args[1]) : 8080; // Initialize the todo store todo_store = new TodoStore(); print("╔══════════════════════════════════════════════════════════════╗\n"); print("║ Astralis DocumentBuilder Example ║\n"); print("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•Ŗ\n"); print(@"║ Port: $port"); for (int i = 0; i < 50 - port.to_string().length - 7; i++) print(" "); print(" ║\n"); print("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•Ŗ\n"); print("║ Endpoints: ║\n"); print("║ / - Todo list (built with DocumentModel) ║\n"); print("║ /add - Add a todo item (POST) ║\n"); print("║ /toggle - Toggle todo completion (POST) ║\n"); print("║ /delete - Delete a todo item (POST) ║\n"); print("║ /api/todos - JSON API for todos ║\n"); print("║ /xpath - XPath selector demo ║\n"); print("╚══════════════════════════════════════════════════════════════╝\n"); print("\nPress Ctrl+C to stop the server\n\n"); try { var application = new WebApplication(port); // Register compression components (optional, for better performance) application.container.register_singleton(() => new GzipCompressor()); application.container.register_singleton(() => new ZstdCompressor()); application.container.register_singleton(() => new BrotliCompressor()); // Register endpoints using the pattern from SimpleApi.vala application.add_endpoint(new EndpointRoute("/")); application.add_endpoint(new EndpointRoute("/add")); application.add_endpoint(new EndpointRoute("/toggle")); application.add_endpoint(new EndpointRoute("/delete")); application.add_endpoint(new EndpointRoute("/api/todos")); application.add_endpoint(new EndpointRoute("/xpath")); application.run(); } catch (Error e) { printerr("Error: %s\n", e.message); Process.exit(1); } }