|
@@ -0,0 +1,597 @@
|
|
|
|
|
+using Astralis;
|
|
|
|
|
+using Astralis.Document;
|
|
|
|
|
+using Invercargill;
|
|
|
|
|
+using Invercargill.DataStructures;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * DocumentBuilder Example
|
|
|
|
|
+ *
|
|
|
|
|
+ * Demonstrates using the DocumentModel classes (HtmlDocument, HtmlNode) 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<TodoItem> items = new Series<TodoItem>();
|
|
|
|
|
+ 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<TodoItem> 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 HtmlDocument create_base_document(string title) throws Error {
|
|
|
|
|
+ // Create document from string template
|
|
|
|
|
+ var doc = new HtmlDocument.from_string("""
|
|
|
|
|
+ <!DOCTYPE html>
|
|
|
|
|
+ <html>
|
|
|
|
|
+ <head>
|
|
|
|
|
+ <meta charset="UTF-8"/>
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
|
|
|
+ <title></title>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
|
|
|
|
+ .card { background: white; border-radius: 8px; padding: 20px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
|
|
|
+ h1 { color: #333; margin-top: 0; }
|
|
|
|
|
+ h2 { color: #555; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
|
|
|
|
|
+ .todo-item { display: flex; align-items: center; padding: 10px; margin: 5px 0; background: #fafafa; border-radius: 4px; border-left: 4px solid #4CAF50; }
|
|
|
|
|
+ .todo-item.completed { border-left-color: #ccc; opacity: 0.7; }
|
|
|
|
|
+ .todo-item.completed .title { text-decoration: line-through; color: #888; }
|
|
|
|
|
+ .todo-item .title { flex: 1; margin: 0 10px; }
|
|
|
|
|
+ .todo-item form { margin: 0; }
|
|
|
|
|
+ .stats { display: flex; gap: 20px; margin: 10px 0; }
|
|
|
|
|
+ .stat { background: #e8f5e9; padding: 10px 20px; border-radius: 4px; }
|
|
|
|
|
+ .stat-value { font-size: 24px; font-weight: bold; color: #4CAF50; }
|
|
|
|
|
+ .stat-label { font-size: 12px; color: #666; }
|
|
|
|
|
+ input[type="text"] { padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1; }
|
|
|
|
|
+ button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
|
|
|
|
|
+ .btn-primary { background: #4CAF50; color: white; }
|
|
|
|
|
+ .btn-primary:hover { background: #45a049; }
|
|
|
|
|
+ .btn-toggle { background: #2196F3; color: white; padding: 5px 10px; font-size: 12px; }
|
|
|
|
|
+ .btn-delete { background: #f44336; color: white; padding: 5px 10px; font-size: 12px; }
|
|
|
|
|
+ code { background: #e8e8e8; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
|
|
|
|
|
+ pre { background: #263238; color: #aed581; padding: 15px; border-radius: 4px; overflow-x: auto; }
|
|
|
|
|
+ .feature { margin: 15px 0; }
|
|
|
|
|
+ .feature code { display: block; background: #f5f5f5; padding: 10px; margin: 5px 0; }
|
|
|
|
|
+ a { color: #2196F3; text-decoration: none; }
|
|
|
|
|
+ a:hover { text-decoration: underline; }
|
|
|
|
|
+ .empty { color: #999; font-style: italic; padding: 20px; text-align: center; }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ </head>
|
|
|
|
|
+ <body></body>
|
|
|
|
|
+ </html>
|
|
|
|
|
+ """);
|
|
|
|
|
+
|
|
|
|
|
+ // Set the title using the document's title property
|
|
|
|
|
+ doc.title = title;
|
|
|
|
|
+
|
|
|
|
|
+ return doc;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void add_header_section(HtmlDocument doc, HtmlNode 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("HtmlDocument");
|
|
|
|
|
+ desc.append_text(" and ");
|
|
|
|
|
+ code = desc.append_child_element("code");
|
|
|
|
|
+ code.append_text("HtmlNode");
|
|
|
|
|
+ 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(HtmlDocument doc, HtmlNode 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(HtmlDocument doc, HtmlNode 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(HtmlDocument doc, HtmlNode 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 HtmlDocument.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(HtmlDocument doc, HtmlNode 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<string>();
|
|
|
|
|
+ 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<string>("", (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 HtmlDocument.from_string("""
|
|
|
|
|
+ <!DOCTYPE html>
|
|
|
|
|
+ <html>
|
|
|
|
|
+ <body>
|
|
|
|
|
+ <div id="header">
|
|
|
|
|
+ <h1>Welcome</h1>
|
|
|
|
|
+ <nav class="menu">
|
|
|
|
|
+ <a href="/" class="link active">Home</a>
|
|
|
|
|
+ <a href="/about" class="link">About</a>
|
|
|
|
|
+ </nav>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="content">
|
|
|
|
|
+ <article class="post featured">
|
|
|
|
|
+ <h2>First Post</h2>
|
|
|
|
|
+ <p class="intro">This is the introduction.</p>
|
|
|
|
|
+ </article>
|
|
|
|
|
+ <article class="post">
|
|
|
|
|
+ <h2>Second Post</h2>
|
|
|
|
|
+ <p class="intro">Another introduction.</p>
|
|
|
|
|
+ </article>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </body>
|
|
|
|
|
+ </html>
|
|
|
|
|
+ """);
|
|
|
|
|
+
|
|
|
|
|
+ // Build a response showing various XPath queries
|
|
|
|
|
+ var result_doc = new HtmlDocument.from_string("""
|
|
|
|
|
+ <!DOCTYPE html>
|
|
|
|
|
+ <html>
|
|
|
|
|
+ <head>
|
|
|
|
|
+ <meta charset="UTF-8"/>
|
|
|
|
|
+ <title>XPath Demo</title>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ body { font-family: monospace; max-width: 900px; margin: 20px auto; padding: 20px; }
|
|
|
|
|
+ .query { background: #f5f5f5; padding: 15px; margin: 10px 0; border-radius: 4px; }
|
|
|
|
|
+ .xpath { color: #d73a49; font-weight: bold; }
|
|
|
|
|
+ .result { background: #e8f5e9; padding: 10px; margin-top: 10px; border-radius: 4px; }
|
|
|
|
|
+ pre { margin: 0; white-space: pre-wrap; }
|
|
|
|
|
+ a { color: #2196F3; }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ </head>
|
|
|
|
|
+ <body></body>
|
|
|
|
|
+ </html>
|
|
|
|
|
+ """);
|
|
|
|
|
+
|
|
|
|
|
+ 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(HtmlDocument doc, HtmlNode body, string title, string xpath, HtmlNodeList 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<string>();
|
|
|
|
|
+ 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<string>("", (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<GzipCompressor>(() => new GzipCompressor());
|
|
|
|
|
+ application.container.register_singleton<ZstdCompressor>(() => new ZstdCompressor());
|
|
|
|
|
+ application.container.register_singleton<BrotliCompressor>(() => new BrotliCompressor());
|
|
|
|
|
+
|
|
|
|
|
+ // Register endpoints using the pattern from SimpleApi.vala
|
|
|
|
|
+ application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
|
|
|
|
|
+ application.add_endpoint<AddTodoEndpoint>(new EndpointRoute("/add"));
|
|
|
|
|
+ application.add_endpoint<ToggleTodoEndpoint>(new EndpointRoute("/toggle"));
|
|
|
|
|
+ application.add_endpoint<DeleteTodoEndpoint>(new EndpointRoute("/delete"));
|
|
|
|
|
+ application.add_endpoint<TodoJsonEndpoint>(new EndpointRoute("/api/todos"));
|
|
|
|
|
+ application.add_endpoint<XPathDemoEndpoint>(new EndpointRoute("/xpath"));
|
|
|
|
|
+
|
|
|
|
|
+ application.run();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ printerr("Error: %s\n", e.message);
|
|
|
|
|
+ Process.exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|