Skip to main content

synx_core/
lib.rs

1//! # SYNX Core — The Active Data Format
2//!
3//! High-performance SYNX parser with full `!active` engine support.
4//! Single Rust crate powering all language bindings.
5//!
6//! ```rust
7//! use synx_core::{Synx, Value};
8//!
9//! let data = Synx::parse("name Wario\nage 30\nactive true");
10//! assert_eq!(data["name"], Value::String("Wario".into()));
11//! assert_eq!(data["age"], Value::Int(30));
12//! ```
13
14mod value;
15mod parser;
16mod engine;
17mod calc;
18pub(crate) mod rng;
19
20pub use value::{Value, Mode, ParseResult, Meta, MetaMap, Options};
21pub use parser::parse;
22pub use engine::resolve;
23pub use calc::safe_calc;
24
25/// Main entry point for the SYNX parser.
26pub struct Synx;
27
28impl Synx {
29    /// Parse a SYNX string into a key-value map (static mode only).
30    pub fn parse(text: &str) -> std::collections::HashMap<String, Value> {
31        let result = parse(text);
32        match result.root {
33            Value::Object(map) => map,
34            _ => std::collections::HashMap::new(),
35        }
36    }
37
38    /// Parse with full engine resolution (!active mode).
39    pub fn parse_active(text: &str, opts: &Options) -> std::collections::HashMap<String, Value> {
40        let mut result = parse(text);
41        if result.mode == Mode::Active {
42            resolve(&mut result, opts);
43        }
44        match result.root {
45            Value::Object(map) => map,
46            _ => std::collections::HashMap::new(),
47        }
48    }
49
50    /// Parse and return full result including mode and metadata.
51    pub fn parse_full(text: &str) -> ParseResult {
52        parse(text)
53    }
54
55    /// Stringify a Value back to SYNX format.
56    pub fn stringify(value: &Value) -> String {
57        serialize(value, 0)
58    }
59
60    /// Reformat a .synx string into canonical form:
61    /// - Keys sorted alphabetically at every nesting level
62    /// - Exactly 2 spaces per indentation level
63    /// - One blank line between top-level blocks (objects / lists)
64    /// - Comments stripped — canonical form is comment-free
65    /// - Directive lines (`!active`, `!lock`) preserved at the top
66    ///
67    /// The same data always produces byte-for-byte identical output,
68    /// making `.synx` files deterministic and noise-free in `git diff`.
69    pub fn format(text: &str) -> String {
70        fmt_canonical(text)
71    }
72}
73
74fn serialize(value: &Value, indent: usize) -> String {
75    match value {
76        Value::Object(map) => {
77            let mut out = String::new();
78            let spaces = " ".repeat(indent);
79            // Sort keys for deterministic output
80            let mut keys: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
81            keys.sort_unstable();
82            for key in keys {
83                let val = &map[key];
84                match val {
85                    Value::Array(arr) => {
86                        out.push_str(&spaces);
87                        out.push_str(key);
88                        out.push('\n');
89                        for item in arr {
90                            match item {
91                                Value::Object(inner) => {
92                                    let entries: Vec<_> = inner.iter().collect();
93                                    if let Some((k, v)) = entries.first() {
94                                        out.push_str(&spaces);
95                                        out.push_str("  - ");
96                                        out.push_str(k);
97                                        out.push(' ');
98                                        out.push_str(&format_primitive(v));
99                                        out.push('\n');
100                                        for (k, v) in entries.iter().skip(1) {
101                                            out.push_str(&spaces);
102                                            out.push_str("    ");
103                                            out.push_str(k);
104                                            out.push(' ');
105                                            out.push_str(&format_primitive(v));
106                                            out.push('\n');
107                                        }
108                                    }
109                                }
110                                _ => {
111                                    out.push_str(&spaces);
112                                    out.push_str("  - ");
113                                    out.push_str(&format_primitive(item));
114                                    out.push('\n');
115                                }
116                            }
117                        }
118                    }
119                    Value::Object(_) => {
120                        out.push_str(&spaces);
121                        out.push_str(key);
122                        out.push('\n');
123                        out.push_str(&serialize(val, indent + 2));
124                    }
125                    Value::String(s) if s.contains('\n') => {
126                        out.push_str(&spaces);
127                        out.push_str(key);
128                        out.push_str(" |\n");
129                        for line in s.lines() {
130                            out.push_str(&spaces);
131                            out.push_str("  ");
132                            out.push_str(line);
133                            out.push('\n');
134                        }
135                    }
136                    _ => {
137                        out.push_str(&spaces);
138                        out.push_str(key);
139                        out.push(' ');
140                        out.push_str(&format_primitive(val));
141                        out.push('\n');
142                    }
143                }
144            }
145            out
146        }
147        _ => format_primitive(value),
148    }
149}
150
151fn format_primitive(value: &Value) -> String {
152    match value {
153        Value::String(s) => s.clone(),
154        Value::Int(n) => n.to_string(),
155        Value::Float(f) => {
156            let s = f.to_string();
157            if s.contains('.') { s } else { format!("{}.0", s) }
158        }
159        Value::Bool(b) => b.to_string(),
160        Value::Null => "null".to_string(),
161        Value::Array(arr) => {
162            let items: Vec<String> = arr.iter().map(format_primitive).collect();
163            format!("[{}]", items.join(", "))
164        }
165        Value::Object(_) => "[Object]".to_string(),
166        Value::Secret(_) => "[SECRET]".to_string(),
167    }
168}
169
170/// Write a Value as JSON string (for FFI output).
171pub fn write_json(out: &mut String, val: &Value) {
172    match val {
173        Value::Null => out.push_str("null"),
174        Value::Bool(true) => out.push_str("true"),
175        Value::Bool(false) => out.push_str("false"),
176        Value::Int(n) => {
177            let mut buf = itoa::Buffer::new();
178            out.push_str(buf.format(*n));
179        }
180        Value::Float(f) => {
181            let mut buf = ryu::Buffer::new();
182            out.push_str(buf.format(*f));
183        }
184        Value::String(s) | Value::Secret(s) => {
185            out.push('"');
186            for ch in s.chars() {
187                match ch {
188                    '"' => out.push_str("\\\""),
189                    '\\' => out.push_str("\\\\"),
190                    '\n' => out.push_str("\\n"),
191                    '\r' => out.push_str("\\r"),
192                    '\t' => out.push_str("\\t"),
193                    c if (c as u32) < 0x20 => {
194                        out.push_str(&format!("\\u{:04x}", c as u32));
195                    }
196                    c => out.push(c),
197                }
198            }
199            out.push('"');
200        }
201        Value::Array(arr) => {
202            out.push('[');
203            for (i, item) in arr.iter().enumerate() {
204                if i > 0 { out.push(','); }
205                write_json(out, item);
206            }
207            out.push(']');
208        }
209        Value::Object(map) => {
210            out.push('{');
211            let mut first = true;
212            // Sort keys for deterministic, diffable JSON output
213            let mut entries: Vec<(&str, &Value)> =
214                map.iter().map(|(k, v)| (k.as_str(), v)).collect();
215            entries.sort_unstable_by_key(|(k, _)| *k);
216            for (key, val) in entries {
217                if !first { out.push(','); }
218                first = false;
219                // Escape the key the same way string values are escaped
220                out.push('"');
221                for ch in key.chars() {
222                    match ch {
223                        '"'  => out.push_str("\\\""),
224                        '\\' => out.push_str("\\\\"),
225                        '\n' => out.push_str("\\n"),
226                        '\r' => out.push_str("\\r"),
227                        '\t' => out.push_str("\\t"),
228                        c if (c as u32) < 0x20 => {
229                            out.push_str(&format!("\\u{:04x}", c as u32));
230                        }
231                        c => out.push(c),
232                    }
233                }
234                out.push_str("\":");
235                write_json(out, val);
236            }
237            out.push('}');
238        }
239    }
240}
241
242/// Convert a Value to a JSON string.
243pub fn to_json(val: &Value) -> String {
244    let mut out = String::with_capacity(2048);
245    write_json(&mut out, val);
246    out
247}
248
249// ─── Canonical Formatter ─────────────────────────────────────────────────────
250
251struct FmtNode {
252    header: String,
253    children: Vec<FmtNode>,
254    list_items: Vec<String>,
255    is_multiline: bool,
256}
257
258fn fmt_indent(line: &str) -> usize {
259    line.len() - line.trim_start().len()
260}
261
262fn fmt_parse(lines: &[&str], start: usize, base: usize) -> (Vec<FmtNode>, usize) {
263    let mut nodes = Vec::new();
264    let mut i = start;
265    while i < lines.len() {
266        let raw = lines[i];
267        let t = raw.trim();
268        if t.is_empty() { i += 1; continue; }
269        let ind = fmt_indent(raw);
270        if ind < base { break; }
271        if ind > base { i += 1; continue; }
272        if t.starts_with("- ") || t.starts_with('#') || t.starts_with("//") { i += 1; continue; }
273        let is_multiline = t.ends_with(" |") || t == "|";
274        let mut node = FmtNode {
275            header: t.to_string(),
276            children: Vec::new(),
277            list_items: Vec::new(),
278            is_multiline,
279        };
280        i += 1;
281        while i < lines.len() {
282            let cr = lines[i];
283            let ct = cr.trim();
284            if ct.is_empty() { i += 1; continue; }
285            let ci = fmt_indent(cr);
286            if ci <= base { break; }
287            if node.is_multiline || ct.starts_with("- ") {
288                node.list_items.push(ct.to_string());
289                i += 1;
290            } else if ct.starts_with('#') || ct.starts_with("//") {
291                i += 1;
292            } else {
293                let (subs, ni) = fmt_parse(lines, i, ci);
294                node.children.extend(subs);
295                i = ni;
296            }
297        }
298        nodes.push(node);
299    }
300    (nodes, i)
301}
302
303fn fmt_sort(nodes: &mut Vec<FmtNode>) {
304    nodes.sort_unstable_by(|a, b| {
305        let ka = a.header.split(|c: char| c.is_whitespace() || c == '[' || c == ':' || c == '(')
306            .next().unwrap_or("").to_lowercase();
307        let kb = b.header.split(|c: char| c.is_whitespace() || c == '[' || c == ':' || c == '(')
308            .next().unwrap_or("").to_lowercase();
309        ka.cmp(&kb)
310    });
311    for node in nodes.iter_mut() {
312        fmt_sort(&mut node.children);
313    }
314}
315
316fn fmt_emit(nodes: &[FmtNode], indent: usize, out: &mut String) {
317    let sp = " ".repeat(indent);
318    let item_sp = " ".repeat(indent + 2);
319    for n in nodes {
320        out.push_str(&sp);
321        out.push_str(&n.header);
322        out.push('\n');
323        if !n.children.is_empty() {
324            fmt_emit(&n.children, indent + 2, out);
325        }
326        for li in &n.list_items {
327            out.push_str(&item_sp);
328            out.push_str(li);
329            out.push('\n');
330        }
331        if indent == 0 && (!n.children.is_empty() || !n.list_items.is_empty()) {
332            out.push('\n');
333        }
334    }
335}
336
337fn fmt_canonical(text: &str) -> String {
338    let lines: Vec<&str> = text.lines().collect();
339    let mut directives: Vec<&str> = Vec::new();
340    let mut body_start = 0usize;
341
342    for (i, &line) in lines.iter().enumerate() {
343        let t = line.trim();
344        if t == "!active" || t == "!lock" || t == "#!mode:active" {
345            directives.push(t);
346            body_start = i + 1;
347        } else if t.is_empty() || t.starts_with('#') || t.starts_with("//") {
348            body_start = i + 1;
349        } else {
350            break;
351        }
352    }
353
354    let (mut nodes, _) = fmt_parse(&lines, body_start, 0);
355    fmt_sort(&mut nodes);
356
357    let mut out = String::with_capacity(text.len());
358    if !directives.is_empty() {
359        out.push_str(&directives.join("\n"));
360        out.push_str("\n\n");
361    }
362    fmt_emit(&nodes, 0, &mut out);
363    // Trim trailing blank lines, ensure single newline at end
364    let trimmed = out.trim_end();
365    let mut result = trimmed.to_string();
366    result.push('\n');
367    result
368}