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 clear_skip_before_value(&mut self) {
542 self.skip_next_before_value = false;
543 }
544
545 pub fn begin_seq_after_tag(&mut self) {
547 self.force_quote_next_scalar = false;
549 self.out.push(b'(');
550 self.stack.push(Context::Seq {
552 first: true,
553 inline_start: true,
554 });
555 }
556
557 pub fn write_doc_comment_and_key(&mut self, doc: &str, key: &str) {
560 let (is_first, is_root) = match self.stack.last() {
562 Some(Context::Struct { first, is_root, .. }) => (*first, *is_root),
563 _ => (true, false),
564 };
565
566 if let Some(Context::Struct {
568 first,
569 force_multiline,
570 ..
571 }) = self.stack.last_mut()
572 {
573 *first = false;
574 *force_multiline = true;
575 }
576
577 self.fix_comma_separators();
579
580 self.propagate_multiline_to_parents();
582
583 if !is_first && is_root {
586 self.out.push(b'\n');
588 }
589
590 let need_leading_newline = !is_first || !is_root;
592
593 for (i, line) in doc.lines().enumerate() {
594 if i > 0 || need_leading_newline {
595 self.write_newline_indent();
596 }
597 self.out.extend_from_slice(b"/// ");
598 self.out.extend_from_slice(line.as_bytes());
599 }
600
601 if is_first && is_root {
603 self.out.push(b'\n');
604 } else {
605 self.write_newline_indent();
606 }
607
608 if can_be_bare(key) {
610 self.out.extend_from_slice(key.as_bytes());
611 } else {
612 self.write_quoted_string(key);
613 }
614 self.out.push(b' ');
615 }
616
617 pub fn write_doc_comment_and_key_raw(&mut self, doc: &str, key: &str) {
620 let (is_first, is_root) = match self.stack.last() {
622 Some(Context::Struct { first, is_root, .. }) => (*first, *is_root),
623 _ => (true, false),
624 };
625
626 if let Some(Context::Struct {
628 first,
629 force_multiline,
630 ..
631 }) = self.stack.last_mut()
632 {
633 *first = false;
634 *force_multiline = true;
635 }
636
637 self.fix_comma_separators();
639
640 self.propagate_multiline_to_parents();
642
643 if !is_first && is_root {
646 self.out.push(b'\n');
648 }
649
650 let need_leading_newline = !is_first || !is_root;
652
653 for (i, line) in doc.lines().enumerate() {
654 if i > 0 || need_leading_newline {
655 self.write_newline_indent();
656 }
657 self.out.extend_from_slice(b"/// ");
658 self.out.extend_from_slice(line.as_bytes());
659 }
660
661 if is_first && is_root {
663 self.out.push(b'\n');
664 } else {
665 self.write_newline_indent();
666 }
667
668 self.out.extend_from_slice(key.as_bytes());
670 self.out.push(b' ');
671 }
672
673 fn propagate_multiline_to_parents(&mut self) {
680 let mut all_comma_positions: Vec<usize> = Vec::new();
683 let mut structs_to_fix: Vec<usize> = Vec::new(); for (idx, ctx) in self.stack.iter().enumerate() {
686 if let Context::Struct {
687 inline_start,
688 force_multiline,
689 is_root,
690 comma_positions,
691 ..
692 } = ctx
693 && !*is_root
694 && *inline_start
695 && !*force_multiline
696 {
697 structs_to_fix.push(idx);
698 all_comma_positions.extend(comma_positions.iter().copied());
699 }
700 }
701
702 if !all_comma_positions.is_empty() {
704 all_comma_positions.sort_unstable();
706 for &comma_pos in all_comma_positions.iter().rev() {
707 if comma_pos + 2 <= self.out.len()
709 && self.out[comma_pos] == b','
710 && self.out[comma_pos + 1] == b' '
711 {
712 let indent = self.options.indent.repeat(self.indent_depth());
714 let newline_indent = format!("\n{}", indent);
715 self.out.drain(comma_pos..comma_pos + 2);
716 let bytes = newline_indent.as_bytes();
717 for (i, &b) in bytes.iter().enumerate() {
718 self.out.insert(comma_pos + i, b);
719 }
720 }
721 }
722 }
723
724 for &idx in &structs_to_fix {
726 if let Some(Context::Struct {
727 comma_positions, ..
728 }) = self.stack.get_mut(idx)
729 {
730 comma_positions.clear();
731 }
732 }
733
734 let mut fixes: Vec<(usize, usize)> = Vec::new(); let mut effective_depth = 0;
738 for ctx in self.stack.iter_mut() {
739 if let Context::Struct {
740 inline_start,
741 force_multiline,
742 is_root,
743 open_brace_pos: Some(pos),
744 ..
745 } = ctx
746 {
747 if !*is_root {
749 if *inline_start && !*force_multiline {
751 *force_multiline = true;
752 fixes.push((*pos, effective_depth + 1));
754 }
755 effective_depth += 1;
757 }
758 }
759 }
760
761 for (pos, indent_depth) in fixes.into_iter().rev() {
763 let indent = self.options.indent.repeat(indent_depth);
765 let insert = format!("\n{}", indent);
766 for (i, b) in insert.bytes().enumerate() {
767 self.out.insert(pos + i, b);
768 }
769
770 for ctx in self.stack.iter_mut() {
772 if let Context::Struct {
773 open_brace_pos: Some(p),
774 comma_positions,
775 ..
776 } = ctx
777 {
778 if *p > pos {
779 *p += insert.len();
780 }
781 for cp in comma_positions.iter_mut() {
782 if *cp >= pos {
783 *cp += insert.len();
784 }
785 }
786 }
787 }
788 }
789 }
790
791 fn fix_comma_separators(&mut self) {
794 let comma_positions = match self.stack.last_mut() {
796 Some(Context::Struct {
797 comma_positions, ..
798 }) => std::mem::take(comma_positions),
799 _ => return,
800 };
801
802 if comma_positions.is_empty() {
803 return;
804 }
805
806 let indent = self.options.indent.repeat(self.indent_depth());
808 let newline_indent = format!("\n{}", indent);
809
810 for &comma_pos in comma_positions.iter().rev() {
812 if comma_pos + 2 <= self.out.len()
815 && self.out[comma_pos] == b','
816 && self.out[comma_pos + 1] == b' '
817 {
818 self.out.drain(comma_pos..comma_pos + 2);
820 let bytes = newline_indent.as_bytes();
821 for (i, &b) in bytes.iter().enumerate() {
822 self.out.insert(comma_pos + i, b);
823 }
824 }
825 }
826 }
827
828 pub fn before_value(&mut self) {
830 if self.skip_next_before_value {
832 self.skip_next_before_value = false;
833 if let Some(Context::Seq { first, .. }) = self.stack.last_mut() {
835 *first = false;
836 }
837 return;
838 }
839
840 let (is_seq, is_first, inline_start) = match self.stack.last() {
842 Some(Context::Seq {
843 first,
844 inline_start,
845 }) => (true, *first, *inline_start),
846 _ => (false, true, false),
847 };
848
849 if is_seq && !is_first {
850 if inline_start || self.should_inline() {
852 self.out.push(b' ');
853 } else {
854 self.write_newline_indent();
855 }
856 }
857
858 if let Some(Context::Seq { first, .. }) = self.stack.last_mut() {
860 *first = false;
861 }
862 }
863
864 fn write_scalar_string(&mut self, s: &str) {
866 let force_quote = self.force_quote_next_scalar;
868 self.force_quote_next_scalar = false;
869
870 if !force_quote && can_be_bare(s) {
872 self.out.extend_from_slice(s.as_bytes());
873 return;
874 }
875
876 let newline_count = count_newlines(s);
877 let escape_count = count_escapes(s);
878
879 if newline_count >= self.options.heredoc_line_threshold {
881 self.write_heredoc(s);
882 return;
883 }
884
885 if escape_count > 3 && !s.contains("\"#") {
887 self.write_raw_string(s);
888 return;
889 }
890
891 self.write_quoted_string(s);
893 }
894
895 fn write_quoted_string(&mut self, s: &str) {
897 self.out.push(b'"');
898 let escaped = escape_quoted(s);
899 self.out.extend_from_slice(escaped.as_bytes());
900 self.out.push(b'"');
901 }
902
903 fn write_raw_string(&mut self, s: &str) {
905 let mut hashes = 0;
907 let mut check = String::from("\"");
908 while s.contains(&check) {
909 hashes += 1;
910 check = format!("\"{}#", "#".repeat(hashes - 1));
911 }
912
913 self.out.push(b'r');
914 for _ in 0..hashes {
915 self.out.push(b'#');
916 }
917 self.out.push(b'"');
918 self.out.extend_from_slice(s.as_bytes());
919 self.out.push(b'"');
920 for _ in 0..hashes {
921 self.out.push(b'#');
922 }
923 }
924
925 fn write_heredoc(&mut self, s: &str) {
927 let delimiters = ["TEXT", "END", "HEREDOC", "DOC", "STR", "CONTENT"];
929 let delimiter = delimiters
930 .iter()
931 .find(|d| !s.contains(*d))
932 .unwrap_or(&"TEXT");
933
934 self.out.extend_from_slice(b"<<");
935 self.out.extend_from_slice(delimiter.as_bytes());
936 self.out.push(b'\n');
937 self.out.extend_from_slice(s.as_bytes());
938 if !s.ends_with('\n') {
939 self.out.push(b'\n');
940 }
941 self.out.extend_from_slice(delimiter.as_bytes());
942 }
943}
944
945impl Default for StyxWriter {
946 fn default() -> Self {
947 Self::new()
948 }
949}
950
951#[cfg(test)]
952mod tests {
953 use super::*;
954
955 #[test]
956 fn test_simple_struct() {
957 let mut w = StyxWriter::new();
958 w.begin_struct(true);
959 w.field_key("name").unwrap();
960 w.write_string("hello");
961 w.field_key("value").unwrap();
962 w.write_i64(42);
963 w.end_struct().unwrap();
964
965 let result = w.finish_string();
966 assert!(result.contains("name hello"));
967 assert!(result.contains("value 42"));
968 }
969
970 #[test]
971 fn test_nested_inline() {
972 let mut w = StyxWriter::with_options(FormatOptions::default());
973 w.begin_struct(true);
974 w.field_key("point").unwrap();
975 w.begin_struct(false);
976 w.field_key("x").unwrap();
977 w.write_i64(10);
978 w.field_key("y").unwrap();
979 w.write_i64(20);
980 w.end_struct().unwrap();
981 w.end_struct().unwrap();
982
983 let result = w.finish_string();
984 assert!(result.contains("{x 10, y 20}"));
986 }
987
988 #[test]
989 fn test_sequence() {
990 let mut w = StyxWriter::new();
991 w.begin_struct(true);
992 w.field_key("items").unwrap();
993 w.begin_seq();
994 w.write_i64(1);
995 w.write_i64(2);
996 w.write_i64(3);
997 w.end_seq().unwrap();
998 w.end_struct().unwrap();
999
1000 let result = w.finish_string();
1001 assert!(result.contains("items (1 2 3)"));
1002 }
1003
1004 #[test]
1005 fn test_quoted_string() {
1006 let mut w = StyxWriter::new();
1007 w.begin_struct(true);
1008 w.field_key("message").unwrap();
1009 w.write_string("hello world");
1010 w.end_struct().unwrap();
1011
1012 let result = w.finish_string();
1013 assert!(result.contains("message \"hello world\""));
1014 }
1015
1016 #[test]
1017 fn test_force_inline() {
1018 let mut w = StyxWriter::with_options(FormatOptions::default().inline());
1019 w.begin_struct(false);
1020 w.field_key("a").unwrap();
1021 w.write_i64(1);
1022 w.field_key("b").unwrap();
1023 w.write_i64(2);
1024 w.end_struct().unwrap();
1025
1026 let result = w.finish_string();
1027 assert_eq!(result, "{a 1, b 2}");
1028 }
1029
1030 #[test]
1031 fn test_doc_comment_fixes_commas() {
1032 let mut w = StyxWriter::with_options(FormatOptions::default().inline());
1035 w.begin_struct(false);
1036 w.field_key("a").unwrap();
1037 w.write_i64(1);
1038 w.field_key("b").unwrap();
1039 w.write_i64(2);
1040 w.write_doc_comment_and_key("A documented field", "c");
1042 w.write_i64(3);
1043 w.end_struct().unwrap();
1044
1045 let result = w.finish_string();
1046 assert!(
1048 !result.contains(", "),
1049 "Result should not contain commas after doc comment: {}",
1050 result
1051 );
1052 assert!(
1054 result.contains("a 1\n"),
1055 "Expected newline after a: {}",
1056 result
1057 );
1058 }
1059}