1#[cfg(target_arch = "wasm32")]
24mod wasm;
25
26mod error;
27mod number;
28mod options;
29mod parse;
30mod render;
31mod util;
32mod value;
33
34pub use error::{Error, ParseError, Result};
35pub use options::{
36 BareStyle, FoldStyle, IndentGlyphMarkerStyle, IndentGlyphStyle, MultilineStyle,
37 StringArrayStyle, TableUnindentStyle, RenderOptions,
38};
39pub use number::{InvalidNumber, Number};
40pub use value::{Entry, Value};
41#[doc(hidden)]
42pub use options::TjsonConfig;
43
44pub const MIN_WRAP_WIDTH: usize = options::MIN_WRAP_WIDTH;
45pub const DEFAULT_WRAP_WIDTH: usize = options::DEFAULT_WRAP_WIDTH;
46
47use serde::Serialize;
48use serde::de::DeserializeOwned;
49
50use parse::ParseOptions;
51
52
53fn parse_str_with_options(input: &str, options: ParseOptions) -> Result<Value> {
54 parse::Parser::parse_document(input, options.start_indent).map_err(Error::Parse)
55}
56
57#[cfg(test)]
58fn render_string(value: &Value) -> String {
59 value.to_tjson_with(RenderOptions::default())
60}
61
62#[cfg(test)]
63fn render_string_with_options(value: &Value, options: RenderOptions) -> String {
64 value.to_tjson_with(options)
65}
66
67pub fn from_str<T: DeserializeOwned>(input: &str) -> Result<T> {
77 from_tjson_str_with_options(input, ParseOptions::default())
78}
79
80fn from_tjson_str_with_options<T: DeserializeOwned>(
81 input: &str,
82 options: ParseOptions,
83) -> Result<T> {
84 let value = parse_str_with_options(input, options)?;
85 Ok(serde_json::from_str(&value.to_json())?)
86}
87
88pub fn to_string<T: Serialize>(value: &T) -> Result<String> {
98 to_string_with(value, RenderOptions::default())
99}
100
101pub fn to_string_with<T: Serialize>(
108 value: &T,
109 options: RenderOptions,
110) -> Result<String> {
111 let json = serde_json::to_value(value)?;
112 let value = Value::from_serde_json(json);
113 Ok(value.to_tjson_with(options))
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use serde_json::Value as JsonValue;
120
121 fn json(input: &str) -> JsonValue {
122 serde_json::from_str(input).unwrap()
123 }
124
125 fn tjson_value(input: &str) -> Value {
126 Value::from(json(input))
127 }
128
129 fn parse_str(input: &str) -> Result<Value> {
130 input.parse()
131 }
132
133 fn to_json_value(v: Value) -> JsonValue {
134 serde_json::from_str(&v.to_json()).unwrap()
135 }
136 #[test]
137 fn parses_basic_scalar_examples() {
138 assert_eq!(
139 to_json_value(parse_str("null").unwrap()),
140 json("null")
141 );
142 assert_eq!(
143 to_json_value(parse_str("5").unwrap()),
144 json("5")
145 );
146 assert_eq!(
147 to_json_value(parse_str(" a").unwrap()),
148 json("\"a\"")
149 );
150 assert_eq!(
151 to_json_value(parse_str("[]").unwrap()),
152 json("[]")
153 );
154 assert_eq!(
155 to_json_value(parse_str("{}").unwrap()),
156 json("{}")
157 );
158 }
159
160 #[test]
161 fn parses_comments_and_marker_examples() {
162 let input = "// comment\n a:5\n// comment\n x:\n [ [ 1\n { b: text";
163 let expected = json("{\"a\":5,\"x\":[[1],{\"b\":\"text\"}]}");
164 assert_eq!(
165 to_json_value(parse_str(input).unwrap()),
166 expected
167 );
168 }
169
170 #[test]
175 fn parses_folded_json_string_example() {
176 let input =
177 "\"foldingat\n/ onlyafew\\r\\n\n/ characters\n/ hereusing\n/ somejson\n/ escapes\\\\\"";
178 let expected = json("\"foldingatonlyafew\\r\\ncharactershereusingsomejsonescapes\\\\\"");
179 assert_eq!(
180 to_json_value(parse_str(input).unwrap()),
181 expected
182 );
183 }
184
185 #[test]
186 fn parses_folded_json_string_as_object_value() {
187 let input = " note:\"hello \n / world\"";
189 let expected = json("{\"note\":\"hello world\"}");
190 assert_eq!(
191 to_json_value(parse_str(input).unwrap()),
192 expected
193 );
194 }
195
196 #[test]
197 fn parses_folded_json_string_multiple_continuations() {
198 let input = "\"one\n/ two\n/ three\n/ four\"";
200 let expected = json("\"onetwothreefour\"");
201 assert_eq!(
202 to_json_value(parse_str(input).unwrap()),
203 expected
204 );
205 }
206
207 #[test]
208 fn parses_folded_json_string_with_indent() {
209 let input = " key:\"hello \n / world\"";
211 let expected = json("{\"key\":\"hello world\"}");
212 assert_eq!(
213 to_json_value(parse_str(input).unwrap()),
214 expected
215 );
216 }
217
218 #[test]
221 fn parses_folded_bare_string_root() {
222 let input = " hello\n/ world";
224 let expected = json("\"helloworld\"");
225 assert_eq!(
226 to_json_value(parse_str(input).unwrap()),
227 expected
228 );
229 }
230
231 #[test]
232 fn parses_folded_bare_string_as_object_value() {
233 let input = " note: hello\n / world";
235 let expected = json("{\"note\":\"helloworld\"}");
236 assert_eq!(
237 to_json_value(parse_str(input).unwrap()),
238 expected
239 );
240 }
241
242 #[test]
243 fn parses_folded_bare_string_multiple_continuations() {
244 let input = " note: one\n / two\n / three";
245 let expected = json("{\"note\":\"onetwothree\"}");
246 assert_eq!(
247 to_json_value(parse_str(input).unwrap()),
248 expected
249 );
250 }
251
252 #[test]
253 fn parses_folded_bare_string_preserves_space_after_fold_marker() {
254 let input = " note: hello\n / world";
256 let expected = json("{\"note\":\"hello world\"}");
257 assert_eq!(
258 to_json_value(parse_str(input).unwrap()),
259 expected
260 );
261 }
262
263 #[test]
266 fn parses_folded_bare_key() {
267 let input = " averylongkey\n / continuation: value";
269 let expected = json("{\"averylongkeycontinuation\":\"value\"}");
270 assert_eq!(
271 to_json_value(parse_str(input).unwrap()),
272 expected
273 );
274 }
275
276 #[test]
277 fn parses_folded_json_key() {
278 let input = " \"averylongkey\n / continuation\": value";
280 let expected = json("{\"averylongkeycontinuation\":\"value\"}");
281 assert_eq!(
282 to_json_value(parse_str(input).unwrap()),
283 expected
284 );
285 }
286
287 #[test]
290 fn parses_table_with_folded_cell() {
291 let input = concat!(
293 " |name |score |\n",
294 " | Alice |100 |\n",
295 " | Bob with a very long\n",
296 "/ name |200 |\n",
297 " | Carol |300 |",
298 );
299 let expected = json(
300 "[{\"name\":\"Alice\",\"score\":100},{\"name\":\"Bob with a very longname\",\"score\":200},{\"name\":\"Carol\",\"score\":300}]"
301 );
302 assert_eq!(
303 to_json_value(parse_str(input).unwrap()),
304 expected
305 );
306 }
307
308 #[test]
309 fn parses_table_with_folded_cell_no_trailing_pipe() {
310 let input = concat!(
312 " |name |value |\n",
313 " | short |1 |\n",
314 " | this is really long\n",
315 "/ continuation|2 |",
316 );
317 let expected = json(
318 "[{\"name\":\"short\",\"value\":1},{\"name\":\"this is really longcontinuation\",\"value\":2}]"
319 );
320 assert_eq!(
321 to_json_value(parse_str(input).unwrap()),
322 expected
323 );
324 }
325
326 #[test]
327 fn parses_triple_backtick_multiline_string() {
328 let input = " note: ```\nfirst\nsecond\n indented\n ```";
330 let expected = json("{\"note\":\"first\\nsecond\\n indented\"}");
331 assert_eq!(
332 to_json_value(parse_str(input).unwrap()),
333 expected
334 );
335 }
336
337 #[test]
338 fn parses_triple_backtick_crlf_multiline_string() {
339 let input = " note: ```\\r\\n\nfirst\nsecond\n indented\n ```\\r\\n";
341 let expected = json("{\"note\":\"first\\r\\nsecond\\r\\n indented\"}");
342 assert_eq!(
343 to_json_value(parse_str(input).unwrap()),
344 expected
345 );
346 }
347
348 #[test]
349 fn parses_double_backtick_multiline_string() {
350 let input = " ``\n| first\n| second\n ``";
352 let expected = json("\"first\\nsecond\"");
353 assert_eq!(
354 to_json_value(parse_str(input).unwrap()),
355 expected
356 );
357 }
358
359 #[test]
360 fn parses_double_backtick_with_explicit_lf_indicator() {
361 let input = " ``\\n\n| first\n| second\n ``\\n";
362 let expected = json("\"first\\nsecond\"");
363 assert_eq!(
364 to_json_value(parse_str(input).unwrap()),
365 expected
366 );
367 }
368
369 #[test]
370 fn parses_double_backtick_crlf_multiline_string() {
371 let input = " ``\\r\\n\n| first\n| second\n ``\\r\\n";
373 let expected = json("\"first\\r\\nsecond\"");
374 assert_eq!(
375 to_json_value(parse_str(input).unwrap()),
376 expected
377 );
378 }
379
380 #[test]
381 fn parses_double_backtick_with_fold() {
382 let input = " ``\n| first line that is \n/ continued here\n| second\n ``";
384 let expected = json("\"first line that is continued here\\nsecond\"");
385 assert_eq!(
386 to_json_value(parse_str(input).unwrap()),
387 expected
388 );
389 }
390
391 #[test]
392 fn parses_single_backtick_multiline_string() {
393 let input = " note: `\n first\n second\n indented\n `";
395 let expected = json("{\"note\":\"first\\nsecond\\nindented\"}");
396 assert_eq!(
397 to_json_value(parse_str(input).unwrap()),
398 expected
399 );
400 }
401
402 #[test]
403 fn parses_single_backtick_with_fold() {
404 let input = " note: `\n first line that is \n / continued here\n second\n `";
406 let expected = json("{\"note\":\"first line that is continued here\\nsecond\"}");
407 assert_eq!(
408 to_json_value(parse_str(input).unwrap()),
409 expected
410 );
411 }
412
413 #[test]
414 fn parses_single_backtick_with_leading_spaces_in_content() {
415 let input = " `\n first\n indented two extra\n last\n `";
417 let expected = json("\"first\\n indented two extra\\nlast\"");
418 assert_eq!(
419 to_json_value(parse_str(input).unwrap()),
420 expected
421 );
422 }
423
424 #[test]
425 fn rejects_triple_backtick_without_closing_glyph() {
426 let input = " note: ```\nfirst\nsecond";
427 assert!(parse_str(input).is_err());
428 }
429
430 #[test]
431 fn rejects_double_backtick_without_closing_glyph() {
432 let input = " ``\n| first\n| second";
433 assert!(parse_str(input).is_err());
434 }
435
436 #[test]
437 fn rejects_single_backtick_without_closing_glyph() {
438 let input = " note: `\n first\n second";
439 assert!(parse_str(input).is_err());
440 }
441
442 #[test]
443 fn rejects_double_backtick_body_without_pipe() {
444 let input = " ``\njust some text\n| second\n ``";
445 assert!(parse_str(input).is_err());
446 }
447
448 #[test]
449 fn parses_table_array_example() {
450 let input = " |a |b |c |\n |1 | x |true |\n |2 | y |false |\n |3 | z |null |";
451 let expected = json(
452 "[{\"a\":1,\"b\":\"x\",\"c\":true},{\"a\":2,\"b\":\"y\",\"c\":false},{\"a\":3,\"b\":\"z\",\"c\":null}]",
453 );
454 assert_eq!(
455 to_json_value(parse_str(input).unwrap()),
456 expected
457 );
458 }
459
460 #[test]
461 fn parses_minimal_json_inside_array_example() {
462 let input = " [{\"a\":{\"b\":null},\"c\":3}]";
463 let expected = json("[[{\"a\":{\"b\":null},\"c\":3}]]");
464 assert_eq!(
465 to_json_value(parse_str(input).unwrap()),
466 expected
467 );
468 }
469
470 #[test]
471 fn renders_basic_scalar_examples() {
472 assert_eq!(render_string(&tjson_value("null")), "null");
473 assert_eq!(render_string(&tjson_value("5")), "5");
474 assert_eq!(render_string(&tjson_value("\"a\"")), " a");
475 assert_eq!(render_string(&tjson_value("[]")), "[]");
476 assert_eq!(render_string(&tjson_value("{}")), "{}");
477 }
478
479 #[test]
480 fn renders_multiline_string_example() {
481 let rendered =
483 render_string(&tjson_value("{\"note\":\"first\\nsecond\\n indented\"}"));
484 assert_eq!(
485 rendered,
486 " note: ``\n| first\n| second\n| indented\n ``"
487 );
488 }
489
490 #[test]
491 fn renders_crlf_multiline_string_example() {
492 let rendered = render_string(&tjson_value(
494 "{\"note\":\"first\\r\\nsecond\\r\\n indented\"}",
495 ));
496 assert_eq!(
497 rendered,
498 " note: ``\\r\\n\n| first\n| second\n| indented\n ``\\r\\n"
499 );
500 }
501
502 #[test]
503 fn renders_single_backtick_root_string() {
504 let value = Value::String("line one\nline two".to_owned());
506 let rendered = render_string_with_options(
507 &value,
508 RenderOptions { multiline_style: MultilineStyle::Floating, ..RenderOptions::default() },
509 );
510 assert_eq!(rendered, " `\n line one\n line two\n `");
511 }
512
513 #[test]
514 fn renders_single_backtick_shallow_key() {
515 let rendered = render_string_with_options(
517 &tjson_value("{\"note\":\"line one\\nline two\"}"),
518 RenderOptions { multiline_style: MultilineStyle::Floating, ..RenderOptions::default() },
519 );
520 assert_eq!(rendered, " note: `\n line one\n line two\n `");
521 }
522
523 #[test]
524 fn renders_single_backtick_deep_key() {
525 let rendered = render_string_with_options(
527 &tjson_value("{\"outer\":{\"inner\":\"line one\\nline two\"}}"),
528 RenderOptions { multiline_style: MultilineStyle::Floating, ..RenderOptions::default() },
529 );
530 assert_eq!(
531 rendered,
532 " outer:\n inner: `\n line one\n line two\n `"
533 );
534 }
535
536 #[test]
537 fn renders_single_backtick_three_lines() {
538 let rendered = render_string_with_options(
540 &tjson_value("{\"a\":{\"b\":{\"c\":\"x\\ny\\nz\"}}}"),
541 RenderOptions { multiline_style: MultilineStyle::Floating, ..RenderOptions::default() },
542 );
543 assert_eq!(
544 rendered,
545 " a:\n b:\n c: `\n x\n y\n z\n `"
546 );
547 }
548
549 #[test]
550 fn renders_double_backtick_with_bold_style() {
551 let value = Value::String("line one\nline two".to_owned());
553 let rendered = render_string_with_options(
554 &value,
555 RenderOptions {
556 multiline_style: MultilineStyle::Bold,
557 ..RenderOptions::default()
558 },
559 );
560 assert_eq!(rendered, " ``\n| line one\n| line two\n ``");
561 }
562
563 #[test]
564 fn renders_triple_backtick_with_fullwidth_style() {
565 let value = Value::String("normal line\nsecond line".to_owned());
567 let rendered = render_string_with_options(
568 &value,
569 RenderOptions {
570 multiline_style: MultilineStyle::Transparent,
571 ..RenderOptions::default()
572 },
573 );
574 assert_eq!(rendered, " ```\nnormal line\nsecond line\n ```");
575 }
576
577 #[test]
578 fn renders_triple_backtick_falls_back_to_bold_when_pipe_heavy() {
579 let value = Value::String("| piped\n| also piped\nnormal".to_owned());
581 let rendered = render_string_with_options(
582 &value,
583 RenderOptions {
584 multiline_style: MultilineStyle::Transparent,
585 ..RenderOptions::default()
586 },
587 );
588 assert!(rendered.contains(" ``"), "expected `` fallback, got: {rendered}");
589 }
590
591 #[test]
592 fn transparent_never_folds_body_lines_regardless_of_wrap() {
593 let long_line = "a".repeat(200);
596 let value = Value::String(format!("{long_line}\nsecond line"));
597 let rendered = render_string_with_options(
598 &value,
599 RenderOptions::default()
600 .wrap_width(Some(20))
601 .multiline_style(MultilineStyle::Transparent)
602 .string_multiline_fold_style(FoldStyle::Auto),
603 );
604 let body_lines: Vec<&str> = rendered.lines()
607 .filter(|l| !l.trim_start().starts_with("```") && !l.trim_start().starts_with("``"))
608 .collect();
609 for line in &body_lines {
610 assert!(!line.trim_start().starts_with("/ "), "``` body must not have fold continuations: {rendered}");
611 }
612 }
613
614 #[test]
615 fn transparent_with_string_multiline_fold_style_auto_still_no_fold() {
616 let value = Value::String("short\nsecond".to_owned());
619 let rendered = render_string_with_options(
620 &value,
621 RenderOptions::default()
622 .multiline_style(MultilineStyle::Transparent)
623 .string_multiline_fold_style(FoldStyle::Auto),
624 );
625 assert!(rendered.contains("```"), "should use triple backtick: {rendered}");
626 assert!(!rendered.contains("/ "), "Transparent must never fold: {rendered}");
627 }
628
629 #[test]
630 fn floating_falls_back_to_bold_when_line_count_exceeds_max() {
631 let value = Value::String("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk".to_owned());
633 let rendered = render_string_with_options(
634 &value,
635 RenderOptions { multiline_style: MultilineStyle::Floating, ..RenderOptions::default() },
636 );
637 assert!(rendered.starts_with(" ``"), "expected `` fallback for >10 lines, got: {rendered}");
638 }
639
640 #[test]
641 fn floating_falls_back_to_bold_when_line_overflows_width() {
642 let long_line = "x".repeat(80); let value = Value::String(format!("short\n{long_line}"));
645 let rendered = render_string_with_options(
646 &value,
647 RenderOptions { multiline_style: MultilineStyle::Floating, ..RenderOptions::default() },
648 );
649 assert!(rendered.starts_with(" ``"), "expected `` fallback for overflow, got: {rendered}");
650 }
651
652 #[test]
653 fn floating_renders_single_backtick_when_lines_fit() {
654 let value = Value::String("normal line\nsecond line".to_owned());
656 let rendered = render_string_with_options(
657 &value,
658 RenderOptions { multiline_style: MultilineStyle::Floating, ..RenderOptions::default() },
659 );
660 assert!(rendered.starts_with(" `\n"), "expected ` glyph, got: {rendered}");
661 assert!(!rendered.contains("| "), "should not have pipe markers");
662 }
663
664 #[test]
665 fn light_uses_single_backtick_when_safe() {
666 let value = Value::String("short\nsecond".to_owned());
667 let rendered = render_string_with_options(
668 &value,
669 RenderOptions { multiline_style: MultilineStyle::Light, ..RenderOptions::default() },
670 );
671 assert!(rendered.starts_with(" `\n"), "expected ` glyph, got: {rendered}");
672 }
673
674 #[test]
675 fn light_stays_single_backtick_on_overflow() {
676 let long = "x".repeat(80);
678 let value = Value::String(format!("short\n{long}"));
679 let rendered = render_string_with_options(
680 &value,
681 RenderOptions { multiline_style: MultilineStyle::Light, ..RenderOptions::default() },
682 );
683 assert!(rendered.starts_with(" `\n"), "Light should stay as `, got: {rendered}");
684 assert!(!rendered.contains("``"), "Light must not escalate to `` on overflow");
685 }
686
687 #[test]
688 fn light_stays_single_backtick_on_too_many_lines() {
689 let value = Value::String("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk".to_owned());
691 let rendered = render_string_with_options(
692 &value,
693 RenderOptions { multiline_style: MultilineStyle::Light, ..RenderOptions::default() },
694 );
695 assert!(rendered.starts_with(" `\n"), "Light should stay as `, got: {rendered}");
696 assert!(!rendered.contains("``"), "Light must not escalate to `` on line count");
697 }
698
699 #[test]
700 fn light_falls_back_to_bold_on_dangerous_content() {
701 let value = Value::String("| piped\n| also piped\nnormal".to_owned());
703 let rendered = render_string_with_options(
704 &value,
705 RenderOptions { multiline_style: MultilineStyle::Light, ..RenderOptions::default() },
706 );
707 assert!(rendered.starts_with(" ``"), "Light should fall back to `` for pipe-heavy content, got: {rendered}");
708 }
709
710 #[test]
711 fn folding_quotes_uses_json_string_for_eol_strings() {
712 let value = Value::String("first line\nsecond line".to_owned());
713 let rendered = render_string_with_options(
714 &value,
715 RenderOptions { multiline_style: MultilineStyle::FoldingQuotes, ..RenderOptions::default() },
716 );
717 assert!(rendered.starts_with(" \"") || rendered.starts_with("\""),
718 "expected JSON string, got: {rendered}");
719 assert!(!rendered.contains('`'), "FoldingQuotes must not use multiline glyphs");
720 }
721
722 #[test]
723 fn folding_quotes_single_line_strings_unchanged() {
724 let value = Value::String("hello world".to_owned());
726 let rendered = render_string_with_options(
727 &value,
728 RenderOptions { multiline_style: MultilineStyle::FoldingQuotes, ..RenderOptions::default() },
729 );
730 assert_eq!(rendered, " hello world");
731 }
732
733 #[test]
734 fn folding_quotes_folds_long_eol_string() {
735 let value = Value::String("long string with spaces that needs folding\nsecond".to_owned());
739 let rendered = render_string_with_options(
740 &value,
741 RenderOptions {
742 multiline_style: MultilineStyle::FoldingQuotes,
743 wrap_width: Some(40),
744 ..RenderOptions::default()
745 },
746 );
747 assert!(rendered.contains("/ "), "expected fold continuation, got: {rendered}");
748 assert!(!rendered.contains('`'), "must not use multiline glyphs");
749 }
750
751 #[test]
752 fn folding_quotes_skips_fold_when_overrun_within_25_percent() {
753 let value = Value::String("abcdefghijklmnopqrstuvwxyz123456\nsecond".to_owned());
756 let rendered = render_string_with_options(
757 &value,
758 RenderOptions {
759 multiline_style: MultilineStyle::FoldingQuotes,
760 wrap_width: Some(40),
761 ..RenderOptions::default()
762 },
763 );
764 assert_eq!(rendered, "\"abcdefghijklmnopqrstuvwxyz123456\\n\n/ second\"");
765 }
766
767 #[test]
768 fn mixed_newlines_fall_back_to_json_string() {
769 let rendered =
770 render_string(&tjson_value("{\"note\":\"first\\r\\nsecond\\nthird\"}"));
771 assert_eq!(rendered, " note:\"first\\r\\nsecond\\nthird\"");
772 }
773
774 #[test]
775 fn escapes_forbidden_characters_in_json_strings() {
776 let rendered = render_string(&tjson_value("{\"note\":\"a\\u200Db\"}"));
777 assert_eq!(rendered, " note:\"a\\u200db\"");
778 }
779
780 #[test]
781 fn forbidden_characters_force_multiline_fallback_to_json_string() {
782 let rendered = render_string(&tjson_value("{\"lines\":\"x\\ny\\u200Dz\"}"));
783 assert_eq!(rendered, " lines:\"x\\ny\\u200dz\"");
784 }
785
786 #[test]
787 fn pipe_heavy_content_falls_back_to_double_backtick() {
788 let value = Value::String("| line one\n| line two\nnormal line".to_owned());
791 let rendered = render_string(&value);
792 assert!(rendered.contains(" ``"), "expected `` glyph, got: {rendered}");
793 assert!(rendered.contains("| | line one"), "expected piped body");
794 }
795
796 #[test]
797 fn triple_backtick_collision_falls_back_to_double_backtick() {
798 let value = Value::String(" ```\nsecond line".to_owned());
801 let rendered = render_string(&value);
802 assert!(rendered.contains(" ``"), "expected `` glyph, got: {rendered}");
803 }
804
805 #[test]
806 fn backtick_content_falls_back_to_double_backtick() {
807 let value = Value::String("normal line\n `` something".to_owned());
810 let rendered = render_string(&value);
811 assert!(rendered.contains(" ``"), "expected `` glyph, got: {rendered}");
812 assert!(rendered.contains("| normal line"), "expected pipe-guarded body");
813 }
814
815 #[test]
816 fn rejects_raw_forbidden_characters() {
817 let input = format!(" note:\"a{}b\"", '\u{200D}');
818 let error = parse_str(&input).unwrap_err();
819 assert!(error.to_string().contains("U+200D"));
820 }
821
822 #[test]
823 fn renders_table_when_eligible() {
824 let value = tjson_value(
825 "[{\"a\":1,\"b\":\"x\",\"c\":true},{\"a\":2,\"b\":\"y\",\"c\":false},{\"a\":3,\"b\":\"z\",\"c\":null}]",
826 );
827 let rendered = render_string(&value);
828 assert_eq!(
829 rendered,
830 " |a |b |c |\n |1 | x |true |\n |2 | y |false |\n |3 | z |null |"
831 );
832 }
833
834 #[test]
835 fn table_rejected_when_shared_keys_have_different_order() {
836 let value = tjson_value(
839 "[{\"a\":1,\"b\":2,\"c\":3},{\"b\":4,\"a\":5,\"c\":6},{\"a\":7,\"b\":8,\"c\":9}]",
840 );
841 let rendered = render_string(&value);
842 assert!(!rendered.contains('|'), "should not render as table when key order differs: {rendered}");
843 }
844
845 #[test]
846 fn table_allowed_when_rows_have_subset_of_keys() {
847 let value = tjson_value(
849 "[{\"a\":1,\"b\":2,\"c\":3},{\"a\":4,\"b\":5},{\"a\":6,\"b\":7,\"c\":8}]",
850 );
851 let rendered = render_string_with_options(
852 &value,
853 RenderOptions::default().table_min_similarity(0.5),
854 );
855 assert!(rendered.contains('|'), "should render as table when rows are a subset: {rendered}");
856 }
857
858 #[test]
859 fn renders_table_for_array_object_values() {
860 let value = tjson_value(
861 "{\"people\":[{\"name\":\"Alice\",\"age\":30,\"active\":true},{\"name\":\"Bob\",\"age\":25,\"active\":false},{\"name\":\"Carol\",\"age\":35,\"active\":true}]}",
862 );
863 let rendered = render_string(&value);
864 assert_eq!(
865 rendered,
866 " people:\n |name |age |active |\n | Alice |30 |true |\n | Bob |25 |false |\n | Carol |35 |true |"
867 );
868 }
869
870 #[test]
871 fn packs_explicit_nested_arrays_and_objects_kv1() {
872 let value = tjson_value(
873 "{\"nested\":[[1,2],[3,4]],\"rows\":[{\"a\":1,\"b\":2},{\"c\":3,\"d\":4}]}",
874 );
875 let rendered = render_string_with_options(&value, RenderOptions::default().kv_pack_multiple(1).unwrap());
876 assert_eq!(
877 rendered,
878 " nested:\n [ [ 1, 2\n [ 3, 4\n rows:\n [ { a:1 b:2\n { c:3 d:4"
879 );
880 }
881
882 #[test]
883 fn packs_explicit_nested_arrays_and_objects() {
884 let value = tjson_value(
885 "{\"nested\":[[1,2],[3,4]],\"rows\":[{\"a\":1,\"b\":2},{\"c\":3,\"d\":4}]}",
886 );
887 let rendered = render_string(&value);
888 assert_eq!(
889 rendered,
890 " nested:\n [ [ 1, 2\n [ 3, 4\n rows:\n [ { a:1 b:2\n { c:3 d:4"
891 );
892 }
893
894 #[test]
895 fn wraps_long_packed_arrays_before_falling_back_to_multiline() {
896 let value =
897 tjson_value("{\"data\":[100,200,300,400,500,600,700,800,900,1000,1100,1200,1300]}");
898 let rendered = render_string_with_options(
899 &value,
900 RenderOptions {
901 wrap_width: Some(40),
902 ..RenderOptions::default()
903 },
904 );
905 assert_eq!(
906 rendered,
907 " data: 100, 200, 300, 400, 500, 600,\n 700, 800, 900, 1000, 1100, 1200,\n 1300"
908 );
909 }
910
911 #[test]
912 fn default_string_array_style_is_prefer_comma() {
913 let value = tjson_value("{\"items\":[\"alpha\",\"beta\",\"gamma\"]}");
914 let rendered = render_string(&value);
915 assert_eq!(rendered, " items: alpha, beta, gamma");
916 }
917
918 #[test]
919 fn bare_strings_none_quotes_single_line_strings() {
920 let value = tjson_value("{\"greeting\":\"hello world\",\"items\":[\"alpha\",\"beta\"]}");
921 let rendered = render_string_with_options(
922 &value,
923 RenderOptions {
924 bare_strings: BareStyle::None,
925 ..RenderOptions::default()
926 },
927 );
928 assert_eq!(
929 rendered,
930 " greeting:\"hello world\"\n items: \"alpha\", \"beta\""
931 );
932 let reparsed = to_json_value(parse_str(&rendered).unwrap());
933 assert_eq!(reparsed, to_json_value(value));
934 }
935
936 #[test]
937 fn bare_keys_none_quotes_keys_in_objects_and_tables_kv1() {
938 let object_value = tjson_value("{\"alpha\":1,\"beta key\":2}");
939 let rendered_object = render_string_with_options(
940 &object_value,
941 RenderOptions {
942 bare_keys: BareStyle::None,
943 kv_pack_multiple: 1,
944 ..RenderOptions::default()
945 },
946 );
947 assert_eq!(rendered_object, " \"alpha\":1 \"beta key\":2");
948 }
949
950 #[test]
951 fn bare_keys_none_quotes_keys_in_objects_and_tables() {
952 let object_value = tjson_value("{\"alpha\":1,\"beta key\":2}");
953 let rendered_object = render_string_with_options(
954 &object_value,
955 RenderOptions {
956 bare_keys: BareStyle::None,
957 ..RenderOptions::default()
958 },
959 );
960 assert_eq!(rendered_object, " \"alpha\":1 \"beta key\":2");
961
962 let table_value = tjson_value(
963 "{\"rows\":[{\"alpha\":1,\"beta\":2},{\"alpha\":3,\"beta\":4},{\"alpha\":5,\"beta\":6}]}",
964 );
965 let rendered_table = render_string_with_options(
966 &table_value,
967 RenderOptions {
968 bare_keys: BareStyle::None,
969 table_min_columns: 2,
970 ..RenderOptions::default()
971 },
972 );
973 assert_eq!(
974 rendered_table,
975 " \"rows\":\n |\"alpha\" |\"beta\" |\n |1 |2 |\n |3 |4 |\n |5 |6 |"
976 );
977 let reparsed = to_json_value(parse_str(&rendered_table).unwrap());
978 assert_eq!(reparsed, to_json_value(table_value));
979 }
980
981 #[test]
982 fn force_markers_applies_to_root_and_key_nested_single_levels_kv1() {
983 let value =
984 tjson_value("{\"a\":5,\"6\":\"fred\",\"xy\":[],\"de\":{},\"e\":[1],\"o\":{\"k\":2}}");
985 let rendered = render_string_with_options(
986 &value,
987 RenderOptions {
988 force_markers: true,
989 kv_pack_multiple: 1,
990 ..RenderOptions::default()
991 },
992 );
993 assert_eq!(
994 rendered,
995 "{ a:5 6: fred xy:[] de:{}\n e: 1\n o:\n { k:2"
996 );
997 let reparsed = to_json_value(parse_str(&rendered).unwrap());
998 assert_eq!(reparsed, to_json_value(value));
999 }
1000
1001 #[test]
1002 fn force_markers_applies_to_root_and_key_nested_single_levels() {
1003 let value =
1004 tjson_value("{\"a\":5,\"6\":\"fred\",\"xy\":[],\"de\":{},\"e\":[1],\"o\":{\"k\":2}}");
1005 let rendered = render_string_with_options(
1006 &value,
1007 RenderOptions {
1008 force_markers: true,
1009 ..RenderOptions::default()
1010 },
1011 );
1012 assert_eq!(
1013 rendered,
1014 "{ a:5 6: fred xy:[] de:{}\n e: 1\n o:\n { k:2"
1015 );
1016 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1017 assert_eq!(reparsed, to_json_value(value));
1018 }
1019
1020 #[test]
1021 fn force_markers_applies_to_root_arrays() {
1022 let value = tjson_value("[1,2,3]");
1023 let rendered = render_string_with_options(
1024 &value,
1025 RenderOptions {
1026 force_markers: true,
1027 ..RenderOptions::default()
1028 },
1029 );
1030 assert_eq!(rendered, "[ 1, 2, 3");
1031 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1032 assert_eq!(reparsed, to_json_value(value));
1033 }
1034
1035 #[test]
1036 fn force_markers_suppresses_table_rendering_for_array_containers() {
1037 let value = tjson_value("[{\"a\":1,\"b\":2},{\"a\":3,\"b\":4},{\"a\":5,\"b\":6}]");
1038 let rendered = render_string_with_options(
1039 &value,
1040 RenderOptions {
1041 force_markers: true,
1042 table_min_columns: 2,
1043 ..RenderOptions::default()
1044 },
1045 );
1046 assert_eq!(rendered, "[ |a |b |\n |1 |2 |\n |3 |4 |\n |5 |6 |");
1047 }
1048
1049 #[test]
1050 fn string_array_style_spaces_forces_space_packing() {
1051 let value = tjson_value("{\"items\":[\"alpha\",\"beta\",\"gamma\"]}");
1052 let rendered = render_string_with_options(
1053 &value,
1054 RenderOptions {
1055 string_array_style: StringArrayStyle::Spaces,
1056 ..RenderOptions::default()
1057 },
1058 );
1059 assert_eq!(rendered, " items: alpha beta gamma");
1060 }
1061
1062 #[test]
1063 fn string_array_style_none_disables_string_array_packing() {
1064 let value = tjson_value("{\"items\":[\"alpha\",\"beta\",\"gamma\"]}");
1065 let rendered = render_string_with_options(
1066 &value,
1067 RenderOptions {
1068 string_array_style: StringArrayStyle::None,
1069 ..RenderOptions::default()
1070 },
1071 );
1072 assert_eq!(rendered, " items:\n alpha\n beta\n gamma");
1073 }
1074
1075 #[test]
1076 fn prefer_comma_can_fall_back_to_spaces_when_wrap_is_cleaner() {
1077 let value = tjson_value("{\"items\":[\"aa\",\"bb\",\"cc\"]}");
1078 let comma = render_string_with_options(
1079 &value,
1080 RenderOptions {
1081 string_array_style: StringArrayStyle::Comma,
1082 wrap_width: Some(18),
1083 ..RenderOptions::default()
1084 },
1085 );
1086 let prefer_comma = render_string_with_options(
1087 &value,
1088 RenderOptions {
1089 string_array_style: StringArrayStyle::PreferComma,
1090 wrap_width: Some(18),
1091 ..RenderOptions::default()
1092 },
1093 );
1094 assert_eq!(comma, " items: aa, bb,\n cc");
1095 assert_eq!(prefer_comma, " items: aa bb\n cc");
1096 }
1097
1098 #[test]
1099 fn quotes_comma_strings_in_packed_arrays_so_they_round_trip() {
1100 let value = tjson_value("{\"items\":[\"apples, oranges\",\"pears, plums\",\"grapes\"]}");
1101 let rendered = render_string(&value);
1102 assert_eq!(
1103 rendered,
1104 " items: \"apples, oranges\", \"pears, plums\", grapes"
1105 );
1106 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1107 assert_eq!(reparsed, to_json_value(value));
1108 }
1109
1110 #[test]
1111 fn spaces_style_quotes_comma_strings_and_round_trips() {
1112 let value = tjson_value("{\"items\":[\"apples, oranges\",\"pears, plums\"]}");
1113 let rendered = render_string_with_options(
1114 &value,
1115 RenderOptions {
1116 string_array_style: StringArrayStyle::Spaces,
1117 ..RenderOptions::default()
1118 },
1119 );
1120 assert_eq!(rendered, " items: \"apples, oranges\" \"pears, plums\"");
1121 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1122 assert_eq!(reparsed, to_json_value(value));
1123 }
1124
1125 #[test]
1126 fn canonical_rendering_disables_tables_and_inline_packing() {
1127 let value = tjson_value(
1128 "[{\"a\":1,\"b\":\"x\",\"c\":true},{\"a\":2,\"b\":\"y\",\"c\":false},{\"a\":3,\"b\":\"z\",\"c\":null}]",
1129 );
1130 let rendered = render_string_with_options(&value, RenderOptions::canonical());
1131 assert!(!rendered.contains('|'));
1132 assert!(!rendered.contains(", "));
1133 }
1134
1135 #[test]
1141 fn bare_fold_none_does_not_fold() {
1142 let value = Value::from(json(r#"{"k":"aaaaa bbbbb"}"#));
1144 let rendered = render_string_with_options(
1145 &value,
1146 RenderOptions::default()
1147 .wrap_width(Some(15))
1148 .string_bare_fold_style(FoldStyle::None),
1149 );
1150 assert!(!rendered.contains("/ "), "None fold style must not fold: {rendered}");
1151 }
1152
1153 #[test]
1154 fn bare_fold_fixed_folds_at_wrap_width() {
1155 let value = Value::from(json(r#"{"k":"aaaaabbbbbcccccdddd"}"#));
1160 let rendered = render_string_with_options(
1161 &value,
1162 RenderOptions::default()
1163 .wrap_width(Some(20))
1164 .string_bare_fold_style(FoldStyle::Fixed),
1165 );
1166 assert!(rendered.contains("/ "), "Fixed must fold: {rendered}");
1167 assert!(!rendered.contains("/ ") || rendered.lines().count() == 2, "exactly one fold: {rendered}");
1168 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1169 assert_eq!(reparsed, json(r#"{"k":"aaaaabbbbbcccccdddd"}"#));
1170 }
1171
1172 #[test]
1173 fn bare_fold_auto_folds_at_single_space() {
1174 let value = Value::from(json(r#"{"k":"aaaaa bbbbbccccc"}"#));
1178 let rendered = render_string_with_options(
1179 &value,
1180 RenderOptions::default()
1181 .wrap_width(Some(20))
1182 .string_bare_fold_style(FoldStyle::Auto),
1183 );
1184 assert_eq!(rendered, " k: aaaaa\n / bbbbbccccc");
1185 }
1186
1187 #[test]
1188 fn bare_fold_auto_folds_at_word_boundary_slash() {
1189 let value = Value::from(json(r#"{"k":"aaaaa/bbbbbccccc"}"#));
1193 let rendered = render_string_with_options(
1194 &value,
1195 RenderOptions::default()
1196 .wrap_width(Some(20))
1197 .string_bare_fold_style(FoldStyle::Auto),
1198 );
1199 assert!(rendered.contains("/ "), "expected fold: {rendered}");
1200 assert!(rendered.contains("aaaaa/\n"), "slash must trail the line: {rendered}");
1201 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1202 assert_eq!(reparsed, json(r#"{"k":"aaaaa/bbbbbccccc"}"#));
1203 }
1204
1205 #[test]
1206 fn bare_fold_auto_prefers_space_over_word_boundary() {
1207 let value = Value::from(json(r#"{"k":"aa/bbbbbbbbb cccc"}"#));
1211 let rendered = render_string_with_options(
1212 &value,
1213 RenderOptions::default()
1214 .wrap_width(Some(20))
1215 .string_bare_fold_style(FoldStyle::Auto),
1216 );
1217 assert!(rendered.contains("/ "), "expected fold: {rendered}");
1218 assert!(rendered.contains("aa/bbbbbbbbb\n"), "must fold at space not slash: {rendered}");
1220 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1221 assert_eq!(reparsed, json(r#"{"k":"aa/bbbbbbbbb cccc"}"#));
1222 }
1223
1224 #[test]
1225 fn quoted_fold_auto_folds_at_word_boundary_slash() {
1226 let value = Value::from(json(r#"{"k":"aaaaa/bbbbbcccccc"}"#));
1230 let rendered = render_string_with_options(
1231 &value,
1232 RenderOptions::default()
1233 .wrap_width(Some(20))
1234 .bare_strings(BareStyle::None)
1235 .string_quoted_fold_style(FoldStyle::Auto),
1236 );
1237 assert!(rendered.contains("/ "), "expected fold: {rendered}");
1238 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1239 assert_eq!(reparsed, json(r#"{"k":"aaaaa/bbbbbcccccc"}"#));
1240 }
1241
1242 #[test]
1243 fn quoted_fold_none_does_not_fold() {
1244 let value = Value::from(json(r#"{"kk":"aaaaabbbbbcccccdddd"}"#));
1247 let rendered = render_string_with_options(
1248 &value,
1249 RenderOptions::default()
1250 .wrap_width(Some(20))
1251 .bare_strings(BareStyle::None)
1252 .bare_keys(BareStyle::None)
1253 .string_quoted_fold_style(FoldStyle::None),
1254 );
1255 assert!(rendered.contains('"'), "must be quoted");
1256 assert!(!rendered.contains("/ "), "None fold style must not fold: {rendered}");
1257 }
1258
1259 #[test]
1260 fn quoted_fold_fixed_folds_and_roundtrips() {
1261 let value = Value::from(json(r#"{"k":"aaaaabbbbbcccccdd"}"#));
1264 let rendered = render_string_with_options(
1265 &value,
1266 RenderOptions::default()
1267 .wrap_width(Some(20))
1268 .bare_strings(BareStyle::None)
1269 .string_quoted_fold_style(FoldStyle::Fixed),
1270 );
1271 assert!(rendered.contains("/ "), "Fixed must fold: {rendered}");
1272 assert!(!rendered.contains('`'), "must be a JSON string fold, not multiline");
1273 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1274 assert_eq!(reparsed, json(r#"{"k":"aaaaabbbbbcccccdd"}"#));
1275 }
1276
1277 #[test]
1278 fn quoted_fold_auto_folds_at_single_space() {
1279 let value = Value::from(json(r#"{"k":"aaaaa bbbbbccccc"}"#));
1283 let rendered = render_string_with_options(
1284 &value,
1285 RenderOptions::default()
1286 .wrap_width(Some(20))
1287 .bare_strings(BareStyle::None)
1288 .string_quoted_fold_style(FoldStyle::Auto),
1289 );
1290 assert!(rendered.contains("/ "), "Auto must fold: {rendered}");
1291 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1292 assert_eq!(reparsed, json(r#"{"k":"aaaaa bbbbbccccc"}"#));
1293 }
1294
1295 #[test]
1296 fn multiline_fold_none_does_not_fold_body_lines() {
1297 let value = Value::String("aaaaabbbbbcccccdddddeeeeefff\nsecond".to_owned());
1299 let rendered = render_string_with_options(
1300 &value,
1301 RenderOptions::default()
1302 .wrap_width(Some(20))
1303 .string_multiline_fold_style(FoldStyle::None),
1304 );
1305 assert!(rendered.contains('`'), "must be multiline");
1306 assert!(rendered.contains("aaaaabbbbbcccccddddd"), "body must not be folded: {rendered}");
1307 }
1308
1309 #[test]
1310 fn fold_style_none_on_all_types_produces_no_fold_continuations() {
1311 let value = Value::from(json(r#"{"a":"aaaaa bbbbbccccc","b":"x,y,z abcdefghij"}"#));
1313 let rendered = render_string_with_options(
1314 &value,
1315 RenderOptions::default()
1316 .wrap_width(Some(20))
1317 .string_bare_fold_style(FoldStyle::None)
1318 .string_quoted_fold_style(FoldStyle::None)
1319 .string_multiline_fold_style(FoldStyle::None),
1320 );
1321 assert!(!rendered.contains("/ "), "no fold continuations expected: {rendered}");
1322 }
1323
1324 #[test]
1325 fn number_fold_none_does_not_fold() {
1326 let value = Value::Number("123456789012345678901234".parse().unwrap());
1328 let rendered = value.to_tjson_with(
1329 RenderOptions::default()
1330 .wrap_width(Some(20))
1331 .number_fold_style(FoldStyle::None),
1332 );
1333 assert!(!rendered.contains("/ "), "expected no fold: {rendered}");
1334 assert!(rendered.contains("123456789012345678901234"), "must contain full number: {rendered}");
1335 }
1336
1337 #[test]
1338 fn number_fold_fixed_splits_between_digits() {
1339 let value = Value::Number("123456789012345678901234".parse().unwrap());
1341 let rendered = value.to_tjson_with(
1342 RenderOptions::default()
1343 .wrap_width(Some(20))
1344 .number_fold_style(FoldStyle::Fixed),
1345 );
1346 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
1347 let reparsed = rendered.parse::<Value>().unwrap();
1348 assert_eq!(reparsed, Value::Number("123456789012345678901234".parse().unwrap()),
1349 "roundtrip must recover original number");
1350 }
1351
1352 #[test]
1353 fn number_fold_auto_prefers_decimal_point() {
1354 let value = Value::Number("1234567890123456789.01".parse().unwrap());
1358 let rendered = value.to_tjson_with(
1359 RenderOptions::default()
1360 .wrap_width(Some(20))
1361 .number_fold_style(FoldStyle::Auto),
1362 );
1363 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
1364 let first_line = rendered.lines().next().unwrap();
1365 assert!(first_line.ends_with("1234567890123456789"), "should fold before `.`: {rendered}");
1366 let reparsed = rendered.parse::<Value>().unwrap();
1367 assert_eq!(reparsed, Value::Number("1234567890123456789.01".parse().unwrap()),
1368 "roundtrip must recover original number");
1369 }
1370
1371 #[test]
1372 fn number_fold_auto_prefers_exponent() {
1373 let value = Value::Number("1.23456789012345678e+97".parse().unwrap());
1377 let rendered = value.to_tjson_with(
1378 RenderOptions::default()
1379 .wrap_width(Some(20))
1380 .number_fold_style(FoldStyle::Auto),
1381 );
1382 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
1383 let first_line = rendered.lines().next().unwrap();
1384 assert!(first_line.ends_with("1.23456789012345678"), "should fold before `e`: {rendered}");
1385 let reparsed = rendered.parse::<Value>().unwrap();
1386 assert_eq!(reparsed, Value::Number("1.23456789012345678e+97".parse().unwrap()),
1387 "roundtrip must recover original number");
1388 }
1389
1390 #[test]
1391 fn number_fold_auto_folds_before_decimal_point() {
1392 let value = Value::Number("1234567890123456789.01".parse().unwrap());
1396 let rendered = value.to_tjson_with(
1397 RenderOptions::default()
1398 .wrap_width(Some(20))
1399 .number_fold_style(FoldStyle::Auto),
1400 );
1401 assert!(rendered.contains("/ "), "expected fold: {rendered}");
1402 let first_line = rendered.lines().next().unwrap();
1403 assert!(first_line.ends_with("1234567890123456789"),
1404 "should fold before '.': {rendered}");
1405 let cont_line = rendered.lines().nth(1).unwrap();
1406 assert!(cont_line.starts_with("/ ."),
1407 "continuation must start with '/ .': {rendered}");
1408 let reparsed = rendered.parse::<Value>().unwrap();
1409 assert_eq!(reparsed, Value::Number("1234567890123456789.01".parse().unwrap()),
1410 "roundtrip must recover original number");
1411 }
1412
1413 #[test]
1414 fn number_fold_auto_folds_before_exponent() {
1415 let value = Value::Number("1.23456789012345678e+97".parse().unwrap());
1419 let rendered = value.to_tjson_with(
1420 RenderOptions::default()
1421 .wrap_width(Some(20))
1422 .number_fold_style(FoldStyle::Auto),
1423 );
1424 assert!(rendered.contains("/ "), "expected fold: {rendered}");
1425 let first_line = rendered.lines().next().unwrap();
1426 assert!(first_line.ends_with("1.23456789012345678"),
1427 "should fold before 'e': {rendered}");
1428 let cont_line = rendered.lines().nth(1).unwrap();
1429 assert!(cont_line.starts_with("/ e"),
1430 "continuation must start with '/ e': {rendered}");
1431 let reparsed = rendered.parse::<Value>().unwrap();
1432 assert_eq!(reparsed, Value::Number("1.23456789012345678e+97".parse().unwrap()),
1433 "roundtrip must recover original number");
1434 }
1435
1436 #[test]
1437 fn number_fold_fixed_splits_at_wrap_boundary() {
1438 let value = Value::Number("123456789012345678901".parse().unwrap());
1441 let rendered = value.to_tjson_with(
1442 RenderOptions::default()
1443 .wrap_width(Some(20))
1444 .number_fold_style(FoldStyle::Fixed),
1445 );
1446 assert!(rendered.contains("/ "), "expected fold: {rendered}");
1447 let first_line = rendered.lines().next().unwrap();
1448 assert_eq!(first_line, "12345678901234567890",
1449 "fixed fold must split exactly at wrap=20: {rendered}");
1450 let reparsed = rendered.parse::<Value>().unwrap();
1451 assert_eq!(reparsed, Value::Number("123456789012345678901".parse().unwrap()),
1452 "roundtrip must recover original number");
1453 }
1454
1455 #[test]
1456 fn number_fold_auto_falls_back_to_digit_split() {
1457 let value = Value::Number("123456789012345678901234".parse().unwrap());
1460 let rendered = value.to_tjson_with(
1461 RenderOptions::default()
1462 .wrap_width(Some(20))
1463 .number_fold_style(FoldStyle::Auto),
1464 );
1465 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
1466 let first_line = rendered.lines().next().unwrap();
1467 assert_eq!(first_line, "12345678901234567890",
1468 "auto fallback must split at digit boundary at wrap=20: {rendered}");
1469 let reparsed = rendered.parse::<Value>().unwrap();
1470 assert_eq!(reparsed, Value::Number("123456789012345678901234".parse().unwrap()),
1471 "roundtrip must recover original number");
1472 }
1473
1474 #[test]
1475 fn bare_key_fold_fixed_folds_and_roundtrips() {
1476 let value = Value::from(json(r#"{"abcdefghijklmnopqrst":1}"#));
1479 let rendered = value.to_tjson_with(
1480 RenderOptions::default()
1481 .wrap_width(Some(15))
1482 .string_bare_fold_style(FoldStyle::Fixed),
1483 );
1484 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
1485 let reparsed = to_json_value(rendered.parse::<Value>().unwrap());
1486 assert_eq!(reparsed, json(r#"{"abcdefghijklmnopqrst":1}"#),
1487 "roundtrip must recover original key");
1488 }
1489
1490 #[test]
1491 fn bare_key_fold_none_does_not_fold() {
1492 let value = Value::from(json(r#"{"abcdefghijklmnopqrst":1}"#));
1494 let rendered = value.to_tjson_with(
1495 RenderOptions::default()
1496 .wrap_width(Some(15))
1497 .string_bare_fold_style(FoldStyle::None),
1498 );
1499 assert!(!rendered.contains("/ "), "expected no fold: {rendered}");
1500 }
1501
1502 #[test]
1503 fn quoted_key_fold_fixed_folds_and_roundtrips() {
1504 let value = Value::from(json(r#"{"abcdefghijklmnop":1}"#));
1508 let rendered = value.to_tjson_with(
1509 RenderOptions::default()
1510 .wrap_width(Some(15))
1511 .bare_keys(BareStyle::None)
1512 .string_quoted_fold_style(FoldStyle::Fixed),
1513 );
1514 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
1515 let reparsed = to_json_value(rendered.parse::<Value>().unwrap());
1516 assert_eq!(reparsed, json(r#"{"abcdefghijklmnop":1}"#),
1517 "roundtrip must recover original key");
1518 }
1519
1520 #[test]
1521 fn round_trips_generated_examples() {
1522 let values = [
1523 json("{\"a\":5,\"6\":\"fred\",\"xy\":[],\"de\":{},\"e\":[1]}"),
1524 json("{\"nested\":[[1],[2,3],{\"x\":\"y\"}],\"empty\":[],\"text\":\"plain english\"}"),
1525 json("{\"note\":\"first\\nsecond\\n indented\"}"),
1526 json(
1527 "[{\"a\":1,\"b\":\"x\",\"c\":true},{\"a\":2,\"b\":\"y\",\"c\":false},{\"a\":3,\"b\":\"z\",\"c\":null}]",
1528 ),
1529 ];
1530 for value in values {
1531 let rendered = render_string(&Value::from(value.clone()));
1532 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1533 assert_eq!(reparsed, value);
1534 }
1535 }
1536
1537 #[test]
1538 fn keeps_key_order_at_the_ast_and_json_boundary() {
1539 let input = " first:1\n second:2\n third:3";
1540 let value = parse_str(input).unwrap();
1541 match &value {
1542 Value::Object(entries) => {
1543 let keys = entries
1544 .iter()
1545 .map(|e| e.key.as_str())
1546 .collect::<Vec<_>>();
1547 assert_eq!(keys, vec!["first", "second", "third"]);
1548 }
1549 other => panic!("expected an object, found {other:?}"),
1550 }
1551 let json: serde_json::Value = serde_json::from_str(&value.to_json()).unwrap();
1552 let keys = json
1553 .as_object()
1554 .unwrap()
1555 .keys()
1556 .map(String::as_str)
1557 .collect::<Vec<_>>();
1558 assert_eq!(keys, vec!["first", "second", "third"]);
1559 }
1560
1561 #[test]
1562 fn duplicate_keys_are_localized_to_the_json_boundary() {
1563 let input = " dup:1\n dup:2\n keep:3";
1564 let value = parse_str(input).unwrap();
1565 match &value {
1566 Value::Object(entries) => assert_eq!(entries.len(), 3),
1567 other => panic!("expected an object, found {other:?}"),
1568 }
1569 let json_value: serde_json::Value = serde_json::from_str(&value.to_json()).unwrap();
1570 assert_eq!(json_value, json("{\"dup\":2,\"keep\":3}"));
1571 }
1572
1573 #[test]
1576 fn parses_indent_offset_table() {
1577 let input = concat!(
1579 " outer:\n",
1580 " h: /<\n",
1581 " |name |score |\n",
1582 " | Alice |100 |\n",
1583 " | Bob |200 |\n",
1584 " | Carol |300 |\n",
1585 " />\n",
1586 " sib: value\n",
1587 );
1588 let value = to_json_value(parse_str(input).unwrap());
1589 let expected = serde_json::json!({
1590 "outer": {
1591 "h": [
1592 {"name": "Alice", "score": 100},
1593 {"name": "Bob", "score": 200},
1594 {"name": "Carol", "score": 300},
1595 ],
1596 "sib": "value"
1597 }
1598 });
1599 assert_eq!(value, expected);
1600 }
1601
1602 #[test]
1603 fn parses_indent_offset_deep_nesting() {
1604 let input = concat!(
1606 " a:\n",
1607 " b: /<\n",
1608 " c: /<\n",
1609 " d:99\n",
1610 " />\n",
1611 " e:42\n",
1612 " />\n",
1613 " f:1\n",
1614 );
1615 let value = to_json_value(parse_str(input).unwrap());
1616 let expected = serde_json::json!({
1619 "a": {"b": {"c": {"d": 99}, "e": 42}},
1620 "f": 1
1621 });
1622 assert_eq!(value, expected);
1623 }
1624
1625 #[test]
1626 fn renderer_uses_indent_offset_for_deep_tables_that_overflow() {
1627 let deep_table_json = r#"{
1630 "a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":[
1631 {"c1":"really long value 1","c2":"somewhat long val 1","c3":"another long val 12"},
1632 {"c1":"row two c1 value","c2":"row two c2 value","c3":"row two c3 value"},
1633 {"c1":"row three c1 val","c2":"row three c2 val","c3":"row three c3 val"}
1634 ]}}}}}}}}
1635 "#;
1636 let value = Value::from(serde_json::from_str::<JsonValue>(deep_table_json).unwrap());
1637 let rendered = render_string_with_options(
1638 &value,
1639 RenderOptions {
1640 wrap_width: Some(80),
1641 ..RenderOptions::default()
1642 },
1643 );
1644 assert!(
1645 rendered.contains(" /<"),
1646 "expected /< in rendered output:\n{rendered}"
1647 );
1648 assert!(
1649 rendered.contains("/>"),
1650 "expected /> in rendered output:\n{rendered}"
1651 );
1652 let reparsed = to_json_value(parse_str(&rendered).unwrap());
1654 assert_eq!(reparsed, to_json_value(value));
1655 }
1656
1657 #[test]
1658 fn renderer_does_not_use_indent_offset_with_unlimited_wrap() {
1659 let deep_table_json = r#"{
1660 "a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":[
1661 {"c1":"really long value 1","c2":"somewhat long val 1","c3":"another long val 12"},
1662 {"c1":"row two c1 value","c2":"row two c2 value","c3":"row two c3 value"},
1663 {"c1":"row three c1 val","c2":"row three c2 val","c3":"row three c3 val"}
1664 ]}}}}}}}}
1665 "#;
1666 let value = Value::from(serde_json::from_str::<JsonValue>(deep_table_json).unwrap());
1667 let rendered = render_string_with_options(
1668 &value,
1669 RenderOptions {
1670 wrap_width: None, ..RenderOptions::default()
1672 },
1673 );
1674 assert!(
1675 !rendered.contains(" /<"),
1676 "expected no /< with unlimited wrap:\n{rendered}"
1677 );
1678 }
1679
1680 fn deep3_table_value() -> Value {
1685 Value::from(serde_json::from_str::<JsonValue>(r#"{
1686 "a":{"b":{"c":[
1687 {"col1":"value one here","col2":"value two here","col3":"value three here"},
1688 {"col1":"row two col1","col2":"row two col2","col3":"row two col3"},
1689 {"col1":"row three c1","col2":"row three c2","col3":"row three c3"}
1690 ]}}}"#).unwrap())
1691 }
1692
1693 #[test]
1694 fn table_unindent_style_none_never_uses_glyphs() {
1695 let rendered = render_string_with_options(
1697 &deep3_table_value(),
1698 RenderOptions::default()
1699 .wrap_width(Some(50))
1700 .table_unindent_style(TableUnindentStyle::None),
1701 );
1702 assert!(!rendered.contains("/<"), "None must not use indent glyphs: {rendered}");
1703 }
1704
1705 #[test]
1706 fn table_unindent_style_left_always_uses_glyphs_when_fits_at_zero() {
1707 let rendered = render_string_with_options(
1710 &deep3_table_value(),
1711 RenderOptions::default()
1712 .wrap_width(None)
1713 .table_unindent_style(TableUnindentStyle::Left),
1714 );
1715 assert!(rendered.contains("/<"), "Left must always use indent glyphs: {rendered}");
1716 let reparsed = to_json_value(rendered.parse::<Value>().unwrap());
1717 assert_eq!(reparsed, to_json_value(deep3_table_value()));
1718 }
1719
1720 #[test]
1721 fn table_unindent_style_auto_uses_glyphs_only_on_overflow() {
1722 let value = deep3_table_value();
1723 let wide = render_string_with_options(
1725 &value,
1726 RenderOptions::default()
1727 .wrap_width(None)
1728 .table_unindent_style(TableUnindentStyle::Auto),
1729 );
1730 assert!(!wide.contains("/<"), "Auto must not use glyphs when table fits: {wide}");
1731
1732 let narrow = render_string_with_options(
1734 &value,
1735 RenderOptions::default()
1736 .wrap_width(Some(60))
1737 .table_unindent_style(TableUnindentStyle::Auto),
1738 );
1739 assert!(narrow.contains("/<"), "Auto must use glyphs on overflow: {narrow}");
1740 let reparsed = to_json_value(narrow.parse::<Value>().unwrap());
1741 assert_eq!(reparsed, to_json_value(value));
1742 }
1743
1744 #[test]
1745 fn table_unindent_style_floating_pushes_minimum_needed() {
1746 let value = deep3_table_value();
1752 let rendered = render_string_with_options(
1753 &value,
1754 RenderOptions::default()
1755 .wrap_width(Some(65))
1756 .table_unindent_style(TableUnindentStyle::Floating),
1757 );
1758 if rendered.contains("/<") {
1762 let row_line = rendered.lines().find(|l| l.contains('|') && !l.contains("/<") && !l.contains("/>")).unwrap_or("");
1763 let row_indent = row_line.len() - row_line.trim_start().len();
1764 assert!(row_indent > 2, "Floating must not push all the way to indent 0: {rendered}");
1765 }
1766 let reparsed = to_json_value(rendered.parse::<Value>().unwrap());
1767 assert_eq!(reparsed, to_json_value(value));
1768 }
1769
1770 #[test]
1771 fn table_unindent_style_none_with_indent_glyph_none_also_no_glyphs() {
1772 let rendered = render_string_with_options(
1774 &deep3_table_value(),
1775 RenderOptions::default()
1776 .wrap_width(Some(50))
1777 .table_unindent_style(TableUnindentStyle::None)
1778 .indent_glyph_style(IndentGlyphStyle::None),
1779 );
1780 assert!(!rendered.contains("/<"), "must not use indent glyphs: {rendered}");
1781 }
1782
1783 #[test]
1784 fn table_unindent_style_left_independent_of_indent_glyph_none() {
1785 let rendered = render_string_with_options(
1787 &deep3_table_value(),
1788 RenderOptions::default()
1789 .wrap_width(None)
1790 .table_unindent_style(TableUnindentStyle::Left)
1791 .indent_glyph_style(IndentGlyphStyle::None),
1792 );
1793 assert!(rendered.contains("/<"), "table_unindent_style=Left must still fire with indent_glyph_style=None: {rendered}");
1794 }
1795
1796 #[test]
1797 fn renderer_does_not_use_indent_offset_when_indent_is_small() {
1798 let json_str = r#"{"h":[
1800 {"c1":"really long value 1","c2":"somewhat long val 1","c3":"another long val 12"},
1801 {"c1":"row two c1 value","c2":"row two c2 value","c3":"row two c3 value"},
1802 {"c1":"row three c1 val","c2":"row three c2 val","c3":"row three c3 val"}
1803 ]}"#;
1804 let value = Value::from(serde_json::from_str::<JsonValue>(json_str).unwrap());
1805 let rendered = render_string_with_options(
1806 &value,
1807 RenderOptions {
1808 wrap_width: Some(80),
1809 ..RenderOptions::default()
1810 },
1811 );
1812 assert!(
1813 !rendered.contains(" /<"),
1814 "expected no /< when indent is small:\n{rendered}"
1815 );
1816 }
1817
1818 #[test]
1819 fn tjson_config_camel_case_enums() {
1820 let c: TjsonConfig = serde_json::from_str(r#"{"stringArrayStyle":"preferSpaces","multilineStyle":"boldFloating"}"#).unwrap();
1822 assert_eq!(c.string_array_style, Some(StringArrayStyle::PreferSpaces));
1823 assert_eq!(c.multiline_style, Some(MultilineStyle::BoldFloating));
1824
1825 let c: TjsonConfig = serde_json::from_str(r#"{"stringArrayStyle":"PreferComma","multilineStyle":"FoldingQuotes"}"#).unwrap();
1827 assert_eq!(c.string_array_style, Some(StringArrayStyle::PreferComma));
1828 assert_eq!(c.multiline_style, Some(MultilineStyle::FoldingQuotes));
1829
1830 let c: TjsonConfig = serde_json::from_str(r#"{
1832 "bareStrings": "prefer",
1833 "numberFoldStyle": "auto",
1834 "indentGlyphStyle": "fixed",
1835 "tableUnindentStyle": "floating",
1836 "indentGlyphMarkerStyle": "compact"
1837 }"#).unwrap();
1838 assert_eq!(c.bare_strings, Some(BareStyle::Prefer));
1839 assert_eq!(c.number_fold_style, Some(FoldStyle::Auto));
1840 assert_eq!(c.indent_glyph_style, Some(IndentGlyphStyle::Fixed));
1841 assert_eq!(c.table_unindent_style, Some(TableUnindentStyle::Floating));
1842 assert_eq!(c.indent_glyph_marker_style, Some(IndentGlyphMarkerStyle::Compact));
1843 }
1844}