Skip to main content

hedl_wasm/
lib.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! HEDL WebAssembly Bindings
19//!
20//! This crate provides WebAssembly bindings for HEDL, enabling HEDL parsing
21//! and manipulation in browsers and other JavaScript/TypeScript environments.
22//!
23//! # Usage (JavaScript/TypeScript)
24//!
25//! ```typescript
26//! import init, { parse, toJson, fromJson, format, validate, getStats } from 'hedl-wasm';
27//!
28//! await init();
29//!
30//! // Parse HEDL
31//! const doc = parse(`
32//! %VERSION 1.0
33//! %STRUCT User[id, name, email]
34//! ---
35//! users:@User
36//!  | alice | Alice Smith | alice@example.com |
37//!  | bob   | Bob Jones   | bob@example.com   |
38//! `);
39//!
40//! // Convert to JSON
41//! const json = toJson(doc);
42//!
43//! // Convert JSON to HEDL
44//! const hedl = fromJson(jsonData);
45//!
46//! // Format HEDL
47//! const formatted = format(hedlString);
48//!
49//! // Validate HEDL
50//! const result = validate(hedlString);
51//! if (!result.valid) {
52//!     console.error(result.errors);
53//! }
54//!
55//! // Get token statistics
56//! const stats = getStats(hedlString);
57//! console.log(`Token savings: ${stats.savingsPercent}%`);
58//! ```
59
60#![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
69// Modules
70mod document;
71mod stats;
72mod validation;
73
74#[cfg(test)]
75mod tests;
76
77// Re-exports for internal use
78use 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// TypeScript custom type definitions for better type inference
88#[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
111/// Default maximum input size: 500 MB
112/// This is a conservative default that balances memory safety with practical use cases.
113/// Can be customized using `setMaxInputSize()` for larger documents.
114pub const DEFAULT_MAX_INPUT_SIZE: usize = 500 * 1024 * 1024; // 500 MB
115
116/// Global maximum input size configuration
117/// Uses atomic for thread-safe access in WASM context
118static MAX_INPUT_SIZE: AtomicUsize = AtomicUsize::new(DEFAULT_MAX_INPUT_SIZE);
119
120// Conditional imports for JSON feature
121#[cfg(feature = "json")]
122use hedl_json::{from_json_value, to_json_value, FromJsonConfig, ToJsonConfig};
123
124/// Initialize panic hook for error handling.
125///
126/// In debug builds, show full panic messages for debugging.
127/// In release builds, show generic message to avoid information disclosure.
128#[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        // Generic error message - avoids disclosing internal paths/state
136        web_sys::console::error_1(&"HEDL: An internal error occurred".into());
137    }));
138}
139
140/// HEDL version constant.
141#[wasm_bindgen]
142#[must_use]
143pub fn version() -> String {
144    env!("CARGO_PKG_VERSION").to_string()
145}
146
147/// Set the maximum input size in bytes.
148///
149/// This controls the maximum size of HEDL/JSON input strings that can be processed.
150/// Default is 500 MB. Set to a higher value if you need to process larger documents.
151///
152/// # Arguments
153/// * `size` - Maximum input size in bytes
154///
155/// # Example (JavaScript)
156/// ```javascript
157/// import { setMaxInputSize } from 'hedl-wasm';
158///
159/// // Allow processing up to 1 GB documents
160/// setMaxInputSize(1024 * 1024 * 1024);
161/// ```
162#[wasm_bindgen(js_name = setMaxInputSize)]
163pub fn set_max_input_size(size: usize) {
164    MAX_INPUT_SIZE.store(size, Ordering::Relaxed);
165}
166
167/// Get the current maximum input size in bytes.
168///
169/// # Returns
170/// Current maximum input size setting
171///
172/// # Example (JavaScript)
173/// ```javascript
174/// import { getMaxInputSize } from 'hedl-wasm';
175///
176/// const currentLimit = getMaxInputSize();
177/// console.log(`Current limit: ${currentLimit / (1024 * 1024)} MB`);
178/// ```
179#[wasm_bindgen(js_name = getMaxInputSize)]
180pub fn get_max_input_size() -> usize {
181    MAX_INPUT_SIZE.load(Ordering::Relaxed)
182}
183
184/// Validate input size against the configured limit.
185#[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// --- Parse Result Types ---
205
206/// Parsed HEDL document wrapper.
207#[wasm_bindgen]
208pub struct HedlDocument {
209    inner: Document,
210}
211
212#[wasm_bindgen]
213impl HedlDocument {
214    /// Get the HEDL version as a string (e.g., "1.0").
215    #[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    /// Get the number of schema definitions.
222    #[wasm_bindgen(getter, js_name = schemaCount)]
223    #[must_use]
224    pub fn schema_count(&self) -> usize {
225        self.inner.structs.len()
226    }
227
228    /// Get the number of alias definitions.
229    #[wasm_bindgen(getter, js_name = aliasCount)]
230    #[must_use]
231    pub fn alias_count(&self) -> usize {
232        self.inner.aliases.len()
233    }
234
235    /// Get the number of nest relationships.
236    #[wasm_bindgen(getter, js_name = nestCount)]
237    #[must_use]
238    pub fn nest_count(&self) -> usize {
239        self.inner.nests.len()
240    }
241
242    /// Get the number of root items.
243    #[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    /// Get all schema names.
250    #[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    /// Get schema columns for a type.
257    #[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    /// Get all aliases as a JSON object.
264    ///
265    /// Returns a JavaScript object mapping alias names to their resolved values.
266    /// Returns an empty object if there are no aliases.
267    #[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    /// Get all nest relationships as a JSON object.
274    ///
275    /// Returns a JavaScript object mapping parent type names to arrays of child type names.
276    /// Returns an empty object if there are no nest relationships.
277    #[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    /// Convert to JSON object.
284    ///
285    /// Returns the HEDL document as a structured JSON value that can be used
286    /// directly in JavaScript. The returned value conforms to the `JsonValue` type,
287    /// which is a recursive union of JSON primitives, objects, and arrays.
288    ///
289    /// # Feature
290    /// Requires the "json" feature to be enabled.
291    ///
292    /// # Returns
293    /// A `JsonValue` representing the complete document structure.
294    #[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    /// Convert to JSON string.
306    ///
307    /// # Feature
308    /// Requires the "json" feature to be enabled.
309    #[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    /// Canonicalize to HEDL string.
323    ///
324    /// Uses v2.0 canonical format (no ditto optimization).
325    #[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    /// Count entities by type.
333    #[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    /// Query entities by type and optional ID.
347    ///
348    /// Returns an array of `EntityResult` objects matching the specified criteria.
349    /// Each result contains the entity type, ID, and field values as a `JsonValue` map.
350    ///
351    /// # Arguments
352    /// * `type_name` - Optional type filter (e.g., "User"). If None, matches all types.
353    /// * `id` - Optional ID filter. If None, matches all IDs.
354    ///
355    /// # Returns
356    /// Array of `EntityResult` objects with properly typed fields (`JsonValue` instead of any).
357    ///
358    /// # Feature
359    /// Requires the "query-api" feature to be enabled.
360    #[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// --- Main API Functions ---
375
376/// Parse a HEDL string and return a document.
377///
378/// # Arguments
379/// * `input` - HEDL document string
380///
381/// # Errors
382/// Returns an error if:
383/// - Input exceeds the configured maximum size (default: 500 MB)
384/// - Parsing fails due to syntax errors
385/// - Inline child list syntax errors:
386///   - Count mismatch between declared and actual children
387///   - Invalid count format (non-numeric)
388///   - Missing required separators (`#` or `:|`)
389///   - Invalid type names in inline children
390///   - Undefined child types or missing NEST relationships
391///
392/// Use `setMaxInputSize()` to increase the size limit for larger documents.
393///
394/// # Inline Child Lists
395/// Inline child list syntax: `@TypeName#count:|child1|child2|...`
396/// Example: `@Comment#2:|c1,Good|c2,Bad`
397///
398/// This feature allows compact representation of child entities directly within
399/// a parent row, useful for small numbers of children (typically ≤3).
400#[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/// Convert HEDL string to JSON.
409///
410/// # Arguments
411/// * `hedl` - HEDL document string
412/// * `pretty` - Whether to pretty-print the JSON (default: true)
413///
414/// # Errors
415/// Returns an error if:
416/// - Input exceeds the configured maximum size (default: 500 MB)
417/// - Parsing or conversion fails
418///
419/// # Feature
420/// Requires the "json" feature to be enabled.
421#[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/// Convert JSON string to HEDL.
439///
440/// # Arguments
441/// * `json` - JSON string to convert
442///
443/// # Errors
444/// Returns an error if:
445/// - Input exceeds the configured maximum size (default: 500 MB)
446/// - JSON parsing or conversion fails
447///
448/// # Feature
449/// Requires the "json" feature to be enabled.
450#[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// --- YAML Conversion ---
467
468/// Convert HEDL string to YAML.
469///
470/// # Arguments
471/// * `hedl` - HEDL document string
472///
473/// # Errors
474/// Returns an error if:
475/// - Input exceeds the configured maximum size (default: 500 MB)
476/// - Parsing or conversion fails
477///
478/// # Feature
479/// Requires the "yaml" feature to be enabled.
480#[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/// Convert YAML string to HEDL.
493///
494/// # Arguments
495/// * `yaml` - YAML string to convert
496///
497/// # Errors
498/// Returns an error if:
499/// - Input exceeds the configured maximum size (default: 500 MB)
500/// - YAML parsing or conversion fails
501///
502/// # Feature
503/// Requires the "yaml" feature to be enabled.
504#[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// --- XML Conversion ---
519
520/// Convert HEDL string to XML.
521///
522/// # Arguments
523/// * `hedl` - HEDL document string
524///
525/// # Errors
526/// Returns an error if:
527/// - Input exceeds the configured maximum size (default: 500 MB)
528/// - Parsing or conversion fails
529///
530/// # Feature
531/// Requires the "xml" feature to be enabled.
532#[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/// Convert XML string to HEDL.
544///
545/// # Arguments
546/// * `xml` - XML string to convert
547///
548/// # Errors
549/// Returns an error if:
550/// - Input exceeds the configured maximum size (default: 500 MB)
551/// - XML parsing or conversion fails
552///
553/// # Feature
554/// Requires the "xml" feature to be enabled.
555#[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// --- CSV Conversion ---
570
571/// Convert HEDL string to CSV.
572///
573/// # Arguments
574/// * `hedl` - HEDL document string
575///
576/// # Errors
577/// Returns an error if:
578/// - Input exceeds the configured maximum size (default: 500 MB)
579/// - Parsing or conversion fails
580///
581/// # Feature
582/// Requires the "csv" feature to be enabled.
583#[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/// Convert CSV string to HEDL.
594///
595/// The CSV must have a header row. Column names from the header become the schema.
596/// The type name defaults to "Row" but can be customized.
597///
598/// # Arguments
599/// * `csv` - CSV string to convert (must have header row)
600/// * `type_name` - Optional type name for entities (default: "Row")
601///
602/// # Errors
603/// Returns an error if:
604/// - Input exceeds the configured maximum size (default: 500 MB)
605/// - CSV parsing or conversion fails
606/// - CSV has no header row
607///
608/// # Feature
609/// Requires the "csv" feature to be enabled.
610#[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    // Parse header row to get schema (excluding 'id' column which is added automatically)
616    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    // hedl_csv::from_csv expects schema WITHOUT the 'id' column (it prepends 'id' automatically)
623    // Skip the first column if it's named 'id'
624    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// --- TOON Conversion ---
642
643/// Convert HEDL string to TOON.
644///
645/// TOON (Typed Object Outline Notation) is an external format specification
646/// for human-readable data serialization.
647///
648/// # Arguments
649/// * `hedl` - HEDL document string
650///
651/// # Errors
652/// Returns an error if:
653/// - Input exceeds the configured maximum size (default: 500 MB)
654/// - Parsing or conversion fails
655///
656/// # Feature
657/// Requires the "toon" feature to be enabled.
658#[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/// Convert TOON string to HEDL.
669///
670/// # Arguments
671/// * `toon` - TOON string to convert
672///
673/// # Errors
674/// Returns an error if:
675/// - Input exceeds the configured maximum size (default: 500 MB)
676/// - TOON parsing or conversion fails
677///
678/// # Feature
679/// Requires the "toon" feature to be enabled.
680#[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/// Format HEDL to canonical form.
694///
695/// # Arguments
696/// * `hedl` - HEDL document string
697///
698/// # Errors
699/// Returns an error if:
700/// - Input exceeds the configured maximum size (default: 500 MB)
701/// - Parsing or formatting fails
702#[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// --- Validation ---
714
715/// Validate HEDL and return detailed diagnostics.
716///
717/// # Arguments
718/// * `hedl` - HEDL document string
719/// * `run_lint` - Run linting rules (default: true, only available with full-validation feature)
720///
721/// # Errors
722/// Returns validation result with errors if:
723/// - Input exceeds the configured maximum size (default: 500 MB)
724/// - Parsing fails due to syntax errors
725/// - Linting detects errors (if enabled and full-validation feature is active)
726#[wasm_bindgen]
727#[must_use]
728pub fn validate(hedl: &str, run_lint: Option<bool>) -> JsValue {
729    // Check input size first
730    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; // Suppress unused variable warning
770                                  // Minimal validation - syntax only (already done by parsing)
771            }
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// --- Statistics ---
787
788/// Get token usage statistics.
789///
790/// # Arguments
791/// * `hedl` - HEDL document string
792///
793/// # Errors
794/// Returns an error if:
795/// - Input exceeds the configured maximum size (default: 500 MB)
796/// - Parsing fails
797///
798/// # Feature
799/// Requires the "statistics" feature to be enabled.
800#[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// --- Live Token Counter ---
834
835/// Compare HEDL and JSON token counts in real-time.
836///
837/// # Feature
838/// Requires the "token-tools" feature to be enabled.
839#[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}