using Xml;
using Html;
using Invercargill;
using Invercargill.DataStructures;
namespace Astralis {
///
/// Represents an HTML element node in the DOM with convenient manipulation methods
///
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;
}
///
/// The tag name of the element (e.g., "div", "span", "p")
///
public string tag_name {
owned get { return xml_node->name; }
}
///
/// The text content of this element and its children
///
public string text_content {
owned get { return xml_node->get_content() ?? ""; }
set { xml_node->set_content(value); }
}
///
/// Gets an attribute value by name
///
public string? get_attribute(string name) {
return xml_node->get_prop(name);
}
///
/// Sets an attribute value
///
public void set_attribute(string name, string value) {
xml_node->set_prop(name, value);
}
///
/// Removes an attribute by name
///
public void remove_attribute(string name) {
xml_node->unset_prop(name);
}
///
/// Gets the id attribute of this element
///
public string? id {
owned get { return get_attribute("id"); }
set {
if (value != null) {
set_attribute("id", value);
} else {
remove_attribute("id");
}
}
}
///
/// Gets the class attribute as a string
///
public string? class_string {
owned get { return get_attribute("class"); }
set {
if (value != null) {
set_attribute("class", value);
} else {
remove_attribute("class");
}
}
}
///
/// Gets a list of CSS classes applied to this element
///
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");
}
}
///
/// Checks if this element has a specific CSS class
///
public bool has_class(string class_name) {
foreach (unowned var cls in classes) {
if (cls == class_name) {
return true;
}
}
return false;
}
///
/// Adds a CSS class to this element
///
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));
}
///
/// Removes a CSS class from this element
///
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");
}
}
}
///
/// Toggles a CSS class on this element
///
public void toggle_class(string class_name) {
if (has_class(class_name)) {
remove_class(class_name);
} else {
add_class(class_name);
}
}
///
/// Replaces all classes with a new set of classes
///
public void set_classes(string[] new_classes) {
set_attribute("class", string.joinv(" ", new_classes));
}
///
/// Gets the parent element, or null if this is the root
///
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);
}
}
///
/// Gets the first child element of this node
///
public MarkupNode? first_element_child {
owned get {
var child = xml_node->first_element_child();
return child != null ? new MarkupNode(document, child) : null;
}
}
///
/// Gets the last child element of this node
///
public MarkupNode? last_element_child {
owned get {
var child = xml_node->last_element_child();
return child != null ? new MarkupNode(document, child) : null;
}
}
///
/// Gets the next sibling element
///
public MarkupNode? next_element_sibling {
owned get {
var sibling = xml_node->next_element_sibling();
return sibling != null ? new MarkupNode(document, sibling) : null;
}
}
///
/// Gets the previous sibling element
///
public MarkupNode? previous_element_sibling {
owned get {
var sibling = xml_node->previous_element_sibling();
return sibling != null ? new MarkupNode(document, sibling) : null;
}
}
///
/// Gets all child elements of this node
///
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;
}
}
///
/// Appends a new child element with the given tag name
///
public MarkupNode append_child_element(string tag_name) {
var new_node = xml_node->new_child(null, tag_name);
return new MarkupNode(document, new_node);
}
///
/// Appends a text node to this element
///
public void append_text(string text) {
xml_node->add_content(text);
}
///
/// Creates and appends a child element with text content
///
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);
}
///
/// Removes this element from the DOM
///
public void remove() {
xml_node->unlink();
delete xml_node;
}
///
/// Removes all children from this element
///
public void clear_children() {
while (xml_node->children != null) {
var child = xml_node->children;
child->unlink();
delete child;
}
}
///
/// Replaces this element with new content
///
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 = "
" + html + "
";
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;
}
///
/// Sets the inner HTML of this element
///
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 = "" + value + "
";
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 and
)
// The HTML serializer outputs proper HTML without XML declaration
string result = buffer ?? "";
// Strip the wrapper div tags
if (result.has_prefix("")) {
result = result.substring(5);
}
if (result.has_suffix("
")) {
result = result.substring(0, result.length - 6);
}
return result;
}
}
///
/// Gets the outer HTML of this element (including the element itself)
///
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 ?? "";
}
}
///
/// Creates an HttpResult from this node's outer HTML.
/// Useful for returning HTML fragments to HTMX requests.
///
/// HTTP status code (defaults to OK)
/// An HttpResult containing the node's HTML with Content-Type text/html
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; }
}
}
///
/// A list of HTML nodes supporting iteration and manipulation
///
public class MarkupNodeList : Invercargill.Enumerable {
private MarkupDocument document;
private List nodes;
internal MarkupNodeList(MarkupDocument doc) {
this.document = doc;
this.nodes = new List();
}
internal void add_node(Xml.Node* node) {
nodes.append(node);
}
///
/// Number of nodes in the list
///
public int length {
get { return (int)nodes.length(); }
}
///
/// Gets a node at the specified index
///
public new MarkupNode? get(int index) {
unowned List item = nodes.nth((uint)index);
if (item == null) {
return null;
}
return new MarkupNode(document, item.data);
}
///
/// Gets the first node, or null if empty
///
public new MarkupNode? first {
owned get {
if (nodes.is_empty()) {
return null;
}
unowned List item = nodes.first();
return item != null ? new MarkupNode(document, item.data) : null;
}
}
///
/// Gets the last node, or null if empty
///
public new MarkupNode? last {
owned get {
if (nodes.is_empty()) {
return null;
}
unowned List item = nodes.last();
return item != null ? new MarkupNode(document, item.data) : null;
}
}
///
/// Sets a property on all nodes in the list
///
public void set_attribute_all(string name, string value) {
foreach (var node in nodes) {
node->set_prop(name, value);
}
}
///
/// Adds a class to all nodes in the list
///
public void add_class_all(string class_name) {
foreach (var node in nodes) {
var wrapper = new MarkupNode(document, node);
wrapper.add_class(class_name);
}
}
///
/// Removes a class from all nodes in the list
///
public void remove_class_all(string class_name) {
foreach (var node in nodes) {
var wrapper = new MarkupNode(document, node);
wrapper.remove_class(class_name);
}
}
///
/// Sets text content on all nodes in the list
///
public void set_text_all(string text) {
foreach (var node in nodes) {
node->set_content(text);
}
}
///
/// Removes all nodes in the list from the DOM
///
public void remove_all() {
foreach (var node in nodes) {
node->unlink();
delete node;
}
nodes = new List();
}
///
/// Gets information about this enumerable
///
public override Invercargill.EnumerableInfo get_info() {
return new Invercargill.EnumerableInfo.infer_ultimate(this, Invercargill.EnumerableCategory.IN_MEMORY);
}
///
/// Gets a tracker for iterating over the nodes
///
public override Invercargill.Tracker get_tracker() {
return new NodeTracker(document, nodes);
}
///
/// Peeks at the count without full enumeration
///
public override uint? peek_count() {
return (uint)nodes.length();
}
///
/// Tracker for iterating over MarkupNodeList
///
private class NodeTracker : Invercargill.Tracker {
private MarkupDocument document;
private unowned List nodes;
private unowned List? current;
internal NodeTracker(MarkupDocument doc, List 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;
}
}
}
}