| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- 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<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 MarkupDocument create_base_document(string title) throws Error {
- // Create document from string template
- var doc = new MarkupDocument.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(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<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 MarkupDocument.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 MarkupDocument.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(MarkupDocument doc, MarkupNode body, string title, string xpath, Enumerable<MarkupNode> 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 = (int)results.to_immutable_buffer().count();
- 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>();
- foreach (var node in results) {
- 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);
- }
- }
|