Pārlūkot izejas kodu

refactor(markup)!: rename Html* classes to Markup* and split into modules

Rename document-related classes to use more generic Markup naming:
- HtmlDocument → MarkupDocument
- HtmlNode → MarkupNode
- HtmlNodeList → MarkupNodeList

Split the monolithic DocumentModel.vala into dedicated files:
- MarkupDocument.vala for document operations
- MarkupNode.vala for node manipulation
- MarkupError.vala for error definitions

Update examples to use new class names and add total_changes tracking
to AppState in DocumentBuilderTemplate.

BREAKING CHANGE: All Html* classes have been renamed to Markup* equivalents. Update imports from Astralis.Document and change HtmlDocument, HtmlNode, HtmlNodeList references to their Markup* counterparts.
Billy Barrow 1 nedēļu atpakaļ
vecāks
revīzija
dbb283acc7

+ 14 - 15
examples/DocumentBuilder.vala

@@ -1,12 +1,11 @@
 using Astralis;
-using Astralis.Document;
 using Invercargill;
 using Invercargill.DataStructures;
 
 /**
  * DocumentBuilder Example
  * 
- * Demonstrates using the DocumentModel classes (HtmlDocument, HtmlNode) to
+ * 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
@@ -111,9 +110,9 @@ class HomePageEndpoint : Object, Endpoint {
         return doc.to_result();
     }
     
-    private HtmlDocument create_base_document(string title) throws Error {
+    private MarkupDocument create_base_document(string title) throws Error {
         // Create document from string template
-        var doc = new HtmlDocument.from_string("""
+        var doc = new MarkupDocument.from_string("""
             <!DOCTYPE html>
             <html>
             <head>
@@ -159,7 +158,7 @@ class HomePageEndpoint : Object, Endpoint {
         return doc;
     }
     
-    private void add_header_section(HtmlDocument doc, HtmlNode body) {
+    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");
@@ -170,10 +169,10 @@ class HomePageEndpoint : Object, Endpoint {
         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");
+        code.append_text("MarkupDocument");
         desc.append_text(" and ");
         code = desc.append_child_element("code");
-        code.append_text("HtmlNode");
+        code.append_text("MarkupNode");
         desc.append_text(" classes from the DocumentModel. The entire page is built programmatically!");
         
         // Add stats
@@ -195,7 +194,7 @@ class HomePageEndpoint : Object, Endpoint {
         completed_label.add_class("stat-label");
     }
     
-    private void add_todo_list_section(HtmlDocument doc, HtmlNode body) {
+    private void add_todo_list_section(MarkupDocument doc, MarkupNode body) {
         var list_card = body.append_child_element("div");
         list_card.add_class("card");
         
@@ -255,7 +254,7 @@ class HomePageEndpoint : Object, Endpoint {
         }
     }
     
-    private void add_form_section(HtmlDocument doc, HtmlNode body) {
+    private void add_form_section(MarkupDocument doc, MarkupNode body) {
         var form_card = body.append_child_element("div");
         form_card.add_class("card");
         
@@ -281,7 +280,7 @@ class HomePageEndpoint : Object, Endpoint {
         submit.append_text("Add Task");
     }
     
-    private void add_features_section(HtmlDocument doc, HtmlNode body) {
+    private void add_features_section(MarkupDocument doc, MarkupNode body) {
         var features_card = body.append_child_element("div");
         features_card.add_class("card");
         
@@ -292,7 +291,7 @@ class HomePageEndpoint : Object, Endpoint {
         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);");
+        code1.append_text("var doc = new MarkupDocument.from_string(html_template);");
         
         // Feature 2: XPath selectors
         var f2 = features_card.append_child_element("div");
@@ -323,7 +322,7 @@ class HomePageEndpoint : Object, Endpoint {
         code5.append_text("return doc.to_result();  // Returns HtmlResult with correct Content-Type");
     }
     
-    private void add_footer(HtmlDocument doc, HtmlNode body) {
+    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;");
@@ -428,7 +427,7 @@ class TodoJsonEndpoint : Object, Endpoint {
 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("""
+        var doc = new MarkupDocument.from_string("""
             <!DOCTYPE html>
             <html>
             <body>
@@ -454,7 +453,7 @@ class XPathDemoEndpoint : Object, Endpoint {
         """);
         
         // Build a response showing various XPath queries
-        var result_doc = new HtmlDocument.from_string("""
+        var result_doc = new MarkupDocument.from_string("""
             <!DOCTYPE html>
             <html>
             <head>
@@ -515,7 +514,7 @@ class XPathDemoEndpoint : Object, Endpoint {
         return result_doc.to_result();
     }
     
-    private void add_xpath_demo(HtmlDocument doc, HtmlNode body, string title, string xpath, HtmlNodeList results) {
+    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");
         

+ 13 - 9
examples/DocumentBuilderTemplate.vala

@@ -1,5 +1,4 @@
 using Astralis;
-using Astralis.Document;
 using Invercargill;
 using Invercargill.DataStructures;
 
@@ -23,29 +22,34 @@ using Invercargill.DataStructures;
 // 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();
     }
@@ -254,7 +258,7 @@ private const string HTML_TEMPLATE = """
         <p>The server loads the HTML template and uses XPath selectors to find and modify elements:</p>
         
         <pre id="code-example">// Load the template
-var doc = new HtmlDocument.from_string(HTML_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']");
@@ -267,7 +271,7 @@ if (counter > 0) {
         
         <h3>DocumentModel Features Used:</h3>
         <ul class="feature-list" id="feature-list">
-            <li><code>HtmlDocument.from_string()</code> - Load HTML from template</li>
+            <li><code>MarkupDocument.from_string()</code> - Load HTML from template</li>
             <li><code>doc.select_one(xpath)</code> - Find single element by XPath</li>
             <li><code>doc.select(xpath)</code> - Find multiple elements</li>
             <li><code>element.text_content</code> - Get/set text content</li>
@@ -291,7 +295,7 @@ if (counter > 0) {
 class HomePageEndpoint : Object, Endpoint {
     public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
         // Load the HTML template
-        var doc = new HtmlDocument.from_string(HTML_TEMPLATE);
+        var doc = new MarkupDocument.from_string(HTML_TEMPLATE);
         
         // Update the counter value
         var counter_el = doc.select_one("//div[@id='counter-value']");
@@ -345,10 +349,10 @@ class HomePageEndpoint : Object, Endpoint {
             time_el.text_content = app_state.last_update.format("%H:%M:%S");
         }
         
-        // Update changes count (use absolute value of counter as "total changes")
+        // Update total changes count
         var changes_el = doc.select_one("//div[@id='changes-value']");
         if (changes_el != null) {
-            changes_el.text_content = app_state.counter.abs().to_string();
+            changes_el.text_content = app_state.total_changes.to_string();
         }
         
         return doc.to_result();
@@ -386,7 +390,7 @@ class ResetEndpoint : Object, Endpoint {
 class RawHtmlEndpoint : Object, Endpoint {
     public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
         // Load template without modifications
-        var doc = new HtmlDocument.from_string(HTML_TEMPLATE);
+        var doc = new MarkupDocument.from_string(HTML_TEMPLATE);
         
         // Add a notice at the top
         var body = doc.body;
@@ -406,7 +410,7 @@ class RawHtmlEndpoint : Object, Endpoint {
 // API endpoint that demonstrates modifying multiple elements at once
 class BulkUpdateEndpoint : Object, Endpoint {
     public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
-        var doc = new HtmlDocument.from_string(HTML_TEMPLATE);
+        var doc = new MarkupDocument.from_string(HTML_TEMPLATE);
         
         // Demonstrate selecting multiple elements
         var all_info_values = doc.select("//div[contains(@class, 'info-value')]");
@@ -428,7 +432,7 @@ class BulkUpdateEndpoint : Object, Endpoint {
         // Add explanation
         var desc = doc.select_one("//p[@id='description']");
         if (desc != null) {
-            desc.text_content = "This demonstrates HtmlNodeList operations: set_text_all(), add_class_all(), and set_attribute_all().";
+            desc.text_content = "This demonstrates MarkupNodeList operations: set_text_all(), add_class_all(), and set_attribute_all().";
         }
         
         return doc.to_result();

+ 1 - 1
examples/meson.build

@@ -82,7 +82,7 @@ executable('fast-resources',
     install: false
 )
 
-# DocumentBuilder Example - demonstrates HtmlDocument/HtmlNode for building dynamic HTML
+# DocumentBuilder Example - demonstrates MarkupDocument/MarkupNode for building dynamic HTML
 executable('document-builder',
     'DocumentBuilder.vala',
     dependencies: [astralis_dep, invercargill_dep],

+ 335 - 0
src/Markup/MarkupDocument.vala

@@ -0,0 +1,335 @@
+using Xml;
+using Invercargill;
+using Invercargill.DataStructures;
+
+namespace Astralis {
+   /// <summary>
+    /// Represents an HTML document that can be loaded, manipulated, and rendered
+    /// </summary>
+    public class MarkupDocument : GLib.Object {
+        private Xml.Doc* doc;
+
+        /// <summary>
+        /// Creates a new empty HTML document
+        /// </summary>
+        public MarkupDocument() {
+            doc = new Xml.Doc();
+            // Create basic HTML structure
+            var html = doc->new_node(null, "html");
+            doc->set_root_element(html);
+            
+            var head = html->new_child(null, "head");
+            var meta = head->new_child(null, "meta");
+            meta->set_prop("charset", "UTF-8");
+            head->new_child(null, "title");
+            
+            html->new_child(null, "body");
+        }
+
+        /// <summary>
+        /// Loads an HTML document from a file
+        /// </summary>
+        public MarkupDocument.from_file(string filepath) throws GLib.Error {
+            var file = File.new_for_path(filepath);
+            if (!file.query_exists()) {
+                throw new MarkupError.FILE_NOT_FOUND("File not found: %s".printf(filepath));
+            }
+            
+            uint8[] contents;
+            string etag_out;
+            file.load_contents(null, out contents, out etag_out);
+            
+            parse_html((string)contents);
+        }
+
+        /// <summary>
+        /// Loads an HTML document from a string
+        /// </summary>
+        public MarkupDocument.from_string(string html) throws GLib.Error {
+            parse_html(html);
+        }
+
+        private void parse_html(string html) throws GLib.Error {
+            // Use XML parser with HTML recovery options
+            // Note: libxml2's HTML parser is in a separate library
+            // For now, we'll use the XML parser with some tolerance
+            Parser.init();
+            
+            int options = (int)(ParserOption.RECOVER |
+                ParserOption.NOERROR |
+                ParserOption.NOWARNING |
+                ParserOption.NOBLANKS |
+                ParserOption.NONET);
+            
+            doc = Parser.read_memory(html, html.length, null, null, options);
+            
+            if (doc == null) {
+                // Try parsing as a fragment wrapped in a basic structure
+                string wrapped = "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"/></head><body>%s</body></html>".printf(html);
+                doc = Parser.read_memory(wrapped, wrapped.length, null, null, options);
+            }
+            
+            if (doc == null) {
+                throw new MarkupError.PARSE_ERROR("Failed to parse HTML document");
+            }
+        }
+
+        ~MarkupDocument() {
+            if (doc != null) {
+                delete doc;
+            }
+        }
+
+        /// <summary>
+        /// Gets the root html element of the document
+        /// </summary>
+        public MarkupNode? root {
+            owned get {
+                var root = doc->get_root_element();
+                return root != null ? new MarkupNode(this, root) : null;
+            }
+        }
+
+        /// <summary>
+        /// Gets the head element of the document
+        /// </summary>
+        public MarkupNode? head {
+            owned get {
+                var root = doc->get_root_element();
+                if (root == null) return null;
+                
+                for (var child = root->children; child != null; child = child->next) {
+                    if (child->name == "head") {
+                        return new MarkupNode(this, child);
+                    }
+                }
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Gets the body element of the document
+        /// </summary>
+        public MarkupNode? body {
+            owned get {
+                var root = doc->get_root_element();
+                if (root == null) return null;
+                
+                for (var child = root->children; child != null; child = child->next) {
+                    if (child->name == "body") {
+                        return new MarkupNode(this, child);
+                    }
+                }
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the document title
+        /// </summary>
+        public string? title {
+            owned get {
+                var head_node = head;
+                if (head_node == null) return null;
+                
+                for (var child = head_node.native->children; child != null; child = child->next) {
+                    if (child->name == "title") {
+                        return child->get_content();
+                    }
+                }
+                return null;
+            }
+            set {
+                var head_node = head;
+                if (head_node == null) return;
+                
+                // Find existing title or create one
+                for (var child = head_node.native->children; child != null; child = child->next) {
+                    if (child->name == "title") {
+                        if (value != null) {
+                            child->set_content(value);
+                        } else {
+                            child->unlink();
+                            delete child;
+                        }
+                        return;
+                    }
+                }
+                
+                if (value != null) {
+                    head_node.native->new_text_child(null, "title", value);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Selects elements using an XPath expression
+        /// </summary>
+        public MarkupNodeList select(string xpath) {
+            var context = new XPath.Context(doc);
+            var result = context.eval(xpath);
+            
+            var list = new MarkupNodeList(this);
+            
+            if (result != null && result->nodesetval != null) {
+                var nodeset = result->nodesetval;
+                int len = nodeset->length();
+                for (int i = 0; i < len; i++) {
+                    var node = nodeset->item(i);
+                    if (node != null) {
+                        list.add_node(node);
+                    }
+                }
+            }
+            
+            delete result;
+            return list;
+        }
+
+        /// <summary>
+        /// Selects a single element using an XPath expression, returns first match or null
+        /// </summary>
+        public MarkupNode? select_one(string xpath) {
+            var results = select(xpath);
+            return results.first;
+        }
+
+        /// <summary>
+        /// Gets an element by its ID
+        /// </summary>
+        public MarkupNode? get_element_by_id(string id) {
+            return select_one("//*[@id='%s']".printf(id));
+        }
+
+        /// <summary>
+        /// Gets all elements with a specific tag name
+        /// </summary>
+        public MarkupNodeList get_elements_by_tag_name(string tag_name) {
+            return select("//%s".printf(tag_name));
+        }
+
+        /// <summary>
+        /// Gets all elements with a specific class name
+        /// </summary>
+        public MarkupNodeList get_elements_by_class_name(string class_name) {
+            return select("//*[contains(concat(' ', normalize-space(@class), ' '), ' %s ')]".printf(class_name));
+        }
+
+        /// <summary>
+        /// Creates a new element with the given tag name
+        /// </summary>
+        public MarkupNode create_element(string tag_name) {
+            var node = doc->new_node(null, tag_name);
+            return new MarkupNode(this, node);
+        }
+
+        /// <summary>
+        /// Creates a new text node
+        /// </summary>
+        public Xml.Node* create_text_node(string text) {
+            return doc->new_text(text);
+        }
+
+        internal Xml.Node* import_node(Xml.Node* node, bool deep = true) {
+            return node->copy(deep ? 1 : 0);
+        }
+
+        /// <summary>
+        /// Converts the document to an HTML string
+        /// </summary>
+        public string to_html() {
+            string buffer;
+            doc->dump_memory(out buffer);
+            return buffer;
+        }
+
+        /// <summary>
+        /// Converts the document to a formatted HTML string with indentation
+        /// </summary>
+        public string to_pretty_html() {
+            string buffer;
+            int len;
+            doc->dump_memory_format(out buffer, out len, true);
+            return buffer;
+        }
+
+        /// <summary>
+        /// Saves the document to a file
+        /// </summary>
+        public void save(string filepath) throws GLib.Error {
+            doc->save_file_enc(filepath, "UTF-8");
+        }
+
+        /// <summary>
+        /// Creates an HttpResult from this document
+        /// </summary>
+        public HttpResult to_result(StatusCode status = StatusCode.OK) {
+            return new HttpStringResult(this.to_html(), status)
+                .set_header("Content-Type", "text/html; charset=UTF-8");
+        }
+
+        /// <summary>
+        /// Sets the text content of an element by ID
+        /// </summary>
+        public bool set_element_text(string id, string text) {
+            var element = get_element_by_id(id);
+            if (element == null) {
+                return false;
+            }
+            element.text_content = text;
+            return true;
+        }
+
+        /// <summary>
+        /// Sets an attribute on an element by ID
+        /// </summary>
+        public bool set_element_attribute(string id, string attr_name, string attr_value) {
+            var element = get_element_by_id(id);
+            if (element == null) {
+                return false;
+            }
+            element.set_attribute(attr_name, attr_value);
+            return true;
+        }
+
+        /// <summary>
+        /// Adds a class to an element by ID
+        /// </summary>
+        public bool add_element_class(string id, string class_name) {
+            var element = get_element_by_id(id);
+            if (element == null) {
+                return false;
+            }
+            element.add_class(class_name);
+            return true;
+        }
+
+        /// <summary>
+        /// Removes a class from an element by ID
+        /// </summary>
+        public bool remove_element_class(string id, string class_name) {
+            var element = get_element_by_id(id);
+            if (element == null) {
+                return false;
+            }
+            element.remove_class(class_name);
+            return true;
+        }
+
+        /// <summary>
+        /// Sets the inner HTML of an element by ID
+        /// </summary>
+        public bool set_element_html(string id, string html) {
+            var element = get_element_by_id(id);
+            if (element == null) {
+                return false;
+            }
+            element.inner_html = html;
+            return true;
+        }
+
+        internal Xml.Doc* native_doc {
+            get { return doc; }
+        }
+    }
+}

+ 18 - 0
src/Markup/MarkupError.vala

@@ -0,0 +1,18 @@
+using Xml;
+using Invercargill;
+using Invercargill.DataStructures;
+
+namespace Astralis {
+
+    /// <summary>
+    /// Error domain for HTML document operations
+    /// </summary>
+    public errordomain MarkupError {
+        PARSE_ERROR,
+        FILE_NOT_FOUND,
+        INVALID_HTML,
+        NODE_NOT_FOUND,
+        INVALID_OPERATION
+    }
+
+}

+ 39 - 406
src/Document/DocumentModel.vala → src/Markup/MarkupNode.vala

@@ -2,27 +2,15 @@ using Xml;
 using Invercargill;
 using Invercargill.DataStructures;
 
-namespace Astralis.Document {
-
-    /// <summary>
-    /// Error domain for HTML document operations
-    /// </summary>
-    public errordomain HtmlError {
-        PARSE_ERROR,
-        FILE_NOT_FOUND,
-        INVALID_HTML,
-        NODE_NOT_FOUND,
-        INVALID_OPERATION
-    }
-
+namespace Astralis {
     /// <summary>
     /// Represents an HTML element node in the DOM with convenient manipulation methods
     /// </summary>
-    public class HtmlNode : GLib.Object {
+    public class MarkupNode : GLib.Object {
         private Xml.Node* xml_node;
-        private HtmlDocument document;
+        private MarkupDocument document;
 
-        internal HtmlNode(HtmlDocument doc, Xml.Node* node) {
+        internal MarkupNode(MarkupDocument doc, Xml.Node* node) {
             this.document = doc;
             this.xml_node = node;
         }
@@ -183,62 +171,62 @@ namespace Astralis.Document {
         /// <summary>
         /// Gets the parent element, or null if this is the root
         /// </summary>
-        public HtmlNode? parent {
+        public MarkupNode? parent {
             owned get {
                 var parent_node = xml_node->parent;
                 if (parent_node == null || parent_node->type == ElementType.DOCUMENT_NODE) {
                     return null;
                 }
-                return new HtmlNode(document, parent_node);
+                return new MarkupNode(document, parent_node);
             }
         }
 
         /// <summary>
         /// Gets the first child element of this node
         /// </summary>
-        public HtmlNode? first_element_child {
+        public MarkupNode? first_element_child {
             owned get {
                 var child = xml_node->first_element_child();
-                return child != null ? new HtmlNode(document, child) : null;
+                return child != null ? new MarkupNode(document, child) : null;
             }
         }
 
         /// <summary>
         /// Gets the last child element of this node
         /// </summary>
-        public HtmlNode? last_element_child {
+        public MarkupNode? last_element_child {
             owned get {
                 var child = xml_node->last_element_child();
-                return child != null ? new HtmlNode(document, child) : null;
+                return child != null ? new MarkupNode(document, child) : null;
             }
         }
 
         /// <summary>
         /// Gets the next sibling element
         /// </summary>
-        public HtmlNode? next_element_sibling {
+        public MarkupNode? next_element_sibling {
             owned get {
                 var sibling = xml_node->next_element_sibling();
-                return sibling != null ? new HtmlNode(document, sibling) : null;
+                return sibling != null ? new MarkupNode(document, sibling) : null;
             }
         }
 
         /// <summary>
         /// Gets the previous sibling element
         /// </summary>
-        public HtmlNode? previous_element_sibling {
+        public MarkupNode? previous_element_sibling {
             owned get {
                 var sibling = xml_node->previous_element_sibling();
-                return sibling != null ? new HtmlNode(document, sibling) : null;
+                return sibling != null ? new MarkupNode(document, sibling) : null;
             }
         }
 
         /// <summary>
         /// Gets all child elements of this node
         /// </summary>
-        public HtmlNodeList children {
+        public MarkupNodeList children {
             owned get {
-                var list = new HtmlNodeList(document);
+                var list = new MarkupNodeList(document);
                 for (var child = xml_node->children; child != null; child = child->next) {
                     if (child->type == ElementType.ELEMENT_NODE) {
                         list.add_node(child);
@@ -251,9 +239,9 @@ namespace Astralis.Document {
         /// <summary>
         /// Appends a new child element with the given tag name
         /// </summary>
-        public HtmlNode append_child_element(string tag_name) {
+        public MarkupNode append_child_element(string tag_name) {
             var new_node = xml_node->new_child(null, tag_name);
-            return new HtmlNode(document, new_node);
+            return new MarkupNode(document, new_node);
         }
 
         /// <summary>
@@ -266,9 +254,9 @@ namespace Astralis.Document {
         /// <summary>
         /// Creates and appends a child element with text content
         /// </summary>
-        public HtmlNode append_child_with_text(string tag_name, string text) {
+        public MarkupNode append_child_with_text(string tag_name, string text) {
             var new_node = xml_node->new_text_child(null, tag_name, text);
-            return new HtmlNode(document, new_node);
+            return new MarkupNode(document, new_node);
         }
 
         /// <summary>
@@ -363,7 +351,7 @@ namespace Astralis.Document {
             }
         }
 
-        internal HtmlDocument doc {
+        internal MarkupDocument doc {
             get { return document; }
         }
 
@@ -372,14 +360,15 @@ namespace Astralis.Document {
         }
     }
 
+
     /// <summary>
     /// A list of HTML nodes supporting iteration and manipulation
     /// </summary>
-    public class HtmlNodeList : GLib.Object {
-        private HtmlDocument document;
+    public class MarkupNodeList : GLib.Object {
+        private MarkupDocument document;
         private List<Xml.Node*> nodes;
 
-        internal HtmlNodeList(HtmlDocument doc) {
+        internal MarkupNodeList(MarkupDocument doc) {
             this.document = doc;
             this.nodes = new List<Xml.Node*>();
         }
@@ -398,37 +387,37 @@ namespace Astralis.Document {
         /// <summary>
         /// Gets a node at the specified index
         /// </summary>
-        public new HtmlNode? get(int index) {
+        public new MarkupNode? get(int index) {
             unowned List<Xml.Node*> item = nodes.nth((uint)index);
             if (item == null) {
                 return null;
             }
-            return new HtmlNode(document, item.data);
+            return new MarkupNode(document, item.data);
         }
 
         /// <summary>
         /// Gets the first node, or null if empty
         /// </summary>
-        public HtmlNode? first {
+        public MarkupNode? first {
             owned get {
                 if (nodes.is_empty()) {
                     return null;
                 }
                 unowned List<Xml.Node*> item = nodes.first();
-                return item != null ? new HtmlNode(document, item.data) : null;
+                return item != null ? new MarkupNode(document, item.data) : null;
             }
         }
 
         /// <summary>
         /// Gets the last node, or null if empty
         /// </summary>
-        public HtmlNode? last {
+        public MarkupNode? last {
             owned get {
                 if (nodes.is_empty()) {
                     return null;
                 }
                 unowned List<Xml.Node*> item = nodes.last();
-                return item != null ? new HtmlNode(document, item.data) : null;
+                return item != null ? new MarkupNode(document, item.data) : null;
             }
         }
 
@@ -446,7 +435,7 @@ namespace Astralis.Document {
         /// </summary>
         public void add_class_all(string class_name) {
             foreach (var node in nodes) {
-                var wrapper = new HtmlNode(document, node);
+                var wrapper = new MarkupNode(document, node);
                 wrapper.add_class(class_name);
             }
         }
@@ -456,7 +445,7 @@ namespace Astralis.Document {
         /// </summary>
         public void remove_class_all(string class_name) {
             foreach (var node in nodes) {
-                var wrapper = new HtmlNode(document, node);
+                var wrapper = new MarkupNode(document, node);
                 wrapper.remove_class(class_name);
             }
         }
@@ -489,14 +478,14 @@ namespace Astralis.Document {
         }
 
         /// <summary>
-        /// Enumerator for iterating over HtmlNodeList
+        /// Enumerator for iterating over MarkupNodeList
         /// </summary>
         public class Enumerator {
-            private HtmlDocument document;
+            private MarkupDocument document;
             private unowned List<Xml.Node*> nodes;
             private unowned List<Xml.Node*>? current;
 
-            internal Enumerator(HtmlDocument doc, List<Xml.Node*> nodes) {
+            internal Enumerator(MarkupDocument doc, List<Xml.Node*> nodes) {
                 this.document = doc;
                 this.nodes = nodes;
                 this.current = null;
@@ -511,11 +500,11 @@ namespace Astralis.Document {
                 return current != null;
             }
 
-            public HtmlNode? get() {
+            public MarkupNode? get() {
                 if (current == null) {
                     return null;
                 }
-                return new HtmlNode(document, current.data);
+                return new MarkupNode(document, current.data);
             }
 
             public void reset() {
@@ -523,360 +512,4 @@ namespace Astralis.Document {
             }
         }
     }
-
-    /// <summary>
-    /// Represents an HTML document that can be loaded, manipulated, and rendered
-    /// </summary>
-    public class HtmlDocument : GLib.Object {
-        private Xml.Doc* doc;
-
-        /// <summary>
-        /// Creates a new empty HTML document
-        /// </summary>
-        public HtmlDocument() {
-            doc = new Xml.Doc();
-            // Create basic HTML structure
-            var html = doc->new_node(null, "html");
-            doc->set_root_element(html);
-            
-            var head = html->new_child(null, "head");
-            var meta = head->new_child(null, "meta");
-            meta->set_prop("charset", "UTF-8");
-            head->new_child(null, "title");
-            
-            html->new_child(null, "body");
-        }
-
-        /// <summary>
-        /// Loads an HTML document from a file
-        /// </summary>
-        public HtmlDocument.from_file(string filepath) throws GLib.Error {
-            var file = File.new_for_path(filepath);
-            if (!file.query_exists()) {
-                throw new HtmlError.FILE_NOT_FOUND("File not found: %s".printf(filepath));
-            }
-            
-            uint8[] contents;
-            string etag_out;
-            file.load_contents(null, out contents, out etag_out);
-            
-            parse_html((string)contents);
-        }
-
-        /// <summary>
-        /// Loads an HTML document from a string
-        /// </summary>
-        public HtmlDocument.from_string(string html) throws GLib.Error {
-            parse_html(html);
-        }
-
-        private void parse_html(string html) throws GLib.Error {
-            // Use XML parser with HTML recovery options
-            // Note: libxml2's HTML parser is in a separate library
-            // For now, we'll use the XML parser with some tolerance
-            Parser.init();
-            
-            int options = (int)(ParserOption.RECOVER |
-                ParserOption.NOERROR |
-                ParserOption.NOWARNING |
-                ParserOption.NOBLANKS |
-                ParserOption.NONET);
-            
-            doc = Parser.read_memory(html, html.length, null, null, options);
-            
-            if (doc == null) {
-                // Try parsing as a fragment wrapped in a basic structure
-                string wrapped = "<!DOCTYPE html><html><head><meta charset=\"UTF-8\"/></head><body>%s</body></html>".printf(html);
-                doc = Parser.read_memory(wrapped, wrapped.length, null, null, options);
-            }
-            
-            if (doc == null) {
-                throw new HtmlError.PARSE_ERROR("Failed to parse HTML document");
-            }
-        }
-
-        ~HtmlDocument() {
-            if (doc != null) {
-                delete doc;
-            }
-        }
-
-        /// <summary>
-        /// Gets the root html element of the document
-        /// </summary>
-        public HtmlNode? root {
-            owned get {
-                var root = doc->get_root_element();
-                return root != null ? new HtmlNode(this, root) : null;
-            }
-        }
-
-        /// <summary>
-        /// Gets the head element of the document
-        /// </summary>
-        public HtmlNode? head {
-            owned get {
-                var root = doc->get_root_element();
-                if (root == null) return null;
-                
-                for (var child = root->children; child != null; child = child->next) {
-                    if (child->name == "head") {
-                        return new HtmlNode(this, child);
-                    }
-                }
-                return null;
-            }
-        }
-
-        /// <summary>
-        /// Gets the body element of the document
-        /// </summary>
-        public HtmlNode? body {
-            owned get {
-                var root = doc->get_root_element();
-                if (root == null) return null;
-                
-                for (var child = root->children; child != null; child = child->next) {
-                    if (child->name == "body") {
-                        return new HtmlNode(this, child);
-                    }
-                }
-                return null;
-            }
-        }
-
-        /// <summary>
-        /// Gets or sets the document title
-        /// </summary>
-        public string? title {
-            owned get {
-                var head_node = head;
-                if (head_node == null) return null;
-                
-                for (var child = head_node.native->children; child != null; child = child->next) {
-                    if (child->name == "title") {
-                        return child->get_content();
-                    }
-                }
-                return null;
-            }
-            set {
-                var head_node = head;
-                if (head_node == null) return;
-                
-                // Find existing title or create one
-                for (var child = head_node.native->children; child != null; child = child->next) {
-                    if (child->name == "title") {
-                        if (value != null) {
-                            child->set_content(value);
-                        } else {
-                            child->unlink();
-                            delete child;
-                        }
-                        return;
-                    }
-                }
-                
-                if (value != null) {
-                    head_node.native->new_text_child(null, "title", value);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Selects elements using an XPath expression
-        /// </summary>
-        public HtmlNodeList select(string xpath) {
-            var context = new XPath.Context(doc);
-            var result = context.eval(xpath);
-            
-            var list = new HtmlNodeList(this);
-            
-            if (result != null && result->nodesetval != null) {
-                var nodeset = result->nodesetval;
-                int len = nodeset->length();
-                for (int i = 0; i < len; i++) {
-                    var node = nodeset->item(i);
-                    if (node != null) {
-                        list.add_node(node);
-                    }
-                }
-            }
-            
-            delete result;
-            return list;
-        }
-
-        /// <summary>
-        /// Selects a single element using an XPath expression, returns first match or null
-        /// </summary>
-        public HtmlNode? select_one(string xpath) {
-            var results = select(xpath);
-            return results.first;
-        }
-
-        /// <summary>
-        /// Gets an element by its ID
-        /// </summary>
-        public HtmlNode? get_element_by_id(string id) {
-            return select_one("//*[@id='%s']".printf(id));
-        }
-
-        /// <summary>
-        /// Gets all elements with a specific tag name
-        /// </summary>
-        public HtmlNodeList get_elements_by_tag_name(string tag_name) {
-            return select("//%s".printf(tag_name));
-        }
-
-        /// <summary>
-        /// Gets all elements with a specific class name
-        /// </summary>
-        public HtmlNodeList get_elements_by_class_name(string class_name) {
-            return select("//*[contains(concat(' ', normalize-space(@class), ' '), ' %s ')]".printf(class_name));
-        }
-
-        /// <summary>
-        /// Creates a new element with the given tag name
-        /// </summary>
-        public HtmlNode create_element(string tag_name) {
-            var node = doc->new_node(null, tag_name);
-            return new HtmlNode(this, node);
-        }
-
-        /// <summary>
-        /// Creates a new text node
-        /// </summary>
-        public Xml.Node* create_text_node(string text) {
-            return doc->new_text(text);
-        }
-
-        internal Xml.Node* import_node(Xml.Node* node, bool deep = true) {
-            return node->copy(deep ? 1 : 0);
-        }
-
-        /// <summary>
-        /// Converts the document to an HTML string
-        /// </summary>
-        public string to_html() {
-            string buffer;
-            doc->dump_memory(out buffer);
-            return buffer;
-        }
-
-        /// <summary>
-        /// Converts the document to a formatted HTML string with indentation
-        /// </summary>
-        public string to_pretty_html() {
-            string buffer;
-            int len;
-            doc->dump_memory_format(out buffer, out len, true);
-            return buffer;
-        }
-
-        /// <summary>
-        /// Saves the document to a file
-        /// </summary>
-        public void save(string filepath) throws GLib.Error {
-            doc->save_file_enc(filepath, "UTF-8");
-        }
-
-        /// <summary>
-        /// Creates an HttpResult from this document
-        /// </summary>
-        public HtmlResult to_result(StatusCode status = StatusCode.OK) {
-            return new HtmlResult(this, status);
-        }
-
-        /// <summary>
-        /// Sets the text content of an element by ID
-        /// </summary>
-        public bool set_element_text(string id, string text) {
-            var element = get_element_by_id(id);
-            if (element == null) {
-                return false;
-            }
-            element.text_content = text;
-            return true;
-        }
-
-        /// <summary>
-        /// Sets an attribute on an element by ID
-        /// </summary>
-        public bool set_element_attribute(string id, string attr_name, string attr_value) {
-            var element = get_element_by_id(id);
-            if (element == null) {
-                return false;
-            }
-            element.set_attribute(attr_name, attr_value);
-            return true;
-        }
-
-        /// <summary>
-        /// Adds a class to an element by ID
-        /// </summary>
-        public bool add_element_class(string id, string class_name) {
-            var element = get_element_by_id(id);
-            if (element == null) {
-                return false;
-            }
-            element.add_class(class_name);
-            return true;
-        }
-
-        /// <summary>
-        /// Removes a class from an element by ID
-        /// </summary>
-        public bool remove_element_class(string id, string class_name) {
-            var element = get_element_by_id(id);
-            if (element == null) {
-                return false;
-            }
-            element.remove_class(class_name);
-            return true;
-        }
-
-        /// <summary>
-        /// Sets the inner HTML of an element by ID
-        /// </summary>
-        public bool set_element_html(string id, string html) {
-            var element = get_element_by_id(id);
-            if (element == null) {
-                return false;
-            }
-            element.inner_html = html;
-            return true;
-        }
-
-        internal Xml.Doc* native_doc {
-            get { return doc; }
-        }
-    }
-
-    /// <summary>
-    /// An HttpResult that renders an HTML document
-    /// </summary>
-    public class HtmlResult : HttpResult {
-        private HtmlDocument document;
-
-        internal HtmlResult(HtmlDocument doc, StatusCode status = StatusCode.OK) {
-            base(status);
-            this.document = doc;
-            set_header("Content-Type", "text/html; charset=UTF-8");
-        }
-
-        /// <summary>
-        /// The HTML document being served
-        /// </summary>
-        public HtmlDocument html_document {
-            get { return document; }
-        }
-
-        public async override void send_body(AsyncOutput output) throws GLib.Error {
-            var html = document.to_html();
-            var bytes = new ByteBuffer.from_byte_array(html.data);
-            content_length = bytes.length;
-            yield output.write(bytes);
-        }
-    }
-}
+}

+ 3 - 1
src/meson.build

@@ -20,7 +20,9 @@ sources = files(
     'Server/ResponseContext.vala',
     'Server/ServerInput.vala',
     'Server/ServerOutput.vala',
-    'Document/DocumentModel.vala',
+    'Markup/MarkupDocument.vala',
+    'Markup/MarkupNode.vala',
+    'Markup/MarkupError.vala',
 )
 
 libastralis = shared_library('astralis',