using Astralis; using Invercargill; using Invercargill.DataStructures; using Inversion; /** * DocumentBuilderTemplate Example * * Demonstrates loading an existing HTML template and modifying elements * using the DocumentModel classes. Shows how to: * - Define a MarkupTemplate subclass with embedded HTML * - Register the template as a singleton with WebApplication * - Inject and use the template in endpoints * - Use XPath selectors to find specific elements * - Modify element content, attributes, and classes * - Handle form POST to update the document state * * This example complements DocumentBuilder.vala by showing template-based * document manipulation rather than building from scratch. * * Usage: document-builder-template [port] */ // Application state - a simple counter class AppState : Object { public int counter { get; set; } public int total_changes { get; set; } public string last_action { get; set; } public DateTime last_update { get; set; } public AppState() { counter = 0; total_changes = 0; last_action = "Initialized"; last_update = new DateTime.now_local(); } public void increment() { counter++; total_changes++; last_action = "Incremented"; last_update = new DateTime.now_local(); } public void decrement() { counter--; total_changes++; last_action = "Decremented"; last_update = new DateTime.now_local(); } public void reset() { counter = 0; total_changes++; last_action = "Reset"; last_update = new DateTime.now_local(); } } /** * CSS content for the counter page. * Served as a FastResource for optimal performance with pre-compression. */ private const string COUNTER_CSS = """ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 700px; margin: 0 auto; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } .card { background: white; border-radius: 12px; padding: 25px; margin: 15px 0; box-shadow: 0 10px 40px rgba(0,0,0,0.2); } h1 { color: #333; margin-top: 0; } .counter-display { text-align: center; padding: 30px; background: #f8f9fa; border-radius: 8px; margin: 20px 0; } .counter-value { font-size: 72px; font-weight: bold; color: #667eea; line-height: 1; } .counter-label { color: #666; font-size: 14px; text-transform: uppercase; letter-spacing: 2px; } .button-group { display: flex; gap: 10px; justify-content: center; margin: 20px 0; } button { padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600; transition: all 0.2s; } button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .btn-primary { background: #667eea; color: white; } .btn-primary:hover { background: #5a6fd6; } .btn-danger { background: #e74c3c; color: white; } .btn-danger:hover { background: #c0392b; } .btn-success { background: #2ecc71; color: white; } .btn-success:hover { background: #27ae60; } .info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin: 20px 0; } .info-item { background: #f8f9fa; padding: 15px; border-radius: 6px; text-align: center; } .info-label { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 1px; } .info-value { font-size: 18px; font-weight: 600; color: #333; margin-top: 5px; } .status-positive { color: #2ecc71 !important; } .status-negative { color: #e74c3c !important; } .status-zero { color: #666 !important; } code { background: #e8e8e8; padding: 2px 6px; border-radius: 4px; font-size: 14px; } pre { background: #263238; color: #aed581; padding: 15px; border-radius: 6px; overflow-x: auto; font-size: 13px; } .feature-list { margin: 0; padding-left: 20px; } .feature-list li { margin: 8px 0; color: #555; } a { color: #667eea; text-decoration: none; } a:hover { text-decoration: underline; } """; /** * CounterTemplate - A cached HTML template for the counter page. * * This class extends MarkupTemplate to provide a reusable, cached template. * The HTML is parsed once and cached; new_instance() returns efficient copies. * CSS is served separately via CounterStyles FastResource for optimal caching. */ class CounterTemplate : MarkupTemplate { /// /// Returns the HTML markup for this template. /// CSS is linked externally via /styles.css for better caching. /// protected override string markup { get { return """ Document Template Example

📄 Template Document Builder

This page demonstrates loading an HTML template and modifying it dynamically.

Current Value
0
Status
Zero
Last Action
Initialized
Last Update
--:--:--
Total Changes
0

How It Works

The server loads the HTML template and uses XPath selectors to find and modify elements:

// Load the template
var doc = new MarkupDocument.from_string(HTML_TEMPLATE);

// Find and modify elements using XPath
var counter_el = doc.select_one("//div[@id='counter-value']");
counter_el.text_content = counter.to_string();

// Add/remove classes based on state
if (counter > 0) {
    counter_el.add_class("status-positive");
}

DocumentModel Features Used:

"""; }} } // Main page endpoint - uses field initializer injection class HomePageEndpoint : Object, Endpoint { // Field initializer injection - template is injected by the DI container private CounterTemplate template = Inversion.inject(); public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { // Get a copy of the cached template var doc = template.new_instance(); // Update the counter value var counter_el = doc.select_one("//div[@id='counter-value']"); if (counter_el != null) { counter_el.text_content = app_state.counter.to_string(); // Update status class based on counter value counter_el.remove_class("status-positive"); counter_el.remove_class("status-negative"); counter_el.remove_class("status-zero"); if (app_state.counter > 0) { counter_el.add_class("status-positive"); } else if (app_state.counter < 0) { counter_el.add_class("status-negative"); } else { counter_el.add_class("status-zero"); } } // Update status text var status_el = doc.select_one("//div[@id='status-value']"); if (status_el != null) { if (app_state.counter > 0) { status_el.text_content = "Positive"; status_el.remove_class("status-negative"); status_el.remove_class("status-zero"); status_el.add_class("status-positive"); } else if (app_state.counter < 0) { status_el.text_content = "Negative"; status_el.remove_class("status-positive"); status_el.remove_class("status-zero"); status_el.add_class("status-negative"); } else { status_el.text_content = "Zero"; status_el.remove_class("status-positive"); status_el.remove_class("status-negative"); status_el.add_class("status-zero"); } } // Update last action var action_el = doc.select_one("//div[@id='action-value']"); if (action_el != null) { action_el.text_content = app_state.last_action; } // Update timestamp var time_el = doc.select_one("//div[@id='time-value']"); if (time_el != null) { time_el.text_content = app_state.last_update.format("%H:%M:%S"); } // Update total changes count var changes_el = doc.select_one("//div[@id='changes-value']"); if (changes_el != null) { changes_el.text_content = app_state.total_changes.to_string(); } return doc.to_result(); } } // Increment endpoint class IncrementEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { app_state.increment(); return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // Decrement endpoint class DecrementEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { app_state.decrement(); return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // Reset endpoint class ResetEndpoint : Object, Endpoint { public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { app_state.reset(); return new HttpStringResult("", (StatusCode)302) .set_header("Location", "/"); } } // Raw HTML endpoint - shows the unmodified template class RawHtmlEndpoint : Object, Endpoint { // Field initializer injection private CounterTemplate template = Inversion.inject(); public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { // Get a fresh copy of the template var doc = template.new_instance(); // Add a notice at the top var body = doc.body; if (body != null) { var notice = body.append_child_element("div"); notice.set_attribute("style", "background: #fff3cd; color: #856404; padding: 15px; margin: 10px 0; border-radius: 8px; border: 1px solid #ffc107;"); notice.append_text("⚠️ This is the raw template without dynamic modifications. "); var link = notice.append_child_element("a"); link.set_attribute("href", "/"); link.append_text("← Back to dynamic version"); } return doc.to_result(); } } // API endpoint that demonstrates modifying multiple elements at once class BulkUpdateEndpoint : Object, Endpoint { // Field initializer injection private CounterTemplate template = Inversion.inject(); public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error { var doc = template.new_instance(); // Demonstrate selecting multiple elements var all_info_values = doc.select("//div[contains(@class, 'info-value')]"); // Update all info values to show they were bulk-modified all_info_values.set_text_all("BULK UPDATED"); all_info_values.add_class_all("status-positive"); // Also demonstrate setting attributes on multiple elements var all_cards = doc.select("//div[contains(@class, 'card')]"); all_cards.set_attribute_all("data-bulk-update", "true"); // Update title to indicate bulk operation var title = doc.select_one("//h1[@id='page-title']"); if (title != null) { title.text_content = "📄 Bulk Update Demo"; } // Add explanation var desc = doc.select_one("//p[@id='description']"); if (desc != null) { desc.text_content = "This demonstrates MarkupNodeList operations: set_text_all(), add_class_all(), and set_attribute_all()."; } return doc.to_result(); } } // Global app state AppState app_state; void main(string[] args) { int port = args.length > 1 ? int.parse(args[1]) : 8080; // Initialize app state app_state = new AppState(); print("╔══════════════════════════════════════════════════════════════╗\n"); print("║ Astralis DocumentBuilderTemplate 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("║ / - Counter page (template with modifications) ║\n"); print("║ /styles.css - CSS stylesheet (FastResource) ║\n"); print("║ /increment - Increase counter (POST) ║\n"); print("║ /decrement - Decrease counter (POST) ║\n"); print("║ /reset - Reset counter (POST) ║\n"); print("║ /raw - Raw template (no modifications) ║\n"); print("║ /bulk - Bulk update demo ║\n"); print("╚══════════════════════════════════════════════════════════════╝\n"); print("\nPress Ctrl+C to stop the server\n\n"); try { var application = new WebApplication(port); // Register compression components application.use_compression(); // Register the template as a singleton - it will be parsed once and cached // Each request gets a copy via new_instance() // Endpoints use field initializer injection with Inversion.inject() application.add_startup(); // Register CSS as a FastResource - pre-compressed, cached in memory // This demonstrates serving static content efficiently with ETag caching application.add_startup_endpoint(new EndpointRoute("/styles.css"), () => { try { return new FastResource.from_string(COUNTER_CSS) .with_content_type("text/css; charset=utf-8") .with_default_compressors(); } catch (Error e) { error("Failed to create CSS resource: %s", e.message); } }); // Register endpoints application.add_endpoint(new EndpointRoute("/")); application.add_endpoint(new EndpointRoute("/increment")); application.add_endpoint(new EndpointRoute("/decrement")); application.add_endpoint(new EndpointRoute("/reset")); application.add_endpoint(new EndpointRoute("/raw")); application.add_endpoint(new EndpointRoute("/bulk")); application.run(); } catch (Error e) { printerr("Error: %s\n", e.message); Process.exit(1); } }