Răsfoiți Sursa

feat(markup): add update signal and CSS style manipulation methods

Add change tracking to MarkupDocument and MarkupNode through a new update
signal that emits whenever the DOM is modified. This enables consumers to
react to document changes.

Changes to MarkupDocument:
- Add update signal that fires with the modified MarkupNode

Changes to MarkupNode:
- Emit update signal on all mutating operations (text, attributes, DOM structure)
- Add CSS style property manipulation methods (get_style, set_style, remove_style)
- Add style_string property and styles dictionary getter
- Track parent nodes for update signals on removal/replacement operations
Billy Barrow 1 săptămână în urmă
părinte
comite
7810325d3c
2 a modificat fișierele cu 225 adăugiri și 10 ștergeri
  1. 7 1
      src/Markup/MarkupDocument.vala
  2. 218 9
      src/Markup/MarkupNode.vala

+ 7 - 1
src/Markup/MarkupDocument.vala

@@ -10,6 +10,8 @@ namespace Astralis {
     public class MarkupDocument : GLib.Object {
         private Html.Doc* doc;
 
+        public signal void update(MarkupNode modified_tag);
+
         /// <summary>
         /// Creates a new empty HTML document
         /// </summary>
@@ -169,16 +171,20 @@ namespace Astralis {
                     if (child->name == "title") {
                         if (value != null) {
                             child->set_content(value);
+                            update(new MarkupNode(this, child));
                         } else {
+                            var title_node = new MarkupNode(this, child);
                             child->unlink();
                             delete child;
+                            update(title_node);
                         }
                         return;
                     }
                 }
                 
                 if (value != null) {
-                    head_node.native->new_text_child(null, "title", value);
+                    var new_title = head_node.native->new_text_child(null, "title", value);
+                    update(new MarkupNode(this, new_title));
                 }
             }
         }

+ 218 - 9
src/Markup/MarkupNode.vala

@@ -28,7 +28,10 @@ namespace Astralis {
         /// </summary>
         public string text_content {
             owned get { return xml_node->get_content() ?? ""; }
-            set { xml_node->set_content(value); }
+            set {
+                xml_node->set_content(value);
+                document.update(this);
+            }
         }
 
         /// <summary>
@@ -43,6 +46,7 @@ namespace Astralis {
         /// </summary>
         public void set_attribute(string name, string value) {
             xml_node->set_prop(name, value);
+            document.update(this);
         }
 
         /// <summary>
@@ -50,6 +54,7 @@ namespace Astralis {
         /// </summary>
         public void remove_attribute(string name) {
             xml_node->unset_prop(name);
+            document.update(this);
         }
 
         /// <summary>
@@ -119,6 +124,7 @@ namespace Astralis {
             }
             new_classes[current_classes.length] = class_name;
             set_attribute("class", string.joinv(" ", new_classes));
+            // Note: set_attribute already emits update signal
         }
 
         /// <summary>
@@ -148,6 +154,7 @@ namespace Astralis {
                 } else {
                     remove_attribute("class");
                 }
+                // Note: set_attribute/remove_attribute already emit update signal
             }
         }
 
@@ -160,6 +167,7 @@ namespace Astralis {
             } else {
                 add_class(class_name);
             }
+            // Note: remove_class/add_class already emit update signal
         }
 
         /// <summary>
@@ -167,6 +175,144 @@ namespace Astralis {
         /// </summary>
         public void set_classes(string[] new_classes) {
             set_attribute("class", string.joinv(" ", new_classes));
+            // Note: set_attribute already emits update signal
+        }
+
+        /// <summary>
+        /// Gets the style attribute as a string
+        /// </summary>
+        public string? style_string {
+            owned get { return get_attribute("style"); }
+            set {
+                if (value != null) {
+                    set_attribute("style", value);
+                } else {
+                    remove_attribute("style");
+                }
+            }
+        }
+
+        /// <summary>
+        /// Parses the style attribute and returns a dictionary of CSS property names to values
+        /// </summary>
+        public ReadOnlyAssociative<string, string> styles {
+            owned get {
+                var result = new Dictionary<string, string>();
+                var style_attr = get_attribute("style");
+                if (style_attr == null || style_attr.strip() == "") {
+                    return result;
+                }
+                
+                // Split by semicolons to get individual property: value pairs
+                var declarations = style_attr.split(";");
+                foreach (unowned var decl in declarations) {
+                    var trimmed = decl.strip();
+                    if (trimmed == "") {
+                        continue;
+                    }
+                    
+                    // Split by colon to separate property and value
+                    var parts = trimmed.split(":", 2);
+                    if (parts.length == 2) {
+                        var prop = parts[0].strip();
+                        var val = parts[1].strip();
+                        if (prop != "" && val != "") {
+                            result.set(prop, val);
+                        }
+                    }
+                }
+                return result;
+            }
+        }
+
+        /// <summary>
+        /// Gets a specific CSS style property value
+        /// </summary>
+        /// <param name="property">The CSS property name (e.g., "color", "background-color")</param>
+        /// <returns>The property value, or null if not set</returns>
+        public string? get_style(string property) {
+            var style_dict = styles;
+            string? result = null;
+            style_dict.try_get(property, out result);
+            return result;
+        }
+
+        /// <summary>
+        /// Checks if a specific CSS style property is set
+        /// </summary>
+        /// <param name="property">The CSS property name to check</param>
+        /// <returns>True if the property is set, false otherwise</returns>
+        public bool has_style(string property) {
+            var style_dict = styles;
+            string? unused = null;
+            return style_dict.try_get(property, out unused);
+        }
+
+        /// <summary>
+        /// Sets a CSS style property value
+        /// </summary>
+        /// <param name="property">The CSS property name (e.g., "color", "font-size")</param>
+        /// <param name="value">The property value (e.g., "red", "14px")</param>
+        public void set_style(string property, string value) {
+            var style_dict = styles.select_to_dictionary<string, string>(s => s.key, s => s.value);
+            style_dict.set(property, value);
+            rebuild_style_attribute(style_dict);
+        }
+
+        /// <summary>
+        /// Removes a CSS style property
+        /// </summary>
+        /// <param name="property">The CSS property name to remove</param>
+        public void remove_style(string property) {
+            var style_dict = styles;
+            string? existing = null;
+            if (!style_dict.try_get(property, out existing)) {
+                return;
+            }
+            // Rebuild without the property
+            var new_dict = new Dictionary<string, string>();
+            style_dict.to_immutable_buffer().iterate((kv) => {
+                if (kv.key != property) {
+                    new_dict.set(kv.key, kv.value);
+                }
+            });
+            rebuild_style_attribute(new_dict);
+        }
+
+        /// <summary>
+        /// Replaces all styles with a new set of styles
+        /// </summary>
+        /// <param name="new_styles">A dictionary of CSS property names to values</param>
+        public void set_styles(Enumerable<KeyValuePair<string, string>> new_styles) {
+            rebuild_style_attribute(new_styles);
+        }
+
+        /// <summary>
+        /// Helper method to rebuild the style attribute from a dictionary of properties
+        /// </summary>
+        private void rebuild_style_attribute(Enumerable<KeyValuePair<string, string>> style_dict) {
+            // Check if empty by trying to count via iteration
+            var builder = new StringBuilder();
+            bool first = true;
+            bool has_any = false;
+            
+            style_dict.to_immutable_buffer().iterate((kv) => {
+                has_any = true;
+                if (!first) {
+                    builder.append("; ");
+                }
+                builder.append(kv.key);
+                builder.append(": ");
+                builder.append(kv.value);
+                first = false;
+            });
+            
+            if (!has_any) {
+                remove_attribute("style");
+                return;
+            }
+            
+            set_attribute("style", builder.str);
         }
 
         /// <summary>
@@ -242,7 +388,9 @@ namespace Astralis {
         /// </summary>
         public MarkupNode append_child_element(string tag_name) {
             var new_node = xml_node->new_child(null, tag_name);
-            return new MarkupNode(document, new_node);
+            var result = new MarkupNode(document, new_node);
+            document.update(this);
+            return result;
         }
 
         /// <summary>
@@ -250,6 +398,7 @@ namespace Astralis {
         /// </summary>
         public void append_text(string text) {
             xml_node->add_content(text);
+            document.update(this);
         }
 
         /// <summary>
@@ -257,15 +406,25 @@ namespace Astralis {
         /// </summary>
         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 MarkupNode(document, new_node);
+            var result = new MarkupNode(document, new_node);
+            document.update(this);
+            return result;
         }
 
         /// <summary>
         /// Removes this element from the DOM
         /// </summary>
         public void remove() {
+            var parent = xml_node->parent;
+            MarkupNode? parent_wrapper = null;
+            if (parent != null && parent->type != ElementType.DOCUMENT_NODE) {
+                parent_wrapper = new MarkupNode(document, parent);
+            }
             xml_node->unlink();
             delete xml_node;
+            if (parent_wrapper != null) {
+                document.update(parent_wrapper);
+            }
         }
 
         /// <summary>
@@ -277,6 +436,7 @@ namespace Astralis {
                 child->unlink();
                 delete child;
             }
+            document.update(this);
         }
 
         /// <summary>
@@ -284,28 +444,50 @@ namespace Astralis {
         /// </summary>
         /// <param name="node">The node to replace this element with</param>
         public void replace_with_node(MarkupNode node) {
+            var parent = xml_node->parent;
+            MarkupNode? parent_wrapper = null;
+            if (parent != null && parent->type != ElementType.DOCUMENT_NODE) {
+                parent_wrapper = new MarkupNode(document, parent);
+            }
             // Insert the new node before this node
             xml_node->add_prev_sibling(node.native->copy(1));
             
             // Remove this node
-            remove();
+            xml_node->unlink();
+            delete xml_node;
+            
+            if (parent_wrapper != null) {
+                document.update(parent_wrapper);
+            }
         }
 
-        public void replace_with_nodes(Enumerable<MarkupNode> nodes) {           
+        public void replace_with_nodes(Enumerable<MarkupNode> nodes) {
+            var parent = xml_node->parent;
+            MarkupNode? parent_wrapper = null;
+            if (parent != null && parent->type != ElementType.DOCUMENT_NODE) {
+                parent_wrapper = new MarkupNode(document, parent);
+            }
             foreach(var node in nodes) {
                 xml_node->add_prev_sibling(node.native->copy(1));
             }
-            remove();
+            xml_node->unlink();
+            delete xml_node;
+            
+            if (parent_wrapper != null) {
+                document.update(parent_wrapper);
+            }
         }
 
         public void append_node(MarkupNode node) {
             xml_node->add_child(node.native->copy(1));
+            document.update(this);
         }
 
         public void append_nodes(Enumerable<MarkupNode> nodes) {
             foreach(var child in nodes) {
                 xml_node->add_child(child.native->copy(1));
             }
+            document.update(this);
         }
 
         /// <summary>
@@ -313,7 +495,7 @@ namespace Astralis {
         /// </summary>
         public string inner_html {
             owned set {
-                clear_children();
+                clear_children(); // Note: clear_children already emits update signal
                 if (value == null || value == "") {
                     return;
                 }
@@ -345,6 +527,7 @@ namespace Astralis {
                 }
                 
                 delete temp_doc;
+                document.update(this);
             }
             owned get {
                 // Use Html.Doc to serialize the children properly
@@ -404,8 +587,14 @@ namespace Astralis {
                     return;
                 }
 
-                // Import and insert children before this node
+                // Get parent before modification
                 var parent = xml_node->parent;
+                MarkupNode? parent_wrapper = null;
+                if (parent != null && parent->type != ElementType.DOCUMENT_NODE) {
+                    parent_wrapper = new MarkupNode(document, parent);
+                }
+
+                // Import and insert children before this node
                 if (parent != null) {
                     for (var child = temp_root->children; child != null; child = child->next) {
                         var imported = doc.import_node(child, true);
@@ -415,8 +604,13 @@ namespace Astralis {
                 }
                 
                 // Remove this node
-                remove();
+                xml_node->unlink();
+                delete xml_node;
                 delete temp_doc;
+                
+                if (parent_wrapper != null) {
+                    document.update(parent_wrapper);
+                }
             }
             owned get {
                 // Create a temporary HTML document to serialize this node
@@ -516,6 +710,7 @@ namespace Astralis {
         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));
             }
         }
 
@@ -526,6 +721,7 @@ namespace Astralis {
             foreach (var node in nodes) {
                 var wrapper = new MarkupNode(document, node);
                 wrapper.add_class(class_name);
+                // Note: add_class already emits update signal
             }
         }
 
@@ -536,6 +732,7 @@ namespace Astralis {
             foreach (var node in nodes) {
                 var wrapper = new MarkupNode(document, node);
                 wrapper.remove_class(class_name);
+                // Note: remove_class already emits update signal
             }
         }
 
@@ -545,6 +742,7 @@ namespace Astralis {
         public void set_text_all(string text) {
             foreach (var node in nodes) {
                 node->set_content(text);
+                document.update(new MarkupNode(document, node));
             }
         }
 
@@ -552,11 +750,22 @@ namespace Astralis {
         /// 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>