1#![cfg_attr(not(test), warn(missing_docs))]
61use hedl_c14n::CanonicalConfig;
62use hedl_core::{parse as core_parse, Document};
63use std::sync::atomic::{AtomicUsize, Ordering};
64use wasm_bindgen::prelude::*;
65
66#[cfg(feature = "full-validation")]
67use hedl_lint::lint;
68
69mod document;
71mod stats;
72mod validation;
73
74#[cfg(test)]
75mod tests;
76
77use document::count_item_entities;
79#[cfg(feature = "query-api")]
80use document::find_entities;
81#[cfg(any(feature = "statistics", feature = "token-tools"))]
82use stats::{estimate_tokens, TokenStats};
83#[cfg(feature = "full-validation")]
84use validation::ValidationWarning;
85use validation::{ValidationError, ValidationResult};
86
87#[wasm_bindgen(typescript_custom_section)]
89const TS_CUSTOM_TYPES: &'static str = r#"
90/**
91 * Represents a JSON primitive value.
92 */
93export type JsonPrimitive = string | number | boolean | null;
94
95/**
96 * Represents a JSON array (recursive).
97 */
98export type JsonArray = JsonValue[];
99
100/**
101 * Represents a JSON object (recursive).
102 */
103export type JsonObject = { [key: string]: JsonValue };
104
105/**
106 * Represents any valid JSON value.
107 */
108export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
109"#;
110
111pub const DEFAULT_MAX_INPUT_SIZE: usize = 500 * 1024 * 1024; static MAX_INPUT_SIZE: AtomicUsize = AtomicUsize::new(DEFAULT_MAX_INPUT_SIZE);
119
120#[cfg(feature = "json")]
122use hedl_json::{from_json_value, to_json_value, FromJsonConfig, ToJsonConfig};
123
124#[wasm_bindgen(start)]
129pub fn init() {
130 #[cfg(debug_assertions)]
131 console_error_panic_hook::set_once();
132
133 #[cfg(not(debug_assertions))]
134 std::panic::set_hook(Box::new(|_| {
135 web_sys::console::error_1(&"HEDL: An internal error occurred".into());
137 }));
138}
139
140#[wasm_bindgen]
142#[must_use]
143pub fn version() -> String {
144 env!("CARGO_PKG_VERSION").to_string()
145}
146
147#[wasm_bindgen(js_name = setMaxInputSize)]
163pub fn set_max_input_size(size: usize) {
164 MAX_INPUT_SIZE.store(size, Ordering::Relaxed);
165}
166
167#[wasm_bindgen(js_name = getMaxInputSize)]
180pub fn get_max_input_size() -> usize {
181 MAX_INPUT_SIZE.load(Ordering::Relaxed)
182}
183
184#[inline(always)]
186fn check_input_size(input: &str) -> Result<(), JsError> {
187 let max_size = MAX_INPUT_SIZE.load(Ordering::Relaxed);
188 let input_size = input.len();
189
190 if input_size > max_size {
191 return Err(JsError::new(&format!(
192 "Input size ({} bytes, {} MB) exceeds maximum allowed size ({} bytes, {} MB). \
193 Use setMaxInputSize() to increase the limit if needed.",
194 input_size,
195 input_size / (1024 * 1024),
196 max_size,
197 max_size / (1024 * 1024)
198 )));
199 }
200
201 Ok(())
202}
203
204#[wasm_bindgen]
208pub struct HedlDocument {
209 inner: Document,
210}
211
212#[wasm_bindgen]
213impl HedlDocument {
214 #[wasm_bindgen(getter)]
216 #[must_use]
217 pub fn version(&self) -> String {
218 format!("{}.{}", self.inner.version.0, self.inner.version.1)
219 }
220
221 #[wasm_bindgen(getter, js_name = schemaCount)]
223 #[must_use]
224 pub fn schema_count(&self) -> usize {
225 self.inner.structs.len()
226 }
227
228 #[wasm_bindgen(getter, js_name = aliasCount)]
230 #[must_use]
231 pub fn alias_count(&self) -> usize {
232 self.inner.aliases.len()
233 }
234
235 #[wasm_bindgen(getter, js_name = nestCount)]
237 #[must_use]
238 pub fn nest_count(&self) -> usize {
239 self.inner.nests.len()
240 }
241
242 #[wasm_bindgen(getter, js_name = rootItemCount)]
244 #[must_use]
245 pub fn root_item_count(&self) -> usize {
246 self.inner.root.len()
247 }
248
249 #[wasm_bindgen(js_name = getSchemaNames)]
251 #[must_use]
252 pub fn get_schema_names(&self) -> Vec<String> {
253 self.inner.structs.keys().cloned().collect()
254 }
255
256 #[wasm_bindgen(js_name = getSchema)]
258 #[must_use]
259 pub fn get_schema(&self, type_name: &str) -> Option<Vec<String>> {
260 self.inner.structs.get(type_name).cloned()
261 }
262
263 #[wasm_bindgen(js_name = getAliases)]
268 #[must_use]
269 pub fn get_aliases(&self) -> JsValue {
270 serde_wasm_bindgen::to_value(&self.inner.aliases).unwrap_or(JsValue::NULL)
271 }
272
273 #[wasm_bindgen(js_name = getNests)]
278 #[must_use]
279 pub fn get_nests(&self) -> JsValue {
280 serde_wasm_bindgen::to_value(&self.inner.nests).unwrap_or(JsValue::NULL)
281 }
282
283 #[cfg(feature = "json")]
295 #[wasm_bindgen(js_name = toJson)]
296 #[must_use]
297 pub fn to_json(&self) -> JsValue {
298 let config = ToJsonConfig::default();
299 match to_json_value(&self.inner, &config) {
300 Ok(json) => serde_wasm_bindgen::to_value(&json).unwrap_or(JsValue::NULL),
301 Err(_) => JsValue::NULL,
302 }
303 }
304
305 #[cfg(feature = "json")]
310 #[wasm_bindgen(js_name = toJsonString)]
311 pub fn to_json_string(&self, pretty: Option<bool>) -> Result<String, JsError> {
312 let config = ToJsonConfig::default();
313 let json = to_json_value(&self.inner, &config).map_err(|e| JsError::new(&e))?;
314
315 if pretty.unwrap_or(true) {
316 serde_json::to_string_pretty(&json).map_err(|e| JsError::new(&e.to_string()))
317 } else {
318 serde_json::to_string(&json).map_err(|e| JsError::new(&e.to_string()))
319 }
320 }
321
322 #[wasm_bindgen(js_name = toHedl)]
326 pub fn to_hedl(&self) -> Result<String, JsError> {
327 let config = CanonicalConfig::default();
328 hedl_c14n::canonicalize_with_config(&self.inner, &config)
329 .map_err(|e| JsError::new(&e.to_string()))
330 }
331
332 #[wasm_bindgen(js_name = countEntities)]
334 #[must_use]
335 pub fn count_entities(&self) -> JsValue {
336 let mut counts: std::collections::BTreeMap<String, usize> =
337 std::collections::BTreeMap::new();
338
339 for item in self.inner.root.values() {
340 count_item_entities(item, &mut counts);
341 }
342
343 serde_wasm_bindgen::to_value(&counts).unwrap_or(JsValue::NULL)
344 }
345
346 #[cfg(feature = "query-api")]
361 #[wasm_bindgen]
362 #[must_use]
363 pub fn query(&self, type_name: Option<String>, id: Option<String>) -> JsValue {
364 let mut results = Vec::new();
365
366 for item in self.inner.root.values() {
367 find_entities(item, &type_name, &id, &mut results);
368 }
369
370 serde_wasm_bindgen::to_value(&results).unwrap_or(JsValue::NULL)
371 }
372}
373
374#[wasm_bindgen]
401pub fn parse(input: &str) -> Result<HedlDocument, JsError> {
402 check_input_size(input)?;
403 core_parse(input.as_bytes())
404 .map(|doc| HedlDocument { inner: doc })
405 .map_err(|e| JsError::new(&format!("Parse error at line {}: {}", e.line, e.message)))
406}
407
408#[cfg(feature = "json")]
422#[wasm_bindgen(js_name = toJson)]
423pub fn to_json(hedl: &str, pretty: Option<bool>) -> Result<String, JsError> {
424 check_input_size(hedl)?;
425 let doc = core_parse(hedl.as_bytes())
426 .map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
427
428 let config = ToJsonConfig::default();
429 let json = to_json_value(&doc, &config).map_err(|e| JsError::new(&e))?;
430
431 if pretty.unwrap_or(true) {
432 serde_json::to_string_pretty(&json).map_err(|e| JsError::new(&e.to_string()))
433 } else {
434 serde_json::to_string(&json).map_err(|e| JsError::new(&e.to_string()))
435 }
436}
437
438#[cfg(feature = "json")]
451#[wasm_bindgen(js_name = fromJson)]
452pub fn from_json(json: &str) -> Result<String, JsError> {
453 check_input_size(json)?;
454 let json_value: serde_json::Value =
455 serde_json::from_str(json).map_err(|e| JsError::new(&format!("Invalid JSON: {e}")))?;
456
457 let config = FromJsonConfig::default();
458 let doc = from_json_value(&json_value, &config)
459 .map_err(|e| JsError::new(&format!("Conversion error: {e}")))?;
460
461 let c14n_config = CanonicalConfig::default();
462 hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
463 .map_err(|e| JsError::new(&format!("Format error: {e}")))
464}
465
466#[cfg(feature = "yaml")]
481#[wasm_bindgen(js_name = toYaml)]
482pub fn to_yaml(hedl: &str) -> Result<String, JsError> {
483 check_input_size(hedl)?;
484 let doc = core_parse(hedl.as_bytes())
485 .map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
486
487 let config = hedl_yaml::ToYamlConfig::default();
488 hedl_yaml::to_yaml(&doc, &config)
489 .map_err(|e| JsError::new(&format!("YAML conversion error: {e}")))
490}
491
492#[cfg(feature = "yaml")]
505#[wasm_bindgen(js_name = fromYaml)]
506pub fn from_yaml(yaml: &str) -> Result<String, JsError> {
507 check_input_size(yaml)?;
508
509 let config = hedl_yaml::FromYamlConfig::default();
510 let doc = hedl_yaml::from_yaml(yaml, &config)
511 .map_err(|e| JsError::new(&format!("YAML parse error: {e}")))?;
512
513 let c14n_config = CanonicalConfig::default();
514 hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
515 .map_err(|e| JsError::new(&format!("Format error: {e}")))
516}
517
518#[cfg(feature = "xml")]
533#[wasm_bindgen(js_name = toXml)]
534pub fn to_xml(hedl: &str) -> Result<String, JsError> {
535 check_input_size(hedl)?;
536 let doc = core_parse(hedl.as_bytes())
537 .map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
538
539 let config = hedl_xml::ToXmlConfig::default();
540 hedl_xml::to_xml(&doc, &config).map_err(|e| JsError::new(&format!("XML conversion error: {e}")))
541}
542
543#[cfg(feature = "xml")]
556#[wasm_bindgen(js_name = fromXml)]
557pub fn from_xml(xml: &str) -> Result<String, JsError> {
558 check_input_size(xml)?;
559
560 let config = hedl_xml::FromXmlConfig::default();
561 let doc = hedl_xml::from_xml(xml, &config)
562 .map_err(|e| JsError::new(&format!("XML parse error: {e}")))?;
563
564 let c14n_config = CanonicalConfig::default();
565 hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
566 .map_err(|e| JsError::new(&format!("Format error: {e}")))
567}
568
569#[cfg(feature = "csv")]
584#[wasm_bindgen(js_name = toCsv)]
585pub fn to_csv(hedl: &str) -> Result<String, JsError> {
586 check_input_size(hedl)?;
587 let doc = core_parse(hedl.as_bytes())
588 .map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
589
590 hedl_csv::to_csv(&doc).map_err(|e| JsError::new(&format!("CSV conversion error: {e}")))
591}
592
593#[cfg(feature = "csv")]
611#[wasm_bindgen(js_name = fromCsv)]
612pub fn from_csv(csv: &str, type_name: Option<String>) -> Result<String, JsError> {
613 check_input_size(csv)?;
614
615 let mut lines = csv.lines();
617 let header = lines
618 .next()
619 .ok_or_else(|| JsError::new("CSV must have a header row"))?;
620 let all_columns: Vec<&str> = header.split(',').map(str::trim).collect();
621
622 let schema: Vec<&str> =
625 if all_columns.first().map(|s| s.to_lowercase()) == Some("id".to_string()) {
626 all_columns[1..].to_vec()
627 } else {
628 all_columns
629 };
630
631 let type_name = type_name.unwrap_or_else(|| "Row".to_string());
632
633 let doc = hedl_csv::from_csv(csv, &type_name, &schema)
634 .map_err(|e| JsError::new(&format!("CSV parse error: {e}")))?;
635
636 let c14n_config = CanonicalConfig::default();
637 hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
638 .map_err(|e| JsError::new(&format!("Format error: {e}")))
639}
640
641#[cfg(feature = "toon")]
659#[wasm_bindgen(js_name = toToon)]
660pub fn to_toon(hedl: &str) -> Result<String, JsError> {
661 check_input_size(hedl)?;
662 let doc = core_parse(hedl.as_bytes())
663 .map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
664
665 hedl_toon::hedl_to_toon(&doc).map_err(|e| JsError::new(&format!("TOON conversion error: {e}")))
666}
667
668#[cfg(feature = "toon")]
681#[wasm_bindgen(js_name = fromToon)]
682pub fn from_toon(toon: &str) -> Result<String, JsError> {
683 check_input_size(toon)?;
684
685 let doc = hedl_toon::toon_to_hedl(toon)
686 .map_err(|e| JsError::new(&format!("TOON parse error: {e}")))?;
687
688 let c14n_config = CanonicalConfig::default();
689 hedl_c14n::canonicalize_with_config(&doc, &c14n_config)
690 .map_err(|e| JsError::new(&format!("Format error: {e}")))
691}
692
693#[wasm_bindgen]
703pub fn format(hedl: &str) -> Result<String, JsError> {
704 check_input_size(hedl)?;
705 let doc = core_parse(hedl.as_bytes())
706 .map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
707
708 let config = CanonicalConfig::default();
709 hedl_c14n::canonicalize_with_config(&doc, &config)
710 .map_err(|e| JsError::new(&format!("Format error: {e}")))
711}
712
713#[wasm_bindgen]
727#[must_use]
728pub fn validate(hedl: &str, run_lint: Option<bool>) -> JsValue {
729 if let Err(e) = check_input_size(hedl) {
731 let result =
732 ValidationResult::with_error(0, format!("{e:?}"), "InputSizeError".to_string());
733 return serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL);
734 }
735
736 let mut result = ValidationResult::new();
737
738 match core_parse(hedl.as_bytes()) {
739 Ok(_doc) => {
740 #[cfg(feature = "full-validation")]
741 {
742 if run_lint.unwrap_or(true) {
743 let diagnostics = lint(&_doc);
744
745 for diag in diagnostics {
746 match diag.severity() {
747 hedl_lint::Severity::Error => {
748 result.valid = false;
749 result.errors.push(ValidationError {
750 line: diag.line().unwrap_or(0),
751 message: diag.message().to_string(),
752 error_type: diag.rule_id().to_string(),
753 });
754 }
755 hedl_lint::Severity::Warning | hedl_lint::Severity::Hint => {
756 result.warnings.push(ValidationWarning {
757 line: diag.line().unwrap_or(0),
758 message: diag.message().to_string(),
759 rule: diag.rule_id().to_string(),
760 });
761 }
762 }
763 }
764 }
765 }
766
767 #[cfg(not(feature = "full-validation"))]
768 {
769 let _ = run_lint; }
772 }
773 Err(e) => {
774 result.valid = false;
775 result.errors.push(ValidationError {
776 line: e.line,
777 message: e.message,
778 error_type: format!("{:?}", e.kind),
779 });
780 }
781 }
782
783 serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
784}
785
786#[cfg(feature = "statistics")]
801#[wasm_bindgen(js_name = getStats)]
802pub fn get_stats(hedl: &str) -> Result<JsValue, JsError> {
803 check_input_size(hedl)?;
804 let doc = core_parse(hedl.as_bytes())
805 .map_err(|e| JsError::new(&format!("Parse error: {}", e.message)))?;
806
807 let config = ToJsonConfig::default();
808 let json_value = to_json_value(&doc, &config).map_err(|e| JsError::new(&e))?;
809 let json_str = serde_json::to_string(&json_value).map_err(|e| JsError::new(&e.to_string()))?;
810
811 let hedl_tokens = estimate_tokens(hedl);
812 let json_tokens = estimate_tokens(&json_str);
813
814 let savings_percent = if json_tokens > 0 {
815 ((json_tokens as i64 - hedl_tokens as i64) * 100 / json_tokens as i64) as i32
816 } else {
817 0
818 };
819
820 let stats = TokenStats {
821 hedl_bytes: hedl.len(),
822 hedl_tokens,
823 hedl_lines: hedl.lines().count(),
824 json_bytes: json_str.len(),
825 json_tokens,
826 savings_percent,
827 tokens_saved: (json_tokens as i32) - (hedl_tokens as i32),
828 };
829
830 serde_wasm_bindgen::to_value(&stats).map_err(|e| JsError::new(&e.to_string()))
831}
832
833#[cfg(feature = "token-tools")]
840#[wasm_bindgen(js_name = compareTokens)]
841#[must_use]
842pub fn compare_tokens(hedl: &str, json: &str) -> JsValue {
843 let hedl_tokens = estimate_tokens(hedl);
844 let json_tokens = estimate_tokens(json);
845
846 let savings = if json_tokens > 0 {
847 ((json_tokens as i64 - hedl_tokens as i64) * 100 / json_tokens as i64) as i32
848 } else {
849 0
850 };
851
852 let result = serde_json::json!({
853 "hedl": {
854 "bytes": hedl.len(),
855 "tokens": hedl_tokens,
856 "lines": hedl.lines().count()
857 },
858 "json": {
859 "bytes": json.len(),
860 "tokens": json_tokens
861 },
862 "savings": {
863 "percent": savings,
864 "tokens": json_tokens as i32 - hedl_tokens as i32
865 }
866 });
867
868 serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
869}