1use crate::options::{ForceStyle, FormatOptions};
7use crate::scalar::{can_be_bare, count_escapes, count_newlines, escape_quoted};
8
9#[derive(Debug, Clone)]
11pub enum Context {
12 Struct {
14 first: bool,
15 is_root: bool,
16 force_multiline: bool,
17 inline_start: bool,
19 comma_positions: Vec<usize>,
21 open_brace_pos: Option<usize>,
23 },
24 Seq {
26 first: bool,
27 inline_start: bool,
29 },
30}
31
32pub struct StyxWriter {
39 out: Vec<u8>,
40 stack: Vec<Context>,
41 options: FormatOptions,
42 skip_next_before_value: bool,
44 force_quote_next_scalar: bool,
47}
48
49impl StyxWriter {
50 pub fn new() -> Self {
52 Self::with_options(FormatOptions::default())
53 }
54
55 pub fn with_options(options: FormatOptions) -> Self {
57 Self {
58 out: Vec::new(),
59 stack: Vec::new(),
60 skip_next_before_value: false,
61 force_quote_next_scalar: false,
62 options,
63 }
64 }
65
66 pub fn finish(self) -> Vec<u8> {
68 self.out
69 }
70
71 pub fn finish_document(mut self) -> Vec<u8> {
74 if !self.out.is_empty() && !self.out.ends_with(b"\n") {
75 self.out.push(b'\n');
76 }
77 self.out
78 }
79
80 pub fn finish_string(self) -> String {
85 String::from_utf8(self.out).expect("Styx output should always be valid UTF-8")
86 }
87
88 pub fn depth(&self) -> usize {
90 self.stack.len()
91 }
92
93 fn indent_depth(&self) -> usize {
95 let mut depth = 0;
96 for ctx in &self.stack {
97 match ctx {
98 Context::Struct { is_root: true, .. } => {
99 }
101 Context::Struct {
102 inline_start: true,
103 force_multiline: false,
104 ..
105 } => {
106 }
108 Context::Seq {
109 inline_start: true, ..
110 } => {
111 }
113 _ => {
114 depth += 1;
115 }
116 }
117 }
118 depth
119 }
120
121 pub fn available_width(&self) -> usize {
123 let used = self.depth() * self.options.indent.len();
124 self.options.max_width.saturating_sub(used)
125 }
126
127 pub fn should_inline(&self) -> bool {
129 if self.options.force_style == ForceStyle::Inline {
130 return true;
131 } else if self.options.force_style == ForceStyle::Multiline {
132 return false;
133 }
134 if self.depth() == 0 {
136 return false;
137 }
138 if let Some(Context::Struct { is_root: true, .. }) = self.stack.first()
140 && self.depth() == 1
141 {
142 return false;
143 }
144 if let Some(Context::Struct {
146 force_multiline: true,
147 ..
148 }) = self.stack.last()
149 {
150 return false;
151 }
152 self.available_width() >= self.options.min_inline_width
154 }
155
156 pub fn write_indent(&mut self) {
158 for _ in 0..self.indent_depth() {
159 self.out.extend_from_slice(self.options.indent.as_bytes());
160 }
161 }
162
163 pub fn write_newline_indent(&mut self) {
165 self.out.push(b'\n');
166 self.write_indent();
167 }
168
169 pub fn write_raw(&mut self, bytes: &[u8]) {
171 self.out.extend_from_slice(bytes);
172 }
173
174 pub fn write_str(&mut self, s: &str) {
176 self.out.extend_from_slice(s.as_bytes());
177 }
178
179 pub fn write_byte(&mut self, b: u8) {
181 self.out.push(b);
182 }
183
184 pub fn begin_struct(&mut self, is_root: bool) {
188 self.begin_struct_with_options(is_root, false);
189 }
190
191 pub fn begin_struct_with_options(&mut self, is_root: bool, force_multiline: bool) {
196 self.before_value();
197
198 let inline_start = !is_root;
201
202 if is_root {
203 self.stack.push(Context::Struct {
204 first: true,
205 is_root: true,
206 force_multiline,
207 inline_start: false,
208 comma_positions: Vec::new(),
209 open_brace_pos: None,
210 });
211 } else {
212 self.out.push(b'{');
213 let open_pos = self.out.len(); self.stack.push(Context::Struct {
215 first: true,
216 is_root: false,
217 force_multiline,
218 inline_start,
219 comma_positions: Vec::new(),
220 open_brace_pos: Some(open_pos),
221 });
222 }
223 }
224
225 pub fn begin_struct_after_tag(&mut self, force_multiline: bool) {
227 self.force_quote_next_scalar = false;
230 self.out.push(b'{');
231 let open_pos = self.out.len(); self.stack.push(Context::Struct {
233 first: true,
234 is_root: false,
235 force_multiline,
236 inline_start: true,
237 comma_positions: Vec::new(),
238 open_brace_pos: Some(open_pos),
239 });
240 }
241
242 pub fn field_key_raw(&mut self, key: &str) -> Result<(), &'static str> {
246 let (is_struct, is_first, is_root, inline_start, force_multiline) = match self.stack.last()
248 {
249 Some(Context::Struct {
250 first,
251 is_root,
252 inline_start,
253 force_multiline,
254 ..
255 }) => (true, *first, *is_root, *inline_start, *force_multiline),
256 _ => (false, true, false, false, false),
257 };
258
259 if !is_struct {
260 return Err("field_key_raw called outside of struct");
261 }
262
263 let struct_is_inline = inline_start && !force_multiline;
265 let should_inline = struct_is_inline || self.should_inline();
266
267 if !is_first {
268 if should_inline && !is_root {
269 let comma_pos = self.out.len();
271 self.out.extend_from_slice(b", ");
272 if let Some(Context::Struct {
273 comma_positions, ..
274 }) = self.stack.last_mut()
275 {
276 comma_positions.push(comma_pos);
277 }
278 } else {
279 if is_root {
281 self.out.push(b'\n');
282 }
283 self.write_newline_indent();
284 }
285 } else {
286 if !is_root && !should_inline {
288 self.write_newline_indent();
289 }
290 }
291
292 if let Some(Context::Struct { first, .. }) = self.stack.last_mut() {
294 *first = false;
295 }
296
297 self.out.extend_from_slice(key.as_bytes());
299 self.out.push(b' ');
300 Ok(())
301 }
302
303 pub fn field_key(&mut self, key: &str) -> Result<(), &'static str> {
307 let (is_struct, is_first, is_root, inline_start, force_multiline) = match self.stack.last()
309 {
310 Some(Context::Struct {
311 first,
312 is_root,
313 inline_start,
314 force_multiline,
315 ..
316 }) => (true, *first, *is_root, *inline_start, *force_multiline),
317 _ => (false, true, false, false, false),
318 };
319
320 if !is_struct {
321 return Err("field_key called outside of struct");
322 }
323
324 let struct_is_inline = inline_start && !force_multiline;
326 let should_inline = struct_is_inline || self.should_inline();
327
328 if !is_first {
329 if should_inline && !is_root {
330 let comma_pos = self.out.len();
332 self.out.extend_from_slice(b", ");
333 if let Some(Context::Struct {
334 comma_positions, ..
335 }) = self.stack.last_mut()
336 {
337 comma_positions.push(comma_pos);
338 }
339 } else {
340 if is_root {
342 self.out.push(b'\n');
343 }
344 self.write_newline_indent();
345 }
346 } else {
347 if !is_root && !should_inline {
349 self.write_newline_indent();
350 }
351 }
352
353 if let Some(Context::Struct { first, .. }) = self.stack.last_mut() {
355 *first = false;
356 }
357
358 if can_be_bare(key) {
360 self.out.extend_from_slice(key.as_bytes());
361 } else {
362 self.write_quoted_string(key);
363 }
364 self.out.push(b' ');
365 Ok(())
366 }
367
368 pub fn end_struct(&mut self) -> Result<(), &'static str> {
372 let should_inline = self.should_inline();
374
375 match self.stack.pop() {
376 Some(Context::Struct {
377 first,
378 is_root,
379 force_multiline,
380 inline_start,
381 ..
382 }) => {
383 if is_root {
384 if !first {
386 self.out.push(b'\n');
387 }
388 } else {
389 let needs_newline =
393 !first && (force_multiline || (!inline_start && !should_inline));
394 if needs_newline {
395 self.out.push(b'\n');
397 self.write_indent();
399 }
400 self.out.push(b'}');
401 }
402 Ok(())
403 }
404 _ => Err("end_struct called without matching begin_struct"),
405 }
406 }
407
408 pub fn begin_seq(&mut self) {
410 self.before_value();
411 self.out.push(b'(');
412 self.stack.push(Context::Seq {
414 first: true,
415 inline_start: true,
416 });
417 }
418
419 pub fn end_seq(&mut self) -> Result<(), &'static str> {
423 let should_inline = self.should_inline();
425
426 match self.stack.pop() {
427 Some(Context::Seq {
428 first,
429 inline_start,
430 }) => {
431 if !first && !inline_start && !should_inline {
434 self.write_newline_indent();
435 }
436 self.out.push(b')');
437 Ok(())
438 }
439 _ => Err("end_seq called without matching begin_seq"),
440 }
441 }
442
443 pub fn write_null(&mut self) {
445 self.before_value();
446 self.out.push(b'@');
447 }
448
449 pub fn write_bool(&mut self, v: bool) {
451 self.before_value();
452 if v {
453 self.out.extend_from_slice(b"true");
454 } else {
455 self.out.extend_from_slice(b"false");
456 }
457 }
458
459 pub fn write_i64(&mut self, v: i64) {
461 self.before_value();
462 self.out.extend_from_slice(v.to_string().as_bytes());
463 }
464
465 pub fn write_u64(&mut self, v: u64) {
467 self.before_value();
468 self.out.extend_from_slice(v.to_string().as_bytes());
469 }
470
471 pub fn write_i128(&mut self, v: i128) {
473 self.before_value();
474 self.out.extend_from_slice(v.to_string().as_bytes());
475 }
476
477 pub fn write_u128(&mut self, v: u128) {
479 self.before_value();
480 self.out.extend_from_slice(v.to_string().as_bytes());
481 }
482
483 pub fn write_f64(&mut self, v: f64) {
485 self.before_value();
486 self.out.extend_from_slice(v.to_string().as_bytes());
487 }
488
489 pub fn write_string(&mut self, s: &str) {
491 self.before_value();
492 self.write_scalar_string(s);
493 }
494
495 pub fn write_char(&mut self, c: char) {
497 self.before_value();
498 let mut buf = [0u8; 4];
499 let s = c.encode_utf8(&mut buf);
500 self.write_scalar_string(s);
501 }
502
503 pub fn write_bytes(&mut self, bytes: &[u8]) {
505 self.before_value();
506 self.out.push(b'"');
507 for byte in bytes.iter() {
508 let hex = |d: u8| {
509 if d < 10 { b'0' + d } else { b'a' + (d - 10) }
510 };
511 self.out.push(hex(byte >> 4));
512 self.out.push(hex(byte & 0xf));
513 }
514 self.out.push(b'"');
515 }
516
517 pub fn write_variant_tag(&mut self, name: &str) {
519 self.before_value();
520 self.out.push(b'@');
521 self.out.extend_from_slice(name.as_bytes());
522 self.skip_next_before_value = true;
525 self.force_quote_next_scalar = true;
526 }
527
528 pub fn write_scalar(&mut self, s: &str) {
531 self.write_string(s);
532 }
533
534 pub fn write_tag(&mut self, name: &str) {
536 self.write_variant_tag(name);
537 }
538
539 pub fn write_tag_chain_segment(&mut self, name: &str) {
541 self.out.extend_from_slice(b"/@");
542 self.out.extend_from_slice(name.as_bytes());
543 self.skip_next_before_value = true;
544 self.force_quote_next_scalar = true;
545 }
546
547 pub fn clear_skip_before_value(&mut self) {
549 self.skip_next_before_value = false;
550 self.force_quote_next_scalar = false;
551 }
552
553 pub fn begin_seq_after_tag(&mut self) {
555 self.force_quote_next_scalar = false;
557 self.out.push(b'(');
558 self.stack.push(Context::Seq {
560 first: true,
561 inline_start: true,
562 });
563 }
564
565 pub fn write_doc_comment_and_key(&mut self, doc: &str, key: &str) {
568 let (is_first, is_root) = match self.stack.last() {
570 Some(Context::Struct { first, is_root, .. }) => (*first, *is_root),
571 _ => (true, false),
572 };
573
574 if let Some(Context::Struct {
576 first,
577 force_multiline,
578 ..
579 }) = self.stack.last_mut()
580 {
581 *first = false;
582 *force_multiline = true;
583 }
584
585 self.fix_comma_separators();
587
588 self.propagate_multiline_to_parents();
590
591 if !is_first && is_root {
594 self.out.push(b'\n');
596 }
597
598 let need_leading_newline = !is_first || !is_root;
600
601 for (i, line) in doc.lines().enumerate() {
602 if i > 0 || need_leading_newline {
603 self.write_newline_indent();
604 }
605 self.out.extend_from_slice(b"/// ");
606 self.out.extend_from_slice(line.as_bytes());
607 }
608
609 if is_first && is_root {
611 self.out.push(b'\n');
612 } else {
613 self.write_newline_indent();
614 }
615
616 if can_be_bare(key) {
618 self.out.extend_from_slice(key.as_bytes());
619 } else {
620 self.write_quoted_string(key);
621 }
622 self.out.push(b' ');
623 }
624
625 pub fn write_doc_comment_and_key_raw(&mut self, doc: &str, key: &str) {
628 let (is_first, is_root) = match self.stack.last() {
630 Some(Context::Struct { first, is_root, .. }) => (*first, *is_root),
631 _ => (true, false),
632 };
633
634 if let Some(Context::Struct {
636 first,
637 force_multiline,
638 ..
639 }) = self.stack.last_mut()
640 {
641 *first = false;
642 *force_multiline = true;
643 }
644
645 self.fix_comma_separators();
647
648 self.propagate_multiline_to_parents();
650
651 if !is_first && is_root {
654 self.out.push(b'\n');
656 }
657
658 let need_leading_newline = !is_first || !is_root;
660
661 for (i, line) in doc.lines().enumerate() {
662 if i > 0 || need_leading_newline {
663 self.write_newline_indent();
664 }
665 self.out.extend_from_slice(b"/// ");
666 self.out.extend_from_slice(line.as_bytes());
667 }
668
669 if is_first && is_root {
671 self.out.push(b'\n');
672 } else {
673 self.write_newline_indent();
674 }
675
676 self.out.extend_from_slice(key.as_bytes());
678 self.out.push(b' ');
679 }
680
681 fn propagate_multiline_to_parents(&mut self) {
688 let mut all_comma_positions: Vec<usize> = Vec::new();
691 let mut structs_to_fix: Vec<usize> = Vec::new(); for (idx, ctx) in self.stack.iter().enumerate() {
694 if let Context::Struct {
695 inline_start,
696 force_multiline,
697 is_root,
698 comma_positions,
699 ..
700 } = ctx
701 && !*is_root
702 && *inline_start
703 && !*force_multiline
704 {
705 structs_to_fix.push(idx);
706 all_comma_positions.extend(comma_positions.iter().copied());
707 }
708 }
709
710 if !all_comma_positions.is_empty() {
712 all_comma_positions.sort_unstable();
714 for &comma_pos in all_comma_positions.iter().rev() {
715 if comma_pos + 2 <= self.out.len()
717 && self.out[comma_pos] == b','
718 && self.out[comma_pos + 1] == b' '
719 {
720 let indent = self.options.indent.repeat(self.indent_depth());
722 let newline_indent = format!("\n{}", indent);
723 self.out.drain(comma_pos..comma_pos + 2);
724 let bytes = newline_indent.as_bytes();
725 for (i, &b) in bytes.iter().enumerate() {
726 self.out.insert(comma_pos + i, b);
727 }
728 }
729 }
730 }
731
732 for &idx in &structs_to_fix {
734 if let Some(Context::Struct {
735 comma_positions, ..
736 }) = self.stack.get_mut(idx)
737 {
738 comma_positions.clear();
739 }
740 }
741
742 let mut fixes: Vec<(usize, usize)> = Vec::new(); let mut effective_depth = 0;
746 for ctx in self.stack.iter_mut() {
747 if let Context::Struct {
748 inline_start,
749 force_multiline,
750 is_root,
751 open_brace_pos: Some(pos),
752 ..
753 } = ctx
754 {
755 if !*is_root {
757 if *inline_start && !*force_multiline {
759 *force_multiline = true;
760 fixes.push((*pos, effective_depth + 1));
762 }
763 effective_depth += 1;
765 }
766 }
767 }
768
769 for (pos, indent_depth) in fixes.into_iter().rev() {
771 let indent = self.options.indent.repeat(indent_depth);
773 let insert = format!("\n{}", indent);
774 for (i, b) in insert.bytes().enumerate() {
775 self.out.insert(pos + i, b);
776 }
777
778 for ctx in self.stack.iter_mut() {
780 if let Context::Struct {
781 open_brace_pos: Some(p),
782 comma_positions,
783 ..
784 } = ctx
785 {
786 if *p > pos {
787 *p += insert.len();
788 }
789 for cp in comma_positions.iter_mut() {
790 if *cp >= pos {
791 *cp += insert.len();
792 }
793 }
794 }
795 }
796 }
797 }
798
799 fn fix_comma_separators(&mut self) {
802 let comma_positions = match self.stack.last_mut() {
804 Some(Context::Struct {
805 comma_positions, ..
806 }) => std::mem::take(comma_positions),
807 _ => return,
808 };
809
810 if comma_positions.is_empty() {
811 return;
812 }
813
814 let indent = self.options.indent.repeat(self.indent_depth());
816 let newline_indent = format!("\n{}", indent);
817
818 for &comma_pos in comma_positions.iter().rev() {
820 if comma_pos + 2 <= self.out.len()
823 && self.out[comma_pos] == b','
824 && self.out[comma_pos + 1] == b' '
825 {
826 self.out.drain(comma_pos..comma_pos + 2);
828 let bytes = newline_indent.as_bytes();
829 for (i, &b) in bytes.iter().enumerate() {
830 self.out.insert(comma_pos + i, b);
831 }
832 }
833 }
834 }
835
836 pub fn before_value(&mut self) {
838 if self.skip_next_before_value {
840 self.skip_next_before_value = false;
841 if let Some(Context::Seq { first, .. }) = self.stack.last_mut() {
843 *first = false;
844 }
845 return;
846 }
847
848 let (is_seq, is_first, inline_start) = match self.stack.last() {
850 Some(Context::Seq {
851 first,
852 inline_start,
853 }) => (true, *first, *inline_start),
854 _ => (false, true, false),
855 };
856
857 if is_seq && !is_first {
858 if inline_start || self.should_inline() {
860 self.out.push(b' ');
861 } else {
862 self.write_newline_indent();
863 }
864 }
865
866 if let Some(Context::Seq { first, .. }) = self.stack.last_mut() {
868 *first = false;
869 }
870 }
871
872 fn write_scalar_string(&mut self, s: &str) {
874 let force_quote = self.force_quote_next_scalar;
876 self.force_quote_next_scalar = false;
877
878 if !force_quote && can_be_bare(s) {
880 self.out.extend_from_slice(s.as_bytes());
881 return;
882 }
883
884 let newline_count = count_newlines(s);
885 let escape_count = count_escapes(s);
886
887 if newline_count >= self.options.heredoc_line_threshold {
889 self.write_heredoc(s);
890 return;
891 }
892
893 if escape_count > 3 && !s.contains("\"#") {
895 self.write_raw_string(s);
896 return;
897 }
898
899 self.write_quoted_string(s);
901 }
902
903 fn write_quoted_string(&mut self, s: &str) {
905 self.out.push(b'"');
906 let escaped = escape_quoted(s);
907 self.out.extend_from_slice(escaped.as_bytes());
908 self.out.push(b'"');
909 }
910
911 fn write_raw_string(&mut self, s: &str) {
913 let mut hashes = 0;
915 let mut check = String::from("\"");
916 while s.contains(&check) {
917 hashes += 1;
918 check = format!("\"{}#", "#".repeat(hashes - 1));
919 }
920
921 self.out.push(b'r');
922 for _ in 0..hashes {
923 self.out.push(b'#');
924 }
925 self.out.push(b'"');
926 self.out.extend_from_slice(s.as_bytes());
927 self.out.push(b'"');
928 for _ in 0..hashes {
929 self.out.push(b'#');
930 }
931 }
932
933 fn write_heredoc(&mut self, s: &str) {
935 let delimiters = ["TEXT", "END", "HEREDOC", "DOC", "STR", "CONTENT"];
937 let delimiter = delimiters
938 .iter()
939 .find(|d| !s.contains(*d))
940 .unwrap_or(&"TEXT");
941
942 self.out.extend_from_slice(b"<<");
943 self.out.extend_from_slice(delimiter.as_bytes());
944 self.out.push(b'\n');
945 self.out.extend_from_slice(s.as_bytes());
946 if !s.ends_with('\n') {
947 self.out.push(b'\n');
948 }
949 self.out.extend_from_slice(delimiter.as_bytes());
950 }
951}
952
953impl Default for StyxWriter {
954 fn default() -> Self {
955 Self::new()
956 }
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962
963 #[test]
964 fn test_simple_struct() {
965 let mut w = StyxWriter::new();
966 w.begin_struct(true);
967 w.field_key("name").unwrap();
968 w.write_string("hello");
969 w.field_key("value").unwrap();
970 w.write_i64(42);
971 w.end_struct().unwrap();
972
973 let result = w.finish_string();
974 assert!(result.contains("name hello"));
975 assert!(result.contains("value 42"));
976 }
977
978 #[test]
979 fn test_nested_inline() {
980 let mut w = StyxWriter::with_options(FormatOptions::default());
981 w.begin_struct(true);
982 w.field_key("point").unwrap();
983 w.begin_struct(false);
984 w.field_key("x").unwrap();
985 w.write_i64(10);
986 w.field_key("y").unwrap();
987 w.write_i64(20);
988 w.end_struct().unwrap();
989 w.end_struct().unwrap();
990
991 let result = w.finish_string();
992 assert!(result.contains("{x 10, y 20}"));
994 }
995
996 #[test]
997 fn test_sequence() {
998 let mut w = StyxWriter::new();
999 w.begin_struct(true);
1000 w.field_key("items").unwrap();
1001 w.begin_seq();
1002 w.write_i64(1);
1003 w.write_i64(2);
1004 w.write_i64(3);
1005 w.end_seq().unwrap();
1006 w.end_struct().unwrap();
1007
1008 let result = w.finish_string();
1009 assert!(result.contains("items (1 2 3)"));
1010 }
1011
1012 #[test]
1013 fn test_quoted_string() {
1014 let mut w = StyxWriter::new();
1015 w.begin_struct(true);
1016 w.field_key("message").unwrap();
1017 w.write_string("hello world");
1018 w.end_struct().unwrap();
1019
1020 let result = w.finish_string();
1021 assert!(result.contains("message \"hello world\""));
1022 }
1023
1024 #[test]
1025 fn test_force_inline() {
1026 let mut w = StyxWriter::with_options(FormatOptions::default().inline());
1027 w.begin_struct(false);
1028 w.field_key("a").unwrap();
1029 w.write_i64(1);
1030 w.field_key("b").unwrap();
1031 w.write_i64(2);
1032 w.end_struct().unwrap();
1033
1034 let result = w.finish_string();
1035 assert_eq!(result, "{a 1, b 2}");
1036 }
1037
1038 #[test]
1039 fn test_doc_comment_fixes_commas() {
1040 let mut w = StyxWriter::with_options(FormatOptions::default().inline());
1043 w.begin_struct(false);
1044 w.field_key("a").unwrap();
1045 w.write_i64(1);
1046 w.field_key("b").unwrap();
1047 w.write_i64(2);
1048 w.write_doc_comment_and_key("A documented field", "c");
1050 w.write_i64(3);
1051 w.end_struct().unwrap();
1052
1053 let result = w.finish_string();
1054 assert!(
1056 !result.contains(", "),
1057 "Result should not contain commas after doc comment: {}",
1058 result
1059 );
1060 assert!(
1062 result.contains("a 1\n"),
1063 "Expected newline after a: {}",
1064 result
1065 );
1066 }
1067}