#![allow(non_snake_case)]
use crate::ffi;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use wasm_bindgen::prelude::*;
extern "C" {
fn shim_fs_register(path: *const c_char, data: *const u8, len: usize) -> c_int;
fn shim_fs_has(path: *const c_char) -> c_int;
fn shim_fs_clear();
}
#[wasm_bindgen]
pub fn register_file(path: String, bytes: Vec<u8>) -> Result<(), JsError> {
let cpath =
CString::new(path.as_str()).map_err(|_| JsError::new("path contains a NUL byte"))?;
let rc = unsafe { shim_fs_register(cpath.as_ptr(), bytes.as_ptr(), bytes.len()) };
if rc != 0 {
return Err(JsError::new("register_file failed (out of memory)"));
}
Ok(())
}
#[wasm_bindgen]
pub fn clear_registered_files() {
unsafe { shim_fs_clear() };
}
#[wasm_bindgen]
pub struct WasmAttribute {
name: String,
value: String,
prefix: Option<String>,
uri: Option<String>,
}
#[wasm_bindgen]
impl WasmAttribute {
#[wasm_bindgen(getter)]
pub fn name(&self) -> String {
self.name.clone()
}
#[wasm_bindgen(getter)]
pub fn value(&self) -> String {
self.value.clone()
}
#[wasm_bindgen(getter)]
pub fn prefix(&self) -> Option<String> {
self.prefix.clone()
}
#[wasm_bindgen(getter)]
pub fn uri(&self) -> Option<String> {
self.uri.clone()
}
}
#[wasm_bindgen]
pub struct WasmCapabilities {
caps: u64,
descriptor_version: u32,
}
#[wasm_bindgen]
impl WasmCapabilities {
#[wasm_bindgen(getter)]
pub fn caps(&self) -> f64 {
self.caps as f64
}
#[wasm_bindgen(getter)]
pub fn descriptor_version(&self) -> u32 {
self.descriptor_version
}
pub fn serves(&self, bit: f64) -> bool {
self.caps & (bit as u64) != 0
}
}
#[wasm_bindgen]
pub struct WasmDocument {
raw: *mut ffi::LeptrisDocument,
}
#[wasm_bindgen]
pub struct WasmSchema {
raw: *mut ffi::LeptrisXsdSchema,
}
impl Drop for WasmSchema {
fn drop(&mut self) {
if !self.raw.is_null() {
unsafe { ffi::leptris_xsd_free(self.raw) };
}
}
}
#[wasm_bindgen]
pub fn engine_capabilities() -> WasmCapabilities {
let raw = unsafe { ffi::leptris_capabilities() };
WasmCapabilities {
caps: raw.caps,
descriptor_version: raw.descriptor_version,
}
}
#[wasm_bindgen]
pub fn compile_xsd_set(paths: Vec<String>) -> Result<WasmSchema, JsError> {
if paths.is_empty() {
return Err(JsError::new("schema set needs at least one path"));
}
let cpaths: Vec<CString> = paths
.iter()
.map(|p| CString::new(p.as_str()))
.collect::<Result<_, _>>()
.map_err(|_| JsError::new("schema path contains a NUL byte"))?;
for (p, cp) in paths.iter().zip(&cpaths) {
if unsafe { shim_fs_has(cp.as_ptr()) } == 0 {
return Err(JsError::new(&format!(
"schema path not registered: {p} — call register_file() for every \
document the set will read (heads and imported members) first"
)));
}
}
let ptrs: Vec<*const std::os::raw::c_char> = cpaths.iter().map(|p| p.as_ptr()).collect();
let mut st = ffi::LEPTRIS_OK;
let raw = unsafe { ffi::leptris_xsd_compile_set(ptrs.as_ptr(), ptrs.len(), &mut st) };
finish_schema(raw, st, "leptris xsd compile_set")
}
fn finish_schema(
raw: *mut ffi::LeptrisXsdSchema,
st: ffi::LeptrisStatus,
what: &str,
) -> Result<WasmSchema, JsError> {
if st != ffi::LEPTRIS_OK {
let mut msg = String::new();
if !raw.is_null() {
let p = unsafe { ffi::leptris_xsd_error(raw) };
if !p.is_null() {
msg = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned();
}
unsafe { ffi::leptris_xsd_free(raw) };
}
return Err(JsError::new(&format!("{what} failed (status {st}): {msg}")));
}
if raw.is_null() {
return Err(JsError::new(&format!("{what} failed (status {st})")));
}
Ok(WasmSchema { raw })
}
#[wasm_bindgen]
pub fn parse_html(html: &str) -> Result<WasmDocument, JsError> {
let chtml = CString::new(html).map_err(|_| JsError::new("html contains a NUL byte"))?;
let mut st = ffi::LEPTRIS_OK;
let raw = unsafe { ffi::leptris_parse_html_string(chtml.as_ptr(), html.len(), &mut st) };
if raw.is_null() {
return Err(JsError::new(&format!(
"leptris html parse failed (status {st})"
)));
}
Ok(WasmDocument { raw })
}
#[wasm_bindgen]
pub fn compile_xsd(xsd: &str) -> Result<WasmSchema, JsError> {
let cxsd = CString::new(xsd).map_err(|_| JsError::new("xsd contains a NUL byte"))?;
let mut st: ffi::LeptrisStatus = ffi::LEPTRIS_OK;
let raw = unsafe { ffi::leptris_xsd_compile(cxsd.as_ptr(), xsd.len(), &mut st) };
finish_schema(raw, st, "leptris xsd compile")
}
#[wasm_bindgen]
impl WasmSchema {
pub fn validate_doc(&self, doc: &WasmDocument) -> Result<bool, JsError> {
let r = unsafe { ffi::leptris_xsd_validate(self.raw, doc.raw) };
if r < 0 {
return Err(JsError::new("leptris xsd validation failed"));
}
Ok(r == 1)
}
pub fn last_errors(&self) -> Vec<String> {
unsafe {
let n = ffi::leptris_xsd_error_count(self.raw);
(0..n)
.filter_map(|i| {
let p = ffi::leptris_xsd_error_at(self.raw, i);
if p.is_null() {
None
} else {
Some(CStr::from_ptr(p).to_string_lossy().into_owned())
}
})
.collect()
}
}
}
impl Drop for WasmDocument {
fn drop(&mut self) {
if !self.raw.is_null() {
unsafe { ffi::leptris_document_free(self.raw) };
}
}
}
#[wasm_bindgen]
impl WasmDocument {
#[wasm_bindgen(constructor)]
pub fn parse(xml: &str) -> Result<WasmDocument, JsError> {
let cxml = CString::new(xml).map_err(|_| JsError::new("xml contains a NUL byte"))?;
let mut st: ffi::LeptrisStatus = ffi::LEPTRIS_OK;
let raw = unsafe { ffi::leptris_parse_string(cxml.as_ptr(), xml.len(), &mut st) };
if raw.is_null() {
return Err(JsError::new(&format!("leptris parse failed (status {st})")));
}
Ok(WasmDocument { raw })
}
pub fn xpath(&self, expression: &str) -> Result<String, JsError> {
let cexpr =
CString::new(expression).map_err(|_| JsError::new("xpath contains a NUL byte"))?;
let result =
unsafe { ffi::leptris_xpath_eval(self.raw, std::ptr::null_mut(), cexpr.as_ptr()) };
if result.is_null() {
return Err(JsError::new("leptris xpath evaluation failed"));
}
let s = unsafe { take_string(ffi::leptris_xpath_result_string(result)) };
unsafe { ffi::leptris_xpath_result_free(result) };
s.map_err(|_| JsError::new("result string materialization failed"))
}
pub fn xpath_count(&self, expression: &str) -> Result<usize, JsError> {
let cexpr =
CString::new(expression).map_err(|_| JsError::new("xpath contains a NUL byte"))?;
let result =
unsafe { ffi::leptris_xpath_eval(self.raw, std::ptr::null_mut(), cexpr.as_ptr()) };
if result.is_null() {
return Err(JsError::new("leptris xpath evaluation failed"));
}
let n = unsafe { ffi::leptris_xpath_result_count(result) };
unsafe { ffi::leptris_xpath_result_free(result) };
Ok(n)
}
pub fn to_xml(&self) -> Result<String, JsError> {
unsafe {
take_string(ffi::leptris_document_serialize(
self.raw,
std::ptr::null_mut(),
))
}
.map_err(|_| JsError::new("serialization failed"))
}
pub fn root_name(&self) -> Option<String> {
let root = unsafe { ffi::leptris_document_root(self.raw) };
if root.is_null() {
return None;
}
unsafe {
Some(
CStr::from_ptr(ffi::leptris_element_name(root))
.to_string_lossy()
.into_owned(),
)
}
}
}
unsafe fn take_string(p: *mut c_char) -> Result<String, ()> {
if p.is_null() {
return Err(());
}
let s = CStr::from_ptr(p as *const c_char)
.to_string_lossy()
.into_owned();
extern "C" {
fn leptris_free_string(str: *mut c_char);
}
leptris_free_string(p);
let _ = c_int::MAX; Ok(s)
}
#[wasm_bindgen]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum WasmNodeKind {
Element = 1,
Text = 2,
Comment = 3,
Cdata = 4,
Pi = 5,
DocType = 6,
Attribute = 7,
Other = 0,
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmNode {
doc: *mut ffi::LeptrisDocument,
node: ffi::LeptrisNodeRef,
}
#[wasm_bindgen]
impl WasmNode {
pub fn name(&self) -> Option<String> {
unsafe {
let p = ffi::leptris_element_name(self.node as *mut ffi::LeptrisElement);
if p.is_null() {
return None;
}
Some(CStr::from_ptr(p).to_string_lossy().into_owned())
}
}
pub fn text(&self) -> Option<String> {
unsafe {
let p = match self.kind() {
WasmNodeKind::Text => ffi::leptris_text_node_get_content(self.node),
_ => ffi::leptris_element_text(self.node as *mut ffi::LeptrisElement),
};
if p.is_null() {
return None;
}
Some(CStr::from_ptr(p).to_string_lossy().into_owned())
}
}
pub fn kind(&self) -> WasmNodeKind {
match unsafe { ffi::leptris_node_get_type(self.node) } {
0 => WasmNodeKind::Element,
1 => WasmNodeKind::Text,
2 => WasmNodeKind::Comment,
3 => WasmNodeKind::Cdata,
4 => WasmNodeKind::Pi,
5 => WasmNodeKind::DocType,
6 => WasmNodeKind::Attribute,
_ => WasmNodeKind::Other,
}
}
pub fn children(&self) -> Vec<WasmNode> {
unsafe {
let n = ffi::leptris_node_children(self.node, std::ptr::null_mut(), 0);
if n == 0 {
return Vec::new();
}
let mut nodes: Vec<ffi::LeptrisNodeRef> = vec![std::ptr::null_mut(); n];
let got = ffi::leptris_node_children(self.node, nodes.as_mut_ptr(), n);
nodes.truncate(got);
nodes
.into_iter()
.map(|node| WasmNode {
doc: self.doc,
node,
})
.collect()
}
}
pub fn set_attribute(&self, name: &str, value: &str) -> Result<(), JsError> {
let cname =
CString::new(name).map_err(|_| JsError::new("attribute name contains a NUL byte"))?;
let cvalue =
CString::new(value).map_err(|_| JsError::new("attribute value contains a NUL byte"))?;
let st = unsafe {
ffi::leptris_element_set_attribute(
self.node as *mut ffi::LeptrisElement,
cname.as_ptr(),
cvalue.as_ptr(),
)
};
if st != ffi::LEPTRIS_OK {
return Err(JsError::new("leptris set_attribute failed"));
}
Ok(())
}
pub fn namespace_uri(&self) -> Option<String> {
unsafe {
let ns = ffi::leptris_element_namespace(self.node as *mut ffi::LeptrisElement);
if ns.is_null() {
return None;
}
let p = ffi::leptris_namespace_uri(ns);
if p.is_null() {
return None;
}
Some(CStr::from_ptr(p).to_string_lossy().into_owned())
}
}
pub fn prefix(&self) -> Option<String> {
unsafe {
let p = ffi::leptris_element_prefix(self.node as *mut ffi::LeptrisElement);
if p.is_null() {
return None;
}
Some(CStr::from_ptr(p).to_string_lossy().into_owned())
}
}
pub fn attribute_pairs(&self) -> Result<Vec<WasmAttribute>, JsError> {
let elem = self.node as *mut ffi::LeptrisElement;
let count = unsafe { ffi::leptris_element_attribute_count(elem) };
if count == 0 {
return Ok(Vec::new());
}
let cap = unsafe {
ffi::leptris_element_attributes_snapshot(
elem,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
)
};
if cap == usize::MAX {
return Err(JsError::new("leptris attribute snapshot sizing failed"));
}
for _ in 0..2 {
let mut names = vec![0u8; cap];
let mut values = vec![0u8; cap];
let mut prefixes = vec![0u8; cap];
let mut uris = vec![0u8; cap];
let mut name_offsets = vec![0usize; count + 1];
let mut value_offsets = vec![0usize; count + 1];
let mut prefix_offsets = vec![0usize; count + 1];
let mut uri_offsets = vec![0usize; count + 1];
let st = unsafe {
ffi::leptris_element_attributes_snapshot(
elem,
names.as_mut_ptr() as *mut c_char,
cap,
name_offsets.as_mut_ptr(),
values.as_mut_ptr() as *mut c_char,
cap,
value_offsets.as_mut_ptr(),
prefixes.as_mut_ptr() as *mut c_char,
cap,
prefix_offsets.as_mut_ptr(),
uris.as_mut_ptr() as *mut c_char,
cap,
uri_offsets.as_mut_ptr(),
)
};
if st == 0 {
let lane = |blob: &[u8], offsets: &[usize], i: usize| -> String {
let start = offsets[i];
let end = offsets[i + 1];
String::from_utf8_lossy(&blob[start..end.max(start)])
.trim_end_matches('\0')
.to_string()
};
let mut out = Vec::with_capacity(count);
for i in 0..count {
let no_ns = uri_offsets[i] == usize::MAX;
out.push(WasmAttribute {
name: lane(&names, &name_offsets, i),
value: lane(&values, &value_offsets, i),
prefix: if no_ns {
None
} else {
Some(lane(&prefixes, &prefix_offsets, i))
},
uri: if no_ns {
None
} else {
Some(lane(&uris, &uri_offsets, i))
},
});
}
return Ok(out);
}
}
Err(JsError::new("leptris attribute snapshot failed"))
}
pub fn add_child(&self, child: WasmNode) -> Result<WasmNode, JsError> {
if !child.doc.eq(&self.doc) {
return Err(JsError::new(
"add_child: cross-document adoption is not served by this face",
));
}
let mut installed: usize = 0;
let st = unsafe {
ffi::leptris_element_add_child_addr_installed(
self.node as usize,
child.node as usize,
&mut installed,
)
};
if st != ffi::LEPTRIS_OK || installed == 0 {
return Err(JsError::new("leptris add_child failed"));
}
Ok(WasmNode {
doc: self.doc,
node: installed as ffi::LeptrisNodeRef,
})
}
pub fn ptr(&self) -> usize {
self.node as usize
}
}
#[wasm_bindgen]
pub struct WasmXPathResult {
kind: i32,
string_value: String,
boolean: bool,
number: f64,
nodes: Vec<WasmNode>,
}
#[wasm_bindgen]
impl WasmXPathResult {
pub fn kind(&self) -> i32 {
self.kind
}
#[wasm_bindgen(js_name = stringValue)]
pub fn string_value(&self) -> String {
self.string_value.clone()
}
pub fn boolean(&self) -> bool {
self.boolean
}
pub fn number(&self) -> f64 {
self.number
}
pub fn nodes(&self) -> Vec<WasmNode> {
self.nodes.clone()
}
}
#[wasm_bindgen]
impl WasmDocument {
pub fn xpath_full(&self, expression: &str) -> Result<WasmXPathResult, JsError> {
let cexpr =
CString::new(expression).map_err(|_| JsError::new("xpath contains a NUL byte"))?;
let result =
unsafe { ffi::leptris_xpath_eval(self.raw, std::ptr::null_mut(), cexpr.as_ptr()) };
if result.is_null() {
return Err(JsError::new("leptris xpath evaluation failed"));
}
unsafe {
let kind = ffi::leptris_xpath_result_type(result);
let sv = take_string(ffi::leptris_xpath_result_string(result)).unwrap_or_default();
let boolean = ffi::leptris_xpath_result_boolean(result) != 0;
let number = ffi::leptris_xpath_result_number(result);
let mut nodes = Vec::new();
if kind == ffi::LEPTRIS_XPATH_NODESET {
let n = ffi::leptris_xpath_result_count(result);
let mut handles: Vec<ffi::LeptrisNodeRef> = vec![std::ptr::null_mut(); n];
let got = ffi::leptris_xpath_result_get_nodes_ex(
result,
handles.as_mut_ptr(),
std::ptr::null_mut(),
n,
);
handles.truncate(got);
nodes = handles
.into_iter()
.map(|node| WasmNode {
doc: self.raw,
node,
})
.collect();
}
ffi::leptris_xpath_result_free(result);
Ok(WasmXPathResult {
kind,
string_value: sv,
boolean,
number,
nodes,
})
}
}
pub fn root(&self) -> Option<WasmNode> {
let root = unsafe { ffi::leptris_document_root(self.raw) };
if root.is_null() {
return None;
}
Some(WasmNode {
doc: self.raw,
node: root as ffi::LeptrisNodeRef,
})
}
pub fn create_element(&self, name: &str) -> Result<WasmNode, JsError> {
let cname =
CString::new(name).map_err(|_| JsError::new("element name contains a NUL byte"))?;
let elem = unsafe {
ffi::leptris_element_new_with_attributes(
self.raw,
cname.as_ptr(),
std::ptr::null(),
std::ptr::null(),
0,
)
};
if elem.is_null() {
return Err(JsError::new("leptris create_element failed"));
}
Ok(WasmNode {
doc: self.raw,
node: elem as ffi::LeptrisNodeRef,
})
}
pub fn create_text(&self, text: &str) -> Result<WasmNode, JsError> {
let ctext = CString::new(text).map_err(|_| JsError::new("text contains a NUL byte"))?;
let node = unsafe { ffi::leptris_text_node_create(self.raw, ctext.as_ptr()) };
if node.is_null() {
return Err(JsError::new("leptris create_text failed"));
}
Ok(WasmNode {
doc: self.raw,
node,
})
}
pub fn set_root(&self, root: WasmNode) -> Result<(), JsError> {
let st = unsafe {
ffi::leptris_document_set_root(self.raw, root.node as *mut ffi::LeptrisElement)
};
if st != ffi::LEPTRIS_OK {
return Err(JsError::new("leptris set_root failed"));
}
Ok(())
}
}