HTML, XML and SVG for MoonBit: parse, query and edit.
let page = @html.loads("<ul id=list><li>coffee<li>tea</ul>")
page.css("li").length() // 2 — the document closed neither
page.xpath("//li[2]").text() // "tea"
page.css("li").last().add_class("done")
page.dumps()| One tree, two syntaxes | Node is element, text, CDATA, comment, processing instruction or doctype. Nothing about a node says which parser made it. |
| One chain | Every query answers a Selection, so css → xpath → selector → custom → items[i] → children()[j] → css again never changes type. |
| Two engines, and yours | CSS Selectors 4's statically decidable subset, all of XPath 1.0, and a Selector trait you implement and register by name. |
| Editing, not re-parsing | A full set of editing verbs on the same selection the query returned, with parent pointers kept true. |
| Two ways to read | Build the tree, or stream Events into your own Sink and build nothing. The tree builder is itself a sink: one parser, no drift. |
| One error family | Refused: malformed input names its offset, a bad selector names its fault. HTML never refuses — it repairs, and tells you what it repaired. |
moon add moonbitstack/moonxmlimport {
"moonbitstack/moonxml", // tree, chain, engines, writer
"moonbitstack/moonxml/html", // @html.loads
"moonbitstack/moonxml/xml", // @xml.loads
"moonbitstack/moonxml/event", // Event, Sink
"moonbitstack/moonxml/htmx", // @htmx.Hx — htmx's attributes, read and carried out
}page.xpath("//table") // any engine
.css("tr:nth-child(2)") // any other
.selector("mine", "") // one you registered
.custom(@moonxml.SelectorFn::new(f)) // one you did not
.items[0] // a Node
.children()[1] // still going
.css("span")
.set_text("done")| read | root document_element() flavour() repairs() |
| search | css(q) xpath(q) xpath_value(q) selector(name, q) custom(sel) by_id(v) by_class(v) by_tag(v) by_name(v) |
| content | text() html() children() descendants() dumps() dump() dumps_html() dump_html() events() |
| change | append(node) prepend(node) clear() smooth() register(name, sel) push(event) |
| search | css xpath xpath_value selector custom by_id by_class by_tag by_name filter(f) closest(q) |
| narrow | first() last() eq(i) children() parent() descendants() items[i] |
| read | length() text() html() attr(name) |
| change | append prepend remove replace_with unwrap wrap clear set_text set_attr remove_attr add_class remove_class smooth |
| fields | name attrs children parent closed |
| structure | append prepend insert(at, node) insert_before insert_after replace_with remove unwrap wrap clear smooth |
| attributes | attr has_attr set_attr remove_attr classes has_class add_class remove_class |
| content | text() set_text(s) html() walk(f) document() |
let page = @html.loads(
"<form hx-post='/todos' hx-target='#list' hx-swap='beforeend'>" +
"<input name='title' value='write it down'><button>Add</button></form>" +
"<ul id='list'></ul>")
let request = @htmx.Hx::of(page.css("form").items[0]).request("submit").unwrap()
request.address() // "/todos"
request.query() // "title=write+it+down"
request.headers() // [("HX-Request", "true"), ("HX-Target", "list")]
request.answer("<li>a second one</li>") // and the list has it| src/examples/tour | the whole surface in one run: moon run src/examples/tour |
| src/examples/hypermedia | a page that declares its requests: read it, make one, swap the answer in |
| src/examples/selectors | every built-in selector against one document — the run behind the selector board |
| src/examples/scrape | a page in, a table out: query, filter, reshape, serialize |
| src/examples/rewrite | read a feed, edit every entry, write it back byte-faithfully |
struct DeepLinks {}
impl Selector for DeepLinks with select(_self, ctx, _query) {
ctx.css("a[href]").filter(fn(node) {
node is ElementNode(e) && e.attr("href").has_prefix("https://")
})
}pub(all) enum Node {
ElementNode(Element)
TextNode(String)
CDataNode(String)
CommentNode(String)
ProcessingInstructionNode(ProcessingInstruction)
DoctypeNode(String)
}pub(all) struct ProcessingInstruction {
target : String
data : String
}pub(all) struct Repairs {
implied : Int
dropped : Int
reparented : Int
closed_at_eof : Int
} doc.xpath("//ul").css("li.external").xpath("@href")
.selector("tag", "a").custom(DeepLinks::new())
.items[2].children[0].css("span").text()impl Selector for SelectorFnfn SelectorFn::select(self : SelectorFn, ctx : Selection, query : String) -> Selection raise Refusedfn namespace_of(name : String) -> String?