| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- using Xml;
- using Html;
- using Invercargill;
- using Invercargill.DataStructures;
- namespace Astralis {
- /// <summary>
- /// Represents an HTML element node in the DOM with convenient manipulation methods
- /// </summary>
- public class MarkupNode : GLib.Object {
- private Xml.Node* xml_node;
- private MarkupDocument document;
- internal MarkupNode(MarkupDocument doc, Xml.Node* node) {
- this.document = doc;
- this.xml_node = node;
- }
- /// <summary>
- /// The tag name of the element (e.g., "div", "span", "p")
- /// </summary>
- public string tag_name {
- owned get { return xml_node->name; }
- }
- /// <summary>
- /// The text content of this element and its children
- /// </summary>
- public string text_content {
- owned get { return xml_node->get_content() ?? ""; }
- set { xml_node->set_content(value); }
- }
- /// <summary>
- /// Gets an attribute value by name
- /// </summary>
- public string? get_attribute(string name) {
- return xml_node->get_prop(name);
- }
- /// <summary>
- /// Sets an attribute value
- /// </summary>
- public void set_attribute(string name, string value) {
- xml_node->set_prop(name, value);
- }
- /// <summary>
- /// Removes an attribute by name
- /// </summary>
- public void remove_attribute(string name) {
- xml_node->unset_prop(name);
- }
- /// <summary>
- /// Gets the id attribute of this element
- /// </summary>
- public string? id {
- owned get { return get_attribute("id"); }
- set {
- if (value != null) {
- set_attribute("id", value);
- } else {
- remove_attribute("id");
- }
- }
- }
- /// <summary>
- /// Gets the class attribute as a string
- /// </summary>
- public string? class_string {
- owned get { return get_attribute("class"); }
- set {
- if (value != null) {
- set_attribute("class", value);
- } else {
- remove_attribute("class");
- }
- }
- }
- /// <summary>
- /// Gets a list of CSS classes applied to this element
- /// </summary>
- public string[] classes {
- owned get {
- var class_attr = get_attribute("class");
- if (class_attr == null || class_attr.strip() == "") {
- return new string[0];
- }
- return class_attr.split_set(" \t\n\r");
- }
- }
- /// <summary>
- /// Checks if this element has a specific CSS class
- /// </summary>
- public bool has_class(string class_name) {
- foreach (unowned var cls in classes) {
- if (cls == class_name) {
- return true;
- }
- }
- return false;
- }
- /// <summary>
- /// Adds a CSS class to this element
- /// </summary>
- public void add_class(string class_name) {
- if (has_class(class_name)) {
- return;
- }
- var current_classes = classes;
- var new_classes = new string[current_classes.length + 1];
- for (int i = 0; i < current_classes.length; i++) {
- new_classes[i] = current_classes[i];
- }
- new_classes[current_classes.length] = class_name;
- set_attribute("class", string.joinv(" ", new_classes));
- }
- /// <summary>
- /// Removes a CSS class from this element
- /// </summary>
- public void remove_class(string class_name) {
- var current_classes = classes;
- var new_list = new StringBuilder();
- bool first = true;
- bool found = false;
-
- foreach (unowned var cls in current_classes) {
- if (cls == class_name) {
- found = true;
- continue;
- }
- if (!first) {
- new_list.append(" ");
- }
- new_list.append(cls);
- first = false;
- }
-
- if (found) {
- if (new_list.len > 0) {
- set_attribute("class", new_list.str);
- } else {
- remove_attribute("class");
- }
- }
- }
- /// <summary>
- /// Toggles a CSS class on this element
- /// </summary>
- public void toggle_class(string class_name) {
- if (has_class(class_name)) {
- remove_class(class_name);
- } else {
- add_class(class_name);
- }
- }
- /// <summary>
- /// Replaces all classes with a new set of classes
- /// </summary>
- public void set_classes(string[] new_classes) {
- set_attribute("class", string.joinv(" ", new_classes));
- }
- /// <summary>
- /// Gets the parent element, or null if this is the root
- /// </summary>
- 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 MarkupNode(document, parent_node);
- }
- }
- /// <summary>
- /// Gets the first child element of this node
- /// </summary>
- public MarkupNode? first_element_child {
- owned get {
- var child = xml_node->first_element_child();
- return child != null ? new MarkupNode(document, child) : null;
- }
- }
- /// <summary>
- /// Gets the last child element of this node
- /// </summary>
- public MarkupNode? last_element_child {
- owned get {
- var child = xml_node->last_element_child();
- return child != null ? new MarkupNode(document, child) : null;
- }
- }
- /// <summary>
- /// Gets the next sibling element
- /// </summary>
- public MarkupNode? next_element_sibling {
- owned get {
- var sibling = xml_node->next_element_sibling();
- return sibling != null ? new MarkupNode(document, sibling) : null;
- }
- }
- /// <summary>
- /// Gets the previous sibling element
- /// </summary>
- public MarkupNode? previous_element_sibling {
- owned get {
- var sibling = xml_node->previous_element_sibling();
- return sibling != null ? new MarkupNode(document, sibling) : null;
- }
- }
- /// <summary>
- /// Gets all child elements of this node
- /// </summary>
- public MarkupNodeList 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;
- }
- }
- /// <summary>
- /// Appends a new child element with the given tag name
- /// </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);
- }
- /// <summary>
- /// Appends a text node to this element
- /// </summary>
- public void append_text(string text) {
- xml_node->add_content(text);
- }
- /// <summary>
- /// Creates and appends a child element with text content
- /// </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);
- }
- /// <summary>
- /// Removes this element from the DOM
- /// </summary>
- public void remove() {
- xml_node->unlink();
- delete xml_node;
- }
- /// <summary>
- /// Removes all children from this element
- /// </summary>
- public void clear_children() {
- while (xml_node->children != null) {
- var child = xml_node->children;
- child->unlink();
- delete child;
- }
- }
- /// <summary>
- /// Replaces this element with new content
- /// </summary>
- public void replace_with_html(string html) {
- // Parse the HTML fragment using HTML parser
- int options = (int)(Html.ParserOption.RECOVER |
- Html.ParserOption.NOERROR |
- Html.ParserOption.NOWARNING |
- Html.ParserOption.NOBLANKS |
- Html.ParserOption.NONET);
-
- string wrapped = "<div>" + html + "</div>";
- char[] buffer = wrapped.to_utf8();
- var temp_doc = Html.Doc.read_memory(buffer, buffer.length, "", null, options);
- if (temp_doc == null) {
- return;
- }
-
- var temp_root = temp_doc->get_root_element();
- if (temp_root == null) {
- delete temp_doc;
- return;
- }
- // Import and insert children before this node
- var parent = xml_node->parent;
- if (parent != null) {
- for (var child = temp_root->children; child != null; child = child->next) {
- var imported = doc.import_node(child, true);
- parent->add_prev_sibling(imported);
- parent = imported; // Move insertion point
- }
- }
-
- // Remove this node
- remove();
- delete temp_doc;
- }
- /// <summary>
- /// Sets the inner HTML of this element
- /// </summary>
- public string inner_html {
- owned set {
- clear_children();
- if (value == null || value == "") {
- return;
- }
-
- // Parse the HTML fragment using HTML parser
- int options = (int)(Html.ParserOption.RECOVER |
- Html.ParserOption.NOERROR |
- Html.ParserOption.NOWARNING |
- Html.ParserOption.NOBLANKS |
- Html.ParserOption.NONET);
-
- string wrapped = "<div>" + value + "</div>";
- char[] buffer = wrapped.to_utf8();
- var temp_doc = Html.Doc.read_memory(buffer, buffer.length, "", null, options);
- if (temp_doc == null) {
- return;
- }
-
- var temp_root = temp_doc->get_root_element();
- if (temp_root == null) {
- delete temp_doc;
- return;
- }
- // Import children
- for (var child = temp_root->children; child != null; child = child->next) {
- var imported = doc.import_node(child, true);
- xml_node->add_child(imported);
- }
-
- delete temp_doc;
- }
- owned get {
- // Use Html.Doc to serialize the children properly
- var temp_doc = new Html.Doc();
- Xml.Node* wrapper = temp_doc.new_node(null, "div");
- temp_doc.set_root_element(wrapper);
-
- // Copy children to the wrapper
- for (var child = xml_node->children; child != null; child = child->next) {
- var copied = child->copy(1);
- wrapper->add_child(copied);
- }
-
- // Serialize using HTML serializer
- string buffer;
- int len;
- temp_doc.dump_memory(out buffer, out len);
-
- // Extract just the inner content (between <div> and </div>)
- // The HTML serializer outputs proper HTML without XML declaration
- string result = buffer ?? "";
-
- // Strip the wrapper div tags
- if (result.has_prefix("<div>")) {
- result = result.substring(5);
- }
- if (result.has_suffix("</div>")) {
- result = result.substring(0, result.length - 6);
- }
-
- return result;
- }
- }
- /// <summary>
- /// Gets the outer HTML of this element (including the element itself)
- /// </summary>
- public string outer_html {
- owned get {
- // Create a temporary HTML document to serialize this node
- var temp_doc = new Html.Doc();
- temp_doc.set_root_element(xml_node->copy(1));
- string buffer;
- int len;
- temp_doc.dump_memory(out buffer, out len);
- return buffer ?? "";
- }
- }
- /// <summary>
- /// Creates an HttpResult from this node's outer HTML.
- /// Useful for returning HTML fragments to HTMX requests.
- /// </summary>
- /// <param name="status">HTTP status code (defaults to OK)</param>
- /// <returns>An HttpResult containing the node's HTML with Content-Type text/html</returns>
- public HttpResult to_result(StatusCode status = StatusCode.OK) {
- return new HttpStringResult(outer_html, status)
- .set_header("Content-Type", "text/html; charset=UTF-8");
- }
- internal MarkupDocument doc {
- get { return document; }
- }
- internal Xml.Node* native {
- 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);
- }
- }
- /// <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);
- }
- }
- /// <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);
- }
- }
- /// <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);
- }
- }
- /// <summary>
- /// Removes all nodes in the list from the DOM
- /// </summary>
- public void remove_all() {
- foreach (var node in nodes) {
- node->unlink();
- delete node;
- }
- nodes = new List<Xml.Node*>();
- }
- /// <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(document, nodes);
- }
- /// <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 MarkupDocument document;
- private unowned List<Xml.Node*> nodes;
- private unowned List<Xml.Node*>? current;
- internal NodeTracker(MarkupDocument doc, List<Xml.Node*> nodes) {
- this.document = doc;
- this.nodes = nodes;
- this.current = null;
- }
- public override MarkupNode get_next() {
- if (current == null) {
- current = 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(document, current.data);
- }
- public override bool has_next() {
- if (current == null) {
- return !nodes.is_empty();
- }
- return current.next != null;
- }
- }
- }
- }
|