1#![doc = include_str!("../README.md")]
2use serde::Serialize;
8use serde_json::json;
9use styx_parse::{ScalarKind, Separator};
10use styx_tree::{Entry, Object, Payload, Scalar, Sequence, Tag, Value};
11use wasm_bindgen::prelude::*;
12
13fn to_js_value<T: Serialize>(value: &T) -> Result<JsValue, serde_wasm_bindgen::Error> {
15 let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
16 value.serialize(&serializer)
17}
18
19#[derive(Debug, Clone, Serialize)]
21pub struct Diagnostic {
22 pub message: String,
24 pub start: u32,
26 pub end: u32,
28 pub severity: String,
30}
31
32#[derive(Debug, Clone, Serialize)]
34pub struct ParseResult {
35 pub success: bool,
37 pub diagnostics: Vec<Diagnostic>,
39}
40
41#[wasm_bindgen]
45pub fn parse(source: &str) -> JsValue {
46 let parser = styx_parse::Parser::new(source);
47 let mut events = Vec::new();
48 parser.parse(&mut events);
49
50 let mut diagnostics = Vec::new();
51 for event in events {
52 if let styx_parse::Event::Error { span, kind } = event {
53 diagnostics.push(Diagnostic {
54 message: format_error(&kind),
55 start: span.start,
56 end: span.end,
57 severity: "error".to_string(),
58 });
59 }
60 }
61
62 let result = ParseResult {
63 success: diagnostics.is_empty(),
64 diagnostics,
65 };
66
67 to_js_value(&result).unwrap_or(JsValue::NULL)
68}
69
70#[wasm_bindgen]
76pub fn to_json(source: &str) -> JsValue {
77 match styx_tree::parse(source) {
78 Ok(value) => {
79 let json_value = value_to_json(&value);
80 let json_string =
81 serde_json::to_string_pretty(&json_value).unwrap_or_else(|e| e.to_string());
82
83 to_js_value(&json!({
84 "success": true,
85 "json": json_value,
86 "jsonString": json_string
87 }))
88 .unwrap_or(JsValue::NULL)
89 }
90 Err(e) => to_js_value(&json!({
91 "success": false,
92 "error": e.to_string()
93 }))
94 .unwrap_or(JsValue::NULL),
95 }
96}
97
98fn value_to_json(value: &Value) -> serde_json::Value {
100 let tag = value.tag.as_ref().map(|t| t.name.as_str());
101 let payload = value.payload.as_ref().map(payload_to_json);
102
103 match (tag, payload) {
104 (None, None) => json!(null),
106 (None, Some(p)) => p,
108 (Some(t), None) => json!({"$tag": t}),
110 (Some(t), Some(p)) => json!({"$tag": t, "$value": p}),
112 }
113}
114
115fn payload_to_json(payload: &Payload) -> serde_json::Value {
117 match payload {
118 Payload::Scalar(s) => {
119 if let Ok(n) = s.text.parse::<i64>() {
121 json!(n)
122 } else if let Ok(n) = s.text.parse::<f64>() {
123 json!(n)
124 } else if s.text == "true" {
125 json!(true)
126 } else if s.text == "false" {
127 json!(false)
128 } else if s.text == "null" {
129 json!(null)
130 } else {
131 json!(s.text)
132 }
133 }
134 Payload::Sequence(seq) => sequence_to_json(seq),
135 Payload::Object(obj) => object_to_json(obj),
136 }
137}
138
139fn sequence_to_json(seq: &Sequence) -> serde_json::Value {
141 let items: Vec<serde_json::Value> = seq.items.iter().map(value_to_json).collect();
142 json!(items)
143}
144
145fn object_to_json(obj: &Object) -> serde_json::Value {
147 let mut map = serde_json::Map::new();
148
149 for entry in &obj.entries {
150 let key = if entry.key.is_unit() {
152 "@".to_string()
153 } else if let Some(s) = entry.key.as_str() {
154 s.to_string()
155 } else if let Some(tag) = entry.key.tag_name() {
156 format!("@{}", tag)
157 } else {
158 format!("{:?}", entry.key)
160 };
161
162 map.insert(key, value_to_json(&entry.value));
163 }
164
165 serde_json::Value::Object(map)
166}
167
168fn format_error(kind: &styx_parse::ParseErrorKind) -> String {
170 use styx_parse::ParseErrorKind::*;
171 match kind {
172 DuplicateKey { .. } => "Duplicate key in object".to_string(),
173 MixedSeparators => "Mixed separators: use either commas or newlines, not both".to_string(),
174 UnclosedObject => "Unclosed object: missing '}'".to_string(),
175 UnclosedSequence => "Unclosed sequence: missing ')'".to_string(),
176 InvalidEscape(seq) => format!("Invalid escape sequence: '{}'", seq),
177 UnexpectedToken => "Unexpected token".to_string(),
178 ExpectedKey => "Expected a key".to_string(),
179 ExpectedValue => "Expected a value".to_string(),
180 UnexpectedEof => "Unexpected end of input".to_string(),
181 InvalidTagName => "Invalid tag name: must match @[A-Za-z_][A-Za-z0-9_.-]*".to_string(),
182 InvalidKey => "Invalid key: cannot use objects, sequences, or heredocs as keys".to_string(),
183 DanglingDocComment => "Doc comment (///) must be followed by an entry".to_string(),
184 TooManyAtoms => {
185 "Too many atoms: did you mean @tag{}? No whitespace between tag and payload".to_string()
186 }
187 ReopenedPath { closed_path } => {
188 format!(
189 "Cannot reopen path '{}': sibling paths must appear contiguously",
190 closed_path.join(".")
191 )
192 }
193 NestIntoTerminal { terminal_path } => {
194 format!(
195 "Cannot nest into '{}': path already has a terminal value",
196 terminal_path.join(".")
197 )
198 }
199 CommaInSequence => "Sequences use whitespace separators, not commas".to_string(),
200 MissingWhitespaceBeforeBlock => "Missing whitespace before '{' or '(' after bare key (to distinguish from tags like @tag{})".to_string(),
201 }
202}
203
204#[wasm_bindgen]
206pub fn validate(source: &str) -> bool {
207 let parser = styx_parse::Parser::new(source);
208 let mut events = Vec::new();
209 parser.parse(&mut events);
210 !events
211 .iter()
212 .any(|e| matches!(e, styx_parse::Event::Error { .. }))
213}
214
215#[wasm_bindgen]
220pub fn from_json(json_source: &str) -> JsValue {
221 match serde_json::from_str::<serde_json::Value>(json_source) {
222 Ok(json_value) => {
223 let styx_value = json_to_value(&json_value);
224 let styx_string =
225 styx_format::format_value(&styx_value, styx_format::FormatOptions::default());
226
227 to_js_value(&json!({
228 "success": true,
229 "styxString": styx_string
230 }))
231 .unwrap_or(JsValue::NULL)
232 }
233 Err(e) => to_js_value(&json!({
234 "success": false,
235 "error": e.to_string()
236 }))
237 .unwrap_or(JsValue::NULL),
238 }
239}
240
241fn json_to_value(json: &serde_json::Value) -> Value {
243 match json {
244 serde_json::Value::Null => Value::unit(),
245
246 serde_json::Value::Bool(b) => Value {
247 tag: None,
248 payload: Some(Payload::Scalar(Scalar {
249 text: b.to_string(),
250 kind: ScalarKind::Bare,
251 span: None,
252 })),
253 span: None,
254 },
255
256 serde_json::Value::Number(n) => Value {
257 tag: None,
258 payload: Some(Payload::Scalar(Scalar {
259 text: n.to_string(),
260 kind: ScalarKind::Bare,
261 span: None,
262 })),
263 span: None,
264 },
265
266 serde_json::Value::String(s) => {
267 let kind = if needs_quoting(s) {
269 ScalarKind::Quoted
270 } else {
271 ScalarKind::Bare
272 };
273 Value {
274 tag: None,
275 payload: Some(Payload::Scalar(Scalar {
276 text: s.clone(),
277 kind,
278 span: None,
279 })),
280 span: None,
281 }
282 }
283
284 serde_json::Value::Array(arr) => {
285 let items = arr.iter().map(json_to_value).collect();
286 Value {
287 tag: None,
288 payload: Some(Payload::Sequence(Sequence { items, span: None })),
289 span: None,
290 }
291 }
292
293 serde_json::Value::Object(obj) => {
294 if let Some(serde_json::Value::String(tag_name)) = obj.get("$tag") {
296 let payload = obj.get("$value").and_then(|v| json_to_value(v).payload);
297 return Value {
298 tag: Some(Tag {
299 name: tag_name.clone(),
300 span: None,
301 }),
302 payload,
303 span: None,
304 };
305 }
306
307 let entries = obj
309 .iter()
310 .map(|(k, v)| Entry {
311 key: Value {
312 tag: None,
313 payload: Some(Payload::Scalar(Scalar {
314 text: k.clone(),
315 kind: if needs_quoting(k) {
316 ScalarKind::Quoted
317 } else {
318 ScalarKind::Bare
319 },
320 span: None,
321 })),
322 span: None,
323 },
324 value: json_to_value(v),
325 doc_comment: None,
326 })
327 .collect();
328
329 Value {
330 tag: None,
331 payload: Some(Payload::Object(Object {
332 entries,
333 separator: Separator::Newline,
334 span: None,
335 })),
336 span: None,
337 }
338 }
339 }
340}
341
342fn needs_quoting(s: &str) -> bool {
344 if s.is_empty() {
345 return true;
346 }
347
348 s.chars().any(|c| {
350 matches!(
351 c,
352 ' ' | '\t' | '\n' | '\r' | '"' | '{' | '}' | '(' | ')' | ',' | '@' | '>' | '/'
353 )
354 }) || s.starts_with("//")
355}
356
357#[wasm_bindgen]
359pub fn version() -> String {
360 env!("CARGO_PKG_VERSION").to_string()
361}