oxml_wasm/lib.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2026 oxml. All rights reserved.
3
4//! WebAssembly bindings for [`oxml`].
5//!
6//! Being pure Rust with no C dependency is what makes this possible at
7//! all: `libxml`-based crates cannot target WebAssembly without a
8//! libxml2 toolchain, which is most of the reason a browser-side `XPath`
9//! has not been practical in Rust.
10//!
11//! The surface is deliberately small. JavaScript already has
12//! `DOMParser`; what it does not have is a fast, dependency-free `XPath`
13//! that behaves identically to the server-side one. That is what these
14//! bindings expose.
15
16#![forbid(unsafe_code)]
17
18use wasm_bindgen::prelude::*;
19
20/// The work, without the `wasm-bindgen` layer.
21///
22/// Public so it can be benchmarked and so the crate is usable from
23/// Rust as well as from JavaScript. The exports below are a thin
24/// translation of these into types `wasm-bindgen` can carry across
25/// the boundary; everything that decides anything lives here.
26pub mod core;
27
28mod output;
29
30/// A parsed XML document.
31#[wasm_bindgen]
32#[derive(Debug)]
33pub struct Document {
34 inner: oxml::Document,
35}
36
37/// Parse an XML document.
38///
39/// # Errors
40///
41/// Returns a `JsError` carrying the position and reason if the input
42/// is not well-formed.
43#[wasm_bindgen]
44pub fn parse(source: &str) -> Result<Document, JsError> {
45 core::parse(source)
46 .map(|inner| Document { inner })
47 .map_err(|e| JsError::new(&e))
48}
49
50#[wasm_bindgen]
51impl Document {
52 /// The number of nodes, including the document root.
53 #[wasm_bindgen(getter)]
54 #[must_use]
55 pub fn size(&self) -> usize {
56 self.inner.len()
57 }
58
59 /// The name of the root element, if there is one.
60 #[wasm_bindgen(js_name = rootName)]
61 #[must_use]
62 pub fn root_name(&self) -> Option<String> {
63 core::root_name(&self.inner)
64 }
65
66 /// Evaluate an `XPath` expression and return the matched nodes'
67 /// text, as an array of strings.
68 ///
69 /// Returning text rather than node handles is deliberate: a
70 /// `NodeId` is only meaningful against the document that issued
71 /// it, and handing an opaque integer across the WASM boundary
72 /// invites exactly the misuse the Rust API's lifetime rules
73 /// prevent.
74 ///
75 ///
76 /// `namespaces` binds prefixes for the expression, each written
77 /// `"PREFIX=URI"` — the same spelling `oxml-cli` takes for `--ns`.
78 /// A prefix resolves against these bindings and not against the
79 /// document, so one query works across documents that spell the
80 /// prefix differently. An unbound prefix is an error.
81 ///
82 /// # Errors
83 ///
84 /// Returns a `JsError` if the expression is malformed or uses an
85 /// unbound prefix.
86 #[wasm_bindgen(js_name = queryText)]
87 pub fn query_text(
88 &self,
89 expression: &str,
90 namespaces: Option<Vec<String>>,
91 ) -> Result<Vec<String>, JsError> {
92 core::query_text(
93 &self.inner,
94 expression,
95 &namespaces.unwrap_or_default(),
96 )
97 .map_err(|e| JsError::new(&e))
98 }
99
100 /// Evaluate an `XPath` expression and return its value as a string.
101 ///
102 /// Use this for expressions that are not node-sets — `count(..)`,
103 /// `string(..)`, a comparison.
104 ///
105 ///
106 /// `namespaces` binds prefixes for the expression, each written
107 /// `"PREFIX=URI"` — the same spelling `oxml-cli` takes for `--ns`.
108 /// A prefix resolves against these bindings and not against the
109 /// document, so one query works across documents that spell the
110 /// prefix differently. An unbound prefix is an error.
111 ///
112 /// # Errors
113 ///
114 /// Returns a `JsError` if the expression is malformed or uses an
115 /// unbound prefix.
116 #[wasm_bindgen(js_name = queryValue)]
117 pub fn query_value(
118 &self,
119 expression: &str,
120 namespaces: Option<Vec<String>>,
121 ) -> Result<String, JsError> {
122 core::query_value(
123 &self.inner,
124 expression,
125 &namespaces.unwrap_or_default(),
126 )
127 .map_err(|e| JsError::new(&e))
128 }
129
130 /// How many nodes an expression matches.
131 ///
132 ///
133 /// `namespaces` binds prefixes for the expression, each written
134 /// `"PREFIX=URI"` — the same spelling `oxml-cli` takes for `--ns`.
135 /// A prefix resolves against these bindings and not against the
136 /// document, so one query works across documents that spell the
137 /// prefix differently. An unbound prefix is an error.
138 ///
139 /// # Errors
140 ///
141 /// Returns a `JsError` if the expression is malformed or uses an
142 /// unbound prefix.
143 #[wasm_bindgen(js_name = queryCount)]
144 pub fn query_count(
145 &self,
146 expression: &str,
147 namespaces: Option<Vec<String>>,
148 ) -> Result<usize, JsError> {
149 core::query_count(
150 &self.inner,
151 expression,
152 &namespaces.unwrap_or_default(),
153 )
154 .map_err(|e| JsError::new(&e))
155 }
156
157 /// The document as XML.
158 ///
159 /// The counterpart to `parse`. Reading a document and writing it
160 /// back previously meant reaching for `XMLSerializer` and holding
161 /// two representations of the same thing.
162 ///
163 /// Round-trips: the output parses to a document that serialises
164 /// identically. It is not guaranteed byte-identical to the input,
165 /// because a document has more than one valid spelling -- entity
166 /// references and attribute order among them.
167 #[wasm_bindgen(js_name = toXml)]
168 #[must_use]
169 pub fn to_xml(&self) -> String {
170 core::to_xml(&self.inner)
171 }
172}
173
174/// Check whether a document is well-formed, without keeping it.
175///
176/// Cheaper than `parse` when the answer is all you need, because the
177/// tree is dropped immediately.
178#[wasm_bindgen(js_name = isWellFormed)]
179#[must_use]
180pub fn is_well_formed(source: &str) -> bool {
181 core::is_well_formed(source)
182}