jQuery-like HTML DOM manipulation library for Zig. Parse HTML, query elements with CSS selectors, traverse the tree, and manipulate the document.
const std = @import("std");
const zq = @import("zigquery");
pub fn main() !void {
const allocator = std.heap.page_allocator;
var doc = try zq.Document.initFromSlice(allocator,
\\<html>
\\ <body>
\\ <div class="content">
\\ <h1>Hello</h1>
\\ <p>First paragraph</p>
\\ <p>Second paragraph</p>
\\ <a href="/about" class="active">About</a>
\\ </div>
\\ </body>
\\</html>
);
defer doc.deinit();
// Find all paragraphs.
const paragraphs = try doc.find("p");
std.debug.print("Found {} paragraphs\n", .{paragraphs.len()});
// Get text content.
const title = try (try doc.find("h1")).text();
std.debug.print("Title: {s}\n", .{title});
// Read attributes.
const link = try doc.find("a.active");
const href = link.attr("href") orelse "";
std.debug.print("Link: {s}\n", .{href});
}Add zigquery as a dependency in your build.zig.zon:
zig fetch --save git+https://github.com/OrlovEvgeny/zigqueryIf Zig reports an invalid fingerprint, make sure the repository owner is
OrlovEvgeny and rerun zig fetch --save with the corrected URL.
Then in your build.zig:
const zigquery = b.dependency("zigquery", .{
.target = target,
.optimize = optimize,
});
module.addImport("zigquery", zigquery.module("zigquery"));Supported Zig versions: 0.15.2 and 0.16.x.
Supported selector syntax:
| Selector | Example | Description |
|---|---|---|
| Type | div, p, a |
Match by tag name |
| Class | .active, .foo.bar |
Match by class (compound supported) |
| ID | #main |
Match by ID |
| Universal | * |
Match any element |
| Attribute | [href], [type="text"] |
Attribute existence / value |
| Attribute operators | [class~="foo"], [lang|="en"], [href^="/"], [src$=".png"], [data*="val"] |
Includes, dash-match, prefix, suffix, substring |
| Descendant | div p |
p anywhere inside div |
| Child | div > p |
Direct child only |
| Adjacent sibling | h1 + p |
Immediately after |
| General sibling | h1 ~ p |
Any sibling after |
| Group | h1, h2, h3 |
Match any in the list |
| Negation | :not(.hidden) |
Exclude matches |
| Logical lists | :is(h1, h2), :where(.note) |
Match any selector in a list |
:has() |
div:has(> p) |
Parent has matching descendant |
:contains() |
p:contains("hello") |
Element contains text |
:first-child, :last-child, :only-child |
li:first-child |
Structural pseudo-classes |
:first-of-type, :last-of-type, :only-of-type |
p:first-of-type |
Type-based structural pseudo-classes |
:nth-child(), :nth-last-child() |
tr:nth-child(2n+1) |
Positional with an+b formula |
:nth-of-type(), :nth-last-of-type() |
p:nth-of-type(odd) |
Type-positional |
:empty, :root |
div:empty |
Content / root pseudo-classes |
:enabled, :disabled, :checked |
input:enabled |
Form pseudo-classes |
// Parse HTML into a document. All allocations use an internal arena.
var doc = try zq.Document.initFromSlice(allocator, html);
defer doc.deinit();
// Query from the document root.
const sel = try doc.find("div.content");
// Deep-clone the entire document.
var copy = try doc.clone(allocator);
defer copy.deinit();const sel = try doc.find("div");
// Descendants matching a selector.
const links = try sel.find("a");
// Direct children.
const kids = try sel.children();
const filtered_kids = try sel.childrenFiltered("p");
// Parents.
const p = try sel.parent();
const all_parents = try sel.parents();
// Closest ancestor (or self) matching a selector.
const wrapper = try sel.closest(".wrapper");
// Siblings.
const sibs = try sel.siblings();
const next_el = try sel.next();
const prev_all = try sel.prevAll();const items = try doc.find("li");
const active = try items.filter(".active");
const inactive = try items.not(".active");
const with_links = try items.has("a");
const first = try items.first();
const last = try items.last();
const third = try items.eq(2); // zero-based
const from_end = try items.eq(-1); // negative indexes from end
const middle = try items.sliceRange(1, 3);
// Boolean checks.
const is_active = try items.is(".active");const el = try doc.find("a.nav");
// Attributes.
const href = el.attr("href");
const title = el.attrOr("title", "default");
try el.setAttr("target", "_blank");
el.removeAttr("rel");
// Classes.
try el.addClass("highlight bold");
try el.removeClass("nav");
try el.toggleClass("active");
const has = el.hasClass("highlight");
// Content.
const inner = try el.html();
const outer = try zq.outerHtml(el);
const txt = try el.text();
const name = zq.nodeName(el);const div = try doc.find("div");
// Insert content.
try div.appendHtml("<p>appended</p>");
try div.prependHtml("<p>prepended</p>");
// Insert around selection.
const p = try doc.find("p");
try p.afterHtml("<hr/>");
try p.beforeHtml("<!-- marker -->");
// Replace and remove.
_ = try p.replaceWithHtml("<div>replaced</div>");
_ = p.remove();
_ = try div.empty(); // remove all children
// Set content.
try div.setHtml("<b>new content</b>");
try div.setText("plain text");
// Wrap / unwrap.
try div.wrapHtml("<section></section>");
try div.unwrap();const rows = try doc.find("tr");
// Iterator (idiomatic Zig).
var it = rows.iterator();
while (it.next()) |row| {
const cells = try row.find("td");
// ...
}
// Callback-based.
rows.each(struct {
fn f(i: usize, sel: zq.Selection) void {
_ = i;
_ = sel;
}
}.f);const a = try doc.find(".foo");
const b = try doc.find(".bar");
const combined = try a.add(".bar");
const merged = try a.addSelection(b);
const union_sel = try a.@"union"(b);
const common = try a.intersection(b);Compile a selector once when it is reused across queries or documents:
var active_links = try zq.CompiledSelector.init(allocator, "a.active");
defer active_links.deinit();
const links = try doc.findCompiled(&active_links);
const matches = links.isCompiled(&active_links);Document.initFromSlice, Document.initFromNode, Document.clone, parsed fragments,
attributes, and inserted nodes own their data through the document arena. Input buffers
and source documents may be released after these operations complete. Use
Document.initBorrowedNode only when the source tree is guaranteed to outlive the
document.
Operations that allocate return an error union. In v0.2 this includes traversal methods
such as children, positional methods such as first, attribute/class updates, and DOM
mutations. Mutations parse or clone all required data before changing the tree, so an
allocation failure does not leave a partially updated selection.
Add try to allocating Selection calls. Node insertion now deep-clones supplied nodes
for every destination and never detaches the caller's source nodes. Invalid selector
syntax is returned as an error by Selection.is instead of being treated as no match.
The parser is intentionally lenient and covers common HTML document and fragment use,
including implicit html/head/body, raw text/RCDATA, optional closing for common list
and table elements, comments, and core character references. It is not yet a complete
WHATWG tree builder; foreign content, templates, the adoption agency algorithm, and the
full named entity table remain roadmap items. See ROADMAP.md.
zig build testInspired by Go's goquery and, by extension, jQuery