Skip to main content

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}