#[cfg(target_arch = "wasm32")] use decodal::HostEnvironment; use decodal::{Data, EmptyLoader, Engine, SourceId, format_diagnostic_with}; #[cfg(target_arch = "wasm32")] use decodal_language_service::{CompletionKind, LanguageService}; use wasm_bindgen::prelude::*; #[cfg(any(target_arch = "wasm32", test))] mod value; #[cfg(target_arch = "wasm32")] mod web_environment; #[cfg(target_arch = "wasm32")] use web_environment::JsEnvironment; /// Evaluates one standalone Decodal source with no host globals or imports. #[wasm_bindgen] pub fn evaluate(source: &str) -> String { encode_result(evaluate_inner(source)) } /// A browser-facing language service configured entirely by its JavaScript host. /// /// The host owns globals, import loading, and import completion. This keeps /// filesystem, network, and virtual-project policy outside the WASM package. #[cfg(target_arch = "wasm32")] #[wasm_bindgen(js_name = DecodalLanguageService)] pub struct WebLanguageService { environment: JsEnvironment, } #[cfg(target_arch = "wasm32")] #[wasm_bindgen(js_class = DecodalLanguageService)] impl WebLanguageService { #[wasm_bindgen(constructor)] pub fn new(options: JsValue) -> Result { Ok(Self { environment: JsEnvironment::from_options(options)?, }) } /// Evaluates a source using the injected globals and import loader. pub fn evaluate(&self, key: &str, name: &str, source: &str) -> String { encode_result(evaluate_with_environment( &self.environment, key, name, source, )) } /// Completes a source using the same injected environment as evaluation. /// /// Positions and returned ranges are UTF-16 offsets, matching browser /// editors and the Language Server Protocol. pub fn complete(&self, key: &str, source: &str, position: usize, explicit: bool) -> String { encode_completion(&self.environment, key, source, position, explicit) } } fn encode_result(result: Result) -> String { match result { Ok(output) => serde_json::json!({ "ok": true, "output": output }).to_string(), Err(error) => serde_json::json!({ "ok": false, "error": error }).to_string(), } } fn evaluate_inner(source: &str) -> Result { let mut engine = Engine::new(EmptyLoader); let module = match engine.add_root_source("playground", "playground", source) { Ok(module) => module, Err(error) => return Err(format_diagnostic_with_root(&error, "playground")), }; let value = match engine.eval_module(module) { Ok(value) => value, Err(error) => return Err(engine.format_diagnostic(&error)), }; let data = match engine.materialize(&value) { Ok(data) => data, Err(error) => return Err(engine.format_diagnostic(&error)), }; Ok(format_data(&data, 0)) } #[cfg(target_arch = "wasm32")] fn evaluate_with_environment( environment: &JsEnvironment, key: &str, name: &str, source: &str, ) -> Result { let mut engine = environment .create_engine() .map_err(|diagnostic| diagnostic.message)?; let module = match engine.add_root_source(key, name, source) { Ok(module) => module, Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)), }; let value = match engine.eval_module(module) { Ok(value) => value, Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)), }; let data = match engine.materialize(&value) { Ok(data) => data, Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)), }; Ok(format_data(&data, 0)) } #[cfg(target_arch = "wasm32")] fn encode_completion( environment: &JsEnvironment, key: &str, source: &str, position: usize, explicit: bool, ) -> String { let byte_position = utf16_offset_to_byte(source, position); let service = LanguageService::new(environment); let completion = match service.complete(key, source, byte_position, explicit) { Ok(completion) => completion, Err(diagnostic) => { return serde_json::json!({ "ok": false, "error": diagnostic.message, }) .to_string(); } }; let completion = completion.map(|completion| { let from = byte_offset_to_utf16(source, completion.from); let options = completion .items .into_iter() .map(|item| { serde_json::json!({ "label": item.label, "kind": completion_kind_name(item.kind), "detail": item.detail, "priority": item.priority, }) }) .collect::>(); serde_json::json!({ "from": from, "options": options }) }); serde_json::json!({ "ok": true, "completion": completion }).to_string() } #[cfg(target_arch = "wasm32")] fn completion_kind_name(kind: CompletionKind) -> &'static str { match kind { CompletionKind::Keyword => "keyword", CompletionKind::Constant => "constant", CompletionKind::Type => "type", CompletionKind::Variable => "variable", CompletionKind::Namespace => "namespace", CompletionKind::Property => "property", CompletionKind::File => "file", } } #[cfg(any(target_arch = "wasm32", test))] fn utf16_offset_to_byte(source: &str, offset: usize) -> usize { let mut utf16 = 0usize; for (byte, ch) in source.char_indices() { if utf16 >= offset { return byte; } let next = utf16 + ch.len_utf16(); if next > offset { return byte; } utf16 = next; } source.len() } #[cfg(any(target_arch = "wasm32", test))] fn byte_offset_to_utf16(source: &str, mut offset: usize) -> usize { offset = offset.min(source.len()); while !source.is_char_boundary(offset) { offset = offset.saturating_sub(1); } source[..offset].encode_utf16().count() } fn format_diagnostic_with_root(diagnostic: &decodal::Diagnostic, root_name: &str) -> String { format_diagnostic_with(diagnostic, |source| { (source == SourceId(0)).then_some(root_name) }) } fn format_data(data: &Data, indent: usize) -> String { match data { Data::String(value) => json_string(value), Data::Int(value) => value.to_string(), Data::Float(value) => value.to_string(), Data::Bool(value) => value.to_string(), Data::Array(items) => { if items.is_empty() { return String::from("[]"); } let mut out = String::from("[\n"); for (index, item) in items.iter().enumerate() { out.push_str(&" ".repeat(indent + 2)); out.push_str(&format_data(item, indent + 2)); if index + 1 != items.len() { out.push(','); } out.push('\n'); } out.push_str(&" ".repeat(indent)); out.push(']'); out } Data::Object(fields) => { if fields.is_empty() { return String::from("{}"); } let mut out = String::from("{\n"); for (index, field) in fields.iter().enumerate() { out.push_str(&" ".repeat(indent + 2)); out.push_str(&json_string(&field.name)); out.push_str(": "); out.push_str(&format_data(&field.value, indent + 2)); if index + 1 != fields.len() { out.push(','); } out.push('\n'); } out.push_str(&" ".repeat(indent)); out.push('}'); out } } } fn json_string(value: &str) -> String { serde_json::to_string(value).expect("strings are always JSON-serializable") } #[cfg(test)] mod tests { use super::{byte_offset_to_utf16, evaluate_inner, utf16_offset_to_byte}; #[test] fn evaluates_standalone_sources() { assert_eq!( evaluate_inner("value = 1;").unwrap(), "{\n \"value\": 1\n}" ); } #[test] fn converts_web_utf16_offsets() { let source = "a😀β"; assert_eq!(utf16_offset_to_byte(source, 0), 0); assert_eq!(utf16_offset_to_byte(source, 1), 1); assert_eq!(utf16_offset_to_byte(source, 3), 5); assert_eq!(byte_offset_to_utf16(source, 5), 3); assert_eq!(byte_offset_to_utf16(source, source.len()), 4); } }