#![cfg_attr(not(test), warn(missing_docs))]
use hedl_c14n::CanonicalConfig;
use hedl_core::{parse as core_parse, Document};
use std::sync::atomic::{AtomicUsize, Ordering};
use wasm_bindgen::prelude::*;
#[cfg(feature = "full-validation")]
use hedl_lint::lint;
mod document;
mod stats;
mod validation;
#[cfg(test)]
mod tests;
use document::count_item_entities;
#[cfg(feature = "query-api")]
use document::find_entities;
#[cfg(any(feature = "statistics", feature = "token-tools"))]
use stats::{estimate_tokens, TokenStats};
#[cfg(feature = "full-validation")]
use validation::ValidationWarning;
use validation::{ValidationError, ValidationResult};
#[wasm_bindgen(typescript_custom_section)]
const TS_CUSTOM_TYPES: &'static str = r#"
/**
* Represents a JSON primitive value.
*/
export type JsonPrimitive = string | number | boolean | null;
/**
* Represents a JSON array (recursive).
*/
export type JsonArray = JsonValue[];
/**
* Represents a JSON object (recursive).
*/
export type JsonObject = { [key: string]: JsonValue };
/**
* Represents any valid JSON value.
*/
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
"#;
pub const DEFAULT_MAX_INPUT_SIZE: usize = 500 * 1024 * 1024;
static MAX_INPUT_SIZE: AtomicUsize = AtomicUsize::new(DEFAULT_MAX_INPUT_SIZE);
#[cfg(feature = "json")]
use hedl_json::{from_json_value, to_json_value, FromJsonConfig, ToJsonConfig};
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(debug_assertions)]
console_error_panic_hook::set_once();
#[cfg(not(debug_assertions))]
std::panic::set_hook(Box::new(|_| {
web_sys::console::error_1(&"HEDL: An internal error occurred".into());
}));
}
#[wasm_bindgen]
#[must_use]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[wasm_bindgen(js_name = setMaxInputSize)]
pub fn set_max_input_size(size: usize) {
MAX_INPUT_SIZE.store(size, Ordering::Relaxed);
}
#[wasm_bindgen(js_name = getMaxInputSize)]
pub fn get_max_input_size() -> usize {
MAX_INPUT_SIZE.load(Ordering::Relaxed)
}
#[inline(always)]
fn check_input_size(input: &str) -> Result<(), JsError> {
let max_size = MAX_INPUT_SIZE.load(Ordering::Relaxed);
let input_size = input.len();
if input_size > max_size {
return Err(JsError::new(&format!(
"Input size ({} bytes, {} MB) exceeds maximum allowed size ({} bytes, {} MB). \
Use setMaxInputSize() to increase the limit if needed.",
input_size,
input_size / (1024 * 1024),
max_size,
max_size / (1024 * 1024)
)));
}
Ok(())
}
#[wasm_bindgen]
pub struct HedlDocument {
inner: Document,
}
#[wasm_bindgen]
impl HedlDocument {
#[wasm_bindgen(getter)]
#[must_use]
pub fn version(&self) -> String {
format!("{}.{}", self.inner.version.0, self.inner.version.1)
}
#[wasm_bindgen(getter, js_name = schemaCount)]
#[must_use]
pub fn schema_count(&self) -> usize {
self.inner.structs.len()
}
#[wasm_bindgen(getter, js_name = aliasCount)]
#[must_use]
pub fn alias_count(&self) -> usize {
self.inner.aliases.len()
}
#[wasm_bindgen(getter, js_name = nestCount)]
#[must_use]
pub fn nest_count(&self) -> usize {
self.inner.nests.len()
}
#[wasm_bindgen(getter, js_name = rootItemCount)]
#[must_use]
pub fn root_item_count(&self) -> usize {
self.inner.root.len()
}
#[wasm_bindgen(js_name = getSchemaNames)]
#[must_use]
pub fn get_schema_names(&self) -> Vec<String> {
self.inner.structs.keys().cloned().collect()
}
#[wasm_bindgen(js_name = getSchema)]
#[must_use]
pub fn get_schema(&self, type_name: &str) -> Option<Vec<String>> {
self.inner.structs.get(type_name).cloned()
}
#[wasm_bindgen(js_name = getAliases)]
#[must_use]
pub fn get_aliases(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.inner.aliases).unwrap_or(JsValue::NULL)
}
#[wasm_bindgen(js_name = getNests)]
#[must_use]
pub fn get_nests(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.inner.nests).unwrap_or(JsValue::NULL)
}
#[cfg(feature = "json")]
#[wasm_bindgen(js_name = toJson)]
#[must_use]
pub fn to_json(&self) -> JsValue {
let config = ToJsonConfig::default();
match to_json_value(&self.inner, &config) {
Ok(json) => serde_wasm_bindgen::to_value(&json).unwrap_or(JsValue::NULL),
Err(_) => JsValue::NULL,
}
}
#[cfg(feature = "json")]
#[wasm_bindgen(js_name = toJsonString)]
pub fn to_json_string(&self, pretty: Option<bool>) -> Result<String, JsError> {
let config = ToJsonConfig::default();
let json = to_json_value(&self.inner, &config).map_err(|e| JsError::new(&e))?;
if pretty.unwrap_or(true) {
serde_json::to_string_pretty(&json).map_err(|e| JsError::new(&e.to_string()))
} else {
serde_json::to_string(&json).map_err(|e| JsError::new(&e.to_string()))
}
}
#[wasm_bindgen(js_name = toHedl)]
pub fn to_hedl(&self) -> Result<String, JsError> {
let config = CanonicalConfig::default();
hedl_c14n::canonicalize_with_config(&self.inner, &config)
.map_err(|e| JsError::new(&e.to_string()))
}
#[wasm_bindgen(js_name = countEntities)]
#[must_use]
pub fn count_entities(&self) -> JsValue {
let mut counts: std::collections::BTreeMap<String, usize> =
std::collections::BTreeMap::new();
for item in self.inner.root.values() {
count_item_entities(item, &mut counts);
}
serde_wasm_bindgen::to_value(&counts).unwrap_or(JsValue::NULL)
}
#[cfg(feature = "query-api")]
#[wasm_bindgen]
#[must_use]
pub fn query(&self, type_name: Option<String>, id: Option<String>) -> JsValue {
let mut results = Vec::new();
for item in self.inner.root.values() {
find_entities(item, &type_name, &id, &mut results);
}
serde_wasm_bindgen::to_value(&results).unwrap_or(JsValue::NULL)
}
}
#[wasm_bindgen]
pub fn parse(input: &str) -> Result<HedlDocument, JsError> {
check_input_size(input)?;
core_parse(input.as_bytes())
.map(|doc| HedlDocument { inner: doc })
.map_err(|e| JsError::new(&format!("Parse error at line {}: {}", e.line, e.message)))
}
#[cfg(feature = "json")]
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(hedl: &str, pretty: Option<bool>) -> Result<String, JsError> {
check_input_size(hedl)?;
let doc = core_parse(hedl.as_bytes())
.map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
let config = ToJsonConfig::default();
let json = to_json_value(&doc, &config).map_err(|e| JsError::new(&e))?;
if pretty.unwrap_or(true) {
serde_json::to_string_pretty(&json).map_err(|e| JsError::new(&e.to_string()))
} else {
serde_json::to_string(&json).map_err(|e| JsError::new(&e.to_string()))
}
}
#[cfg(feature = "json")]
#[wasm_bindgen(js_name = fromJson)]
pub fn from_json(json: &str) -> Result<String, JsError> {
check_input_size(json)?;
let json_value: serde_json::Value =
serde_json::from_str(json).map_err(|e| JsError::new(&format!("Invalid JSON: {e}")))?;
let config = FromJsonConfig::default();
let doc = from_json_value(&json_value, &config)
.map_err(|e| JsError::new(&format!("Conversion error: {e}")))?;
let c14n_config = CanonicalConfig::default();
hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
.map_err(|e| JsError::new(&format!("Format error: {e}")))
}
#[cfg(feature = "yaml")]
#[wasm_bindgen(js_name = toYaml)]
pub fn to_yaml(hedl: &str) -> Result<String, JsError> {
check_input_size(hedl)?;
let doc = core_parse(hedl.as_bytes())
.map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
let config = hedl_yaml::ToYamlConfig::default();
hedl_yaml::to_yaml(&doc, &config)
.map_err(|e| JsError::new(&format!("YAML conversion error: {e}")))
}
#[cfg(feature = "yaml")]
#[wasm_bindgen(js_name = fromYaml)]
pub fn from_yaml(yaml: &str) -> Result<String, JsError> {
check_input_size(yaml)?;
let config = hedl_yaml::FromYamlConfig::default();
let doc = hedl_yaml::from_yaml(yaml, &config)
.map_err(|e| JsError::new(&format!("YAML parse error: {e}")))?;
let c14n_config = CanonicalConfig::default();
hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
.map_err(|e| JsError::new(&format!("Format error: {e}")))
}
#[cfg(feature = "xml")]
#[wasm_bindgen(js_name = toXml)]
pub fn to_xml(hedl: &str) -> Result<String, JsError> {
check_input_size(hedl)?;
let doc = core_parse(hedl.as_bytes())
.map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
let config = hedl_xml::ToXmlConfig::default();
hedl_xml::to_xml(&doc, &config).map_err(|e| JsError::new(&format!("XML conversion error: {e}")))
}
#[cfg(feature = "xml")]
#[wasm_bindgen(js_name = fromXml)]
pub fn from_xml(xml: &str) -> Result<String, JsError> {
check_input_size(xml)?;
let config = hedl_xml::FromXmlConfig::default();
let doc = hedl_xml::from_xml(xml, &config)
.map_err(|e| JsError::new(&format!("XML parse error: {e}")))?;
let c14n_config = CanonicalConfig::default();
hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
.map_err(|e| JsError::new(&format!("Format error: {e}")))
}
#[cfg(feature = "csv")]
#[wasm_bindgen(js_name = toCsv)]
pub fn to_csv(hedl: &str) -> Result<String, JsError> {
check_input_size(hedl)?;
let doc = core_parse(hedl.as_bytes())
.map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
hedl_csv::to_csv(&doc).map_err(|e| JsError::new(&format!("CSV conversion error: {e}")))
}
#[cfg(feature = "csv")]
#[wasm_bindgen(js_name = fromCsv)]
pub fn from_csv(csv: &str, type_name: Option<String>) -> Result<String, JsError> {
check_input_size(csv)?;
let mut lines = csv.lines();
let header = lines
.next()
.ok_or_else(|| JsError::new("CSV must have a header row"))?;
let all_columns: Vec<&str> = header.split(',').map(str::trim).collect();
let schema: Vec<&str> =
if all_columns.first().map(|s| s.to_lowercase()) == Some("id".to_string()) {
all_columns[1..].to_vec()
} else {
all_columns
};
let type_name = type_name.unwrap_or_else(|| "Row".to_string());
let doc = hedl_csv::from_csv(csv, &type_name, &schema)
.map_err(|e| JsError::new(&format!("CSV parse error: {e}")))?;
let c14n_config = CanonicalConfig::default();
hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
.map_err(|e| JsError::new(&format!("Format error: {e}")))
}
#[cfg(feature = "toon")]
#[wasm_bindgen(js_name = toToon)]
pub fn to_toon(hedl: &str) -> Result<String, JsError> {
check_input_size(hedl)?;
let doc = core_parse(hedl.as_bytes())
.map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
hedl_toon::hedl_to_toon(&doc).map_err(|e| JsError::new(&format!("TOON conversion error: {e}")))
}
#[cfg(feature = "toon")]
#[wasm_bindgen(js_name = fromToon)]
pub fn from_toon(toon: &str) -> Result<String, JsError> {
check_input_size(toon)?;
let doc = hedl_toon::toon_to_hedl(toon)
.map_err(|e| JsError::new(&format!("TOON parse error: {e}")))?;
let c14n_config = CanonicalConfig::default();
hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
.map_err(|e| JsError::new(&format!("Format error: {e}")))
}
#[wasm_bindgen]
pub fn format(hedl: &str) -> Result<String, JsError> {
check_input_size(hedl)?;
let doc = core_parse(hedl.as_bytes())
.map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
let config = CanonicalConfig::default();
hedl_c14n::canonicalize_with_config(&doc, &config)
.map_err(|e| JsError::new(&format!("Format error: {e}")))
}
#[wasm_bindgen]
#[must_use]
pub fn validate(hedl: &str, run_lint: Option<bool>) -> JsValue {
if let Err(e) = check_input_size(hedl) {
let result =
ValidationResult::with_error(0, format!("{e:?}"), "InputSizeError".to_string());
return serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL);
}
let mut result = ValidationResult::new();
match core_parse(hedl.as_bytes()) {
Ok(_doc) => {
#[cfg(feature = "full-validation")]
{
if run_lint.unwrap_or(true) {
let diagnostics = lint(&_doc);
for diag in diagnostics {
match diag.severity() {
hedl_lint::Severity::Error => {
result.valid = false;
result.errors.push(ValidationError {
line: diag.line().unwrap_or(0),
message: diag.message().to_string(),
error_type: diag.rule_id().to_string(),
});
}
hedl_lint::Severity::Warning | hedl_lint::Severity::Hint => {
result.warnings.push(ValidationWarning {
line: diag.line().unwrap_or(0),
message: diag.message().to_string(),
rule: diag.rule_id().to_string(),
});
}
}
}
}
}
#[cfg(not(feature = "full-validation"))]
{
let _ = run_lint; }
}
Err(e) => {
result.valid = false;
result.errors.push(ValidationError {
line: e.line,
message: e.message,
error_type: format!("{:?}", e.kind),
});
}
}
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
}
#[cfg(feature = "statistics")]
#[wasm_bindgen(js_name = getStats)]
pub fn get_stats(hedl: &str) -> Result<JsValue, JsError> {
check_input_size(hedl)?;
let doc = core_parse(hedl.as_bytes())
.map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
let config = ToJsonConfig::default();
let json_value = to_json_value(&doc, &config).map_err(|e| JsError::new(&e))?;
let json_str = serde_json::to_string(&json_value).map_err(|e| JsError::new(&e.to_string()))?;
let hedl_tokens = estimate_tokens(hedl);
let json_tokens = estimate_tokens(&json_str);
let savings_percent = if json_tokens > 0 {
((json_tokens as i64 - hedl_tokens as i64) * 100 / json_tokens as i64) as i32
} else {
0
};
let stats = TokenStats {
hedl_bytes: hedl.len(),
hedl_tokens,
hedl_lines: hedl.lines().count(),
json_bytes: json_str.len(),
json_tokens,
savings_percent,
tokens_saved: (json_tokens as i32) - (hedl_tokens as i32),
};
serde_wasm_bindgen::to_value(&stats).map_err(|e| JsError::new(&e.to_string()))
}
#[cfg(feature = "token-tools")]
#[wasm_bindgen(js_name = compareTokens)]
#[must_use]
pub fn compare_tokens(hedl: &str, json: &str) -> JsValue {
let hedl_tokens = estimate_tokens(hedl);
let json_tokens = estimate_tokens(json);
let savings = if json_tokens > 0 {
((json_tokens as i64 - hedl_tokens as i64) * 100 / json_tokens as i64) as i32
} else {
0
};
let result = serde_json::json!({
"hedl": {
"bytes": hedl.len(),
"tokens": hedl_tokens,
"lines": hedl.lines().count()
},
"json": {
"bytes": json.len(),
"tokens": json_tokens
},
"savings": {
"percent": savings,
"tokens": json_tokens as i32 - hedl_tokens as i32
}
});
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
}