Переглянути джерело

refactor(markup): replace MarkupNodeList with lazy enumerables

Replace the in-memory MarkupNodeList class with lazy enumerable
implementations (XPathResultEnumerable, ChildNodesEnumerable) that
iterate directly over underlying XML structures without buffering.

- Remove MarkupNodeList class and its bulk operation methods
- Add XPathResultEnumerable for lazy XPath result iteration
- Add ChildNodesEnumerable for lazy child node iteration
- Update select(), get_elements_by_tag_name(), get_elements_by_class_name()
  to return Enumerable<MarkupNode>
- Add has_attribute() method and make set_attribute() support null values
- Update examples to use foreach iteration instead of bulk operations

BREAKING CHANGE: MarkupNodeList class removed along with set_text_all(),
add_class_all(), remove_class_all(), set_attribute_all(), and remove_all()
methods. Use foreach iteration to perform operations on multiple nodes.
Billy Barrow 1 тиждень тому
батько
коміт
e17992d5c8

+ 2 - 2
examples/DocumentBuilder.vala

@@ -514,7 +514,7 @@ class XPathDemoEndpoint : Object, Endpoint {
         return result_doc.to_result();
     }
     
-    private void add_xpath_demo(MarkupDocument doc, MarkupNode body, string title, string xpath, MarkupNodeList results) {
+    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");
         
@@ -527,7 +527,7 @@ class XPathDemoEndpoint : Object, Endpoint {
         var result_div = query_div.append_child_element("div");
         result_div.add_class("result");
         
-        var count = results.length;
+        var count = (int)results.to_immutable_buffer().count();
         result_div.append_text(@"Found $count element(s):");
         
         var pre = result_div.append_child_element("pre");

+ 9 - 5
examples/DocumentBuilderTemplate.vala

@@ -439,16 +439,20 @@ class BulkUpdateEndpoint : Object, Endpoint {
     public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
         var doc = template.new_instance();
         
-        // Demonstrate selecting multiple elements
+        // Demonstrate selecting multiple elements and iterating
         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");
+        foreach (var node in all_info_values) {
+            node.text_content = "BULK UPDATED";
+            node.add_class("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");
+        foreach (var card in all_cards) {
+            card.set_attribute("data-bulk-update", "true");
+        }
         
         // Update title to indicate bulk operation
         var title = doc.select_one("//h1[@id='page-title']");
@@ -459,7 +463,7 @@ class BulkUpdateEndpoint : Object, Endpoint {
         // 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().";
+            desc.text_content = "This demonstrates Enumerable<MarkupNode> iteration for bulk operations.";
         }
         
         return doc.to_result();

+ 75 - 0
src/Markup/ChildNodesEnumerable.vala

@@ -0,0 +1,75 @@
+using Xml;
+using Invercargill;
+
+namespace Astralis {
+
+    /// <summary>
+    /// Enumerable for iterating over child element nodes of a MarkupNode.
+    /// This is a private implementation that lazily iterates over the children.
+    /// </summary>
+    internal class ChildNodesEnumerable : Enumerable<MarkupNode> {
+        private weak MarkupDocument document;
+        private Xml.Node* parent_node;
+
+        internal ChildNodesEnumerable(MarkupDocument doc, Xml.Node* parent) {
+            this.document = doc;
+            this.parent_node = parent;
+        }
+
+        public override EnumerableInfo get_info() {
+            return new EnumerableInfo.infer_ultimate(this, EnumerableCategory.IN_MEMORY);
+        }
+
+        public override Tracker<MarkupNode> get_tracker() {
+            return new ChildNodesTracker(this);
+        }
+
+        public override uint? peek_count() {
+            // Count requires full iteration for linked lists
+            return null;
+        }
+
+        /// <summary>
+        /// Tracker for iterating over child nodes
+        /// </summary>
+        private class ChildNodesTracker : Tracker<MarkupNode> {
+            private ChildNodesEnumerable enumerable;
+            private Xml.Node* current_child;
+            private Xml.Node* next_element;
+
+            internal ChildNodesTracker(ChildNodesEnumerable enumerable) {
+                this.enumerable = enumerable;
+                this.current_child = null;
+                this.next_element = null;
+                
+                // Find the first element child
+                for (var child = enumerable.parent_node->children; child != null; child = child->next) {
+                    if (child->type == ElementType.ELEMENT_NODE) {
+                        this.next_element = child;
+                        break;
+                    }
+                }
+            }
+
+            public override MarkupNode get_next() {
+                assert(next_element != null);
+                current_child = next_element;
+                
+                // Find the next element sibling for subsequent calls
+                next_element = null;
+                for (var child = current_child->next; child != null; child = child->next) {
+                    if (child->type == ElementType.ELEMENT_NODE) {
+                        next_element = child;
+                        break;
+                    }
+                }
+                
+                return new MarkupNode(enumerable.document, current_child);
+            }
+
+            public override bool has_next() {
+                return next_element != null;
+            }
+        }
+    }
+}

+ 14 - 20
src/Markup/MarkupDocument.vala

@@ -147,6 +147,12 @@ namespace Astralis {
             }
         }
 
+        public Enumerable<MarkupNode> nodes {
+            owned get {
+                return select("//*");
+            }
+        }
+
         /// <summary>
         /// Gets or sets the document title
         /// </summary>
@@ -192,25 +198,10 @@ namespace Astralis {
         /// <summary>
         /// Selects elements using an XPath expression
         /// </summary>
-        public MarkupNodeList select(string xpath) {
+        public Enumerable<MarkupNode> 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;
+            return new XPathResultEnumerable(this, (owned)result);
         }
 
         /// <summary>
@@ -218,7 +209,10 @@ namespace Astralis {
         /// </summary>
         public MarkupNode? select_one(string xpath) {
             var results = select(xpath);
-            return results.first;
+            foreach (var node in results) {
+                return node;
+            }
+            return null;
         }
 
         /// <summary>
@@ -231,14 +225,14 @@ namespace Astralis {
         /// <summary>
         /// Gets all elements with a specific tag name
         /// </summary>
-        public MarkupNodeList get_elements_by_tag_name(string tag_name) {
+        public Enumerable<MarkupNode> 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) {
+        public Enumerable<MarkupNode> get_elements_by_class_name(string class_name) {
             return select("//*[contains(concat(' ', normalize-space(@class), ' '), ' %s ')]".printf(class_name));
         }
 

+ 22 - 190
src/Markup/MarkupNode.vala

@@ -42,10 +42,27 @@ namespace Astralis {
         }
 
         /// <summary>
-        /// Sets an attribute value
+        /// Checks if this element has a specific attribute
         /// </summary>
-        public void set_attribute(string name, string value) {
-            xml_node->set_prop(name, value);
+        /// <param name="name">The attribute name to check</param>
+        /// <returns>True if the attribute exists, false otherwise</returns>
+        public bool has_attribute(string name) {
+            return xml_node->get_prop(name) != null;
+        }
+
+        /// <summary>
+        /// Sets an attribute value. If value is null, the attribute is removed.
+        /// For boolean attributes (e.g., "disabled", "checked"), pass null to remove 
+        /// or use the attribute name as the value to set.
+        /// </summary>
+        /// <param name="name">The attribute name</param>
+        /// <param name="value">The attribute value, or null to remove the attribute</param>
+        public void set_attribute(string name, string? value) {
+            if (value == null) {
+                xml_node->unset_prop(name);
+            } else {
+                xml_node->set_prop(name, value);
+            }
             document.update(this);
         }
 
@@ -386,15 +403,9 @@ namespace Astralis {
         /// <summary>
         /// Gets all child elements of this node
         /// </summary>
-        public MarkupNodeList children {
+        public Enumerable<MarkupNode> children {
             owned get {
-                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);
-                    }
-                }
-                return list;
+                return new ChildNodesEnumerable(document, xml_node);
             }
         }
 
@@ -645,183 +656,4 @@ namespace Astralis {
             get { return xml_node; }
         }
     }
-
-
-    /// <summary>
-    /// A list of HTML nodes supporting iteration and manipulation
-    /// </summary>
-    public class MarkupNodeList : Invercargill.Enumerable<MarkupNode> {
-        private MarkupDocument document;
-        private List<Xml.Node*> nodes;
-
-        internal MarkupNodeList(MarkupDocument doc) {
-            this.document = doc;
-            this.nodes = new List<Xml.Node*>();
-        }
-
-        internal void add_node(Xml.Node* node) {
-            nodes.append(node);
-        }
-
-        /// <summary>
-        /// Number of nodes in the list
-        /// </summary>
-        public int length {
-            get { return (int)nodes.length(); }
-        }
-
-        /// <summary>
-        /// Gets a node at the specified index
-        /// </summary>
-        public new MarkupNode? get(int index) {
-            unowned List<Xml.Node*> item = nodes.nth((uint)index);
-            if (item == null) {
-                return null;
-            }
-            return new MarkupNode(document, item.data);
-        }
-
-        /// <summary>
-        /// Gets the first node, or null if empty
-        /// </summary>
-        public new MarkupNode? first {
-            owned get {
-                if (nodes.is_empty()) {
-                    return null;
-                }
-                unowned List<Xml.Node*> item = nodes.first();
-                return item != null ? new MarkupNode(document, item.data) : null;
-            }
-        }
-
-        /// <summary>
-        /// Gets the last node, or null if empty
-        /// </summary>
-        public new MarkupNode? last {
-            owned get {
-                if (nodes.is_empty()) {
-                    return null;
-                }
-                unowned List<Xml.Node*> item = nodes.last();
-                return item != null ? new MarkupNode(document, item.data) : null;
-            }
-        }
-
-        /// <summary>
-        /// Sets a property on all nodes in the list
-        /// </summary>
-        public void set_attribute_all(string name, string value) {
-            foreach (var node in nodes) {
-                node->set_prop(name, value);
-                document.update(new MarkupNode(document, node));
-            }
-        }
-
-        /// <summary>
-        /// Adds a class to all nodes in the list
-        /// </summary>
-        public void add_class_all(string class_name) {
-            foreach (var node in nodes) {
-                var wrapper = new MarkupNode(document, node);
-                wrapper.add_class(class_name);
-                // Note: add_class already emits update signal
-            }
-        }
-
-        /// <summary>
-        /// Removes a class from all nodes in the list
-        /// </summary>
-        public void remove_class_all(string class_name) {
-            foreach (var node in nodes) {
-                var wrapper = new MarkupNode(document, node);
-                wrapper.remove_class(class_name);
-                // Note: remove_class already emits update signal
-            }
-        }
-
-        /// <summary>
-        /// Sets text content on all nodes in the list
-        /// </summary>
-        public void set_text_all(string text) {
-            foreach (var node in nodes) {
-                node->set_content(text);
-                document.update(new MarkupNode(document, node));
-            }
-        }
-
-        /// <summary>
-        /// Removes all nodes in the list from the DOM
-        /// </summary>
-        public void remove_all() {
-            // Collect parent nodes before removal for update signals
-            var parents = new List<Xml.Node*>();
-            foreach (var node in nodes) {
-                var parent = node->parent;
-                if (parent != null && parent->type != ElementType.DOCUMENT_NODE) {
-                    parents.append(parent);
-                }
-                node->unlink();
-                delete node;
-            }
-            nodes = new List<Xml.Node*>();
-            
-            // Emit update for all affected parents
-            foreach (var parent in parents) {
-                document.update(new MarkupNode(document, parent));
-            }
-        }
-
-        /// <summary>
-        /// Gets information about this enumerable
-        /// </summary>
-        public override Invercargill.EnumerableInfo get_info() {
-            return new Invercargill.EnumerableInfo.infer_ultimate(this, Invercargill.EnumerableCategory.IN_MEMORY);
-        }
-
-        /// <summary>
-        /// Gets a tracker for iterating over the nodes
-        /// </summary>
-        public override Invercargill.Tracker<MarkupNode> get_tracker() {
-            return new NodeTracker(this);
-        }
-
-        /// <summary>
-        /// Peeks at the count without full enumeration
-        /// </summary>
-        public override uint? peek_count() {
-            return (uint)nodes.length();
-        }
-
-        /// <summary>
-        /// Tracker for iterating over MarkupNodeList
-        /// </summary>
-        private class NodeTracker : Invercargill.Tracker<MarkupNode> {
-            private MarkupNodeList list;
-            private unowned List<Xml.Node*>? current;
-
-            internal NodeTracker(owned MarkupNodeList list) {
-                this.list = list;
-                this.current = null;
-            }
-
-            public override MarkupNode get_next() {
-                if (current == null) {
-                    current = list.nodes.first();
-                } else {
-                    current = current.next;
-                }
-                // has_next() should be called before get_next()
-                // Returning null here would violate the non-nullable contract
-                assert(current != null);
-                return new MarkupNode(list.document, current.data);
-            }
-
-            public override bool has_next() {
-                if (current == null) {
-                    return !list.nodes.is_empty();
-                }
-                return current.next != null;
-            }
-        }
-    }
 }

+ 71 - 0
src/Markup/XPathResultEnumerable.vala

@@ -0,0 +1,71 @@
+using Xml;
+using Invercargill;
+
+namespace Astralis {
+
+    /// <summary>
+    /// Enumerable for iterating over XPath query results.
+    /// This is a private implementation that lazily iterates over the nodeset.
+    /// </summary>
+    internal class XPathResultEnumerable : Enumerable<MarkupNode> {
+        private weak MarkupDocument document;
+        private XPath.Object* xpath_result;
+
+        internal XPathResultEnumerable(MarkupDocument doc, owned XPath.Object* result) {
+            this.document = doc;
+            this.xpath_result = (owned)result;
+        }
+
+        ~XPathResultEnumerable() {
+            if (xpath_result != null) {
+                delete xpath_result;
+            }
+        }
+
+        public override EnumerableInfo get_info() {
+            return new EnumerableInfo.infer_ultimate(this, EnumerableCategory.IN_MEMORY);
+        }
+
+        public override Tracker<MarkupNode> get_tracker() {
+            return new XPathResultTracker(this);
+        }
+
+        public override uint? peek_count() {
+            if (xpath_result == null || xpath_result->nodesetval == null) {
+                return 0;
+            }
+            return (uint)xpath_result->nodesetval->length();
+        }
+
+        /// <summary>
+        /// Tracker for iterating over XPath results
+        /// </summary>
+        private class XPathResultTracker : Tracker<MarkupNode> {
+            private XPathResultEnumerable enumerable;
+            private int current_index;
+            private int count;
+
+            internal XPathResultTracker(XPathResultEnumerable enumerable) {
+                this.enumerable = enumerable;
+                this.current_index = -1;
+                
+                if (enumerable.xpath_result != null && enumerable.xpath_result->nodesetval != null) {
+                    this.count = enumerable.xpath_result->nodesetval->length();
+                } else {
+                    this.count = 0;
+                }
+            }
+
+            public override MarkupNode get_next() {
+                current_index++;
+                var node = enumerable.xpath_result->nodesetval->item(current_index);
+                assert(node != null);
+                return new MarkupNode(enumerable.document, node);
+            }
+
+            public override bool has_next() {
+                return (current_index + 1) < count;
+            }
+        }
+    }
+}

+ 2 - 0
src/meson.build

@@ -26,6 +26,8 @@ sources = files(
     'Markup/MarkupNode.vala',
     'Markup/MarkupError.vala',
     'Markup/MarkupTemplate.vala',
+    'Markup/XPathResultEnumerable.vala',
+    'Markup/ChildNodesEnumerable.vala',
 )