1mod 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
25pub struct Synx;
27
28impl Synx {
29 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 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 pub fn parse_full(text: &str) -> ParseResult {
52 parse(text)
53 }
54
55 pub fn stringify(value: &Value) -> String {
57 serialize(value, 0)
58 }
59
60 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 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
170pub 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 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 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
242pub fn to_json(val: &Value) -> String {
244 let mut out = String::with_capacity(2048);
245 write_json(&mut out, val);
246 out
247}
248
249struct 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 let trimmed = out.trim_end();
365 let mut result = trimmed.to_string();
366 result.push('\n');
367 result
368}