1#[cfg(target_arch = "wasm32")]
2mod wasm;
3
4use std::error::Error as StdError;
5use std::fmt;
6use std::str::FromStr;
7
8use serde::{Deserialize, Serialize};
9use serde::de::DeserializeOwned;
10use serde_json::{Map as JsonMap, Number as JsonNumber, Value as JsonValue};
11use unicode_general_category::{GeneralCategory, get_general_category};
12
13pub const MIN_WRAP_WIDTH: usize = 20;
16pub const DEFAULT_WRAP_WIDTH: usize = 80;
18const MIN_FOLD_CONTINUATION: usize = 10;
19
20#[non_exhaustive]
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
28pub enum IndentGlyphStyle {
29 #[default]
32 Auto,
33 Fixed,
35 None,
37}
38
39impl FromStr for IndentGlyphStyle {
40 type Err = String;
41 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
42 match input {
43 "auto" => Ok(Self::Auto),
44 "fixed" => Ok(Self::Fixed),
45 "none" => Ok(Self::None),
46 _ => Err(format!(
47 "invalid indent glyph style '{input}' (expected one of: auto, fixed, none)"
48 )),
49 }
50 }
51}
52
53#[non_exhaustive]
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
56pub enum IndentGlyphMarkerStyle {
57 #[default]
59 Compact,
60 Separate,
66 }
71
72#[derive(Clone, Copy, Debug, PartialEq)]
75#[allow(dead_code)]
76enum IndentGlyphMode {
77 IndentWeighted(f64),
79 ByteWeighted(f64),
83 Fixed,
85 None,
87}
88
89fn indent_glyph_mode(options: &TjsonOptions) -> IndentGlyphMode {
90 match options.indent_glyph_style {
91 IndentGlyphStyle::Auto => IndentGlyphMode::IndentWeighted(0.2),
92 IndentGlyphStyle::Fixed => IndentGlyphMode::Fixed,
93 IndentGlyphStyle::None => IndentGlyphMode::None,
94 }
95}
96
97#[non_exhaustive]
102#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
103pub enum TableUnindentStyle {
104 Left,
107 #[default]
111 Auto,
112 Floating,
116 None,
119}
120
121impl FromStr for TableUnindentStyle {
122 type Err = String;
123 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
124 match input {
125 "left" => Ok(Self::Left),
126 "auto" => Ok(Self::Auto),
127 "floating" => Ok(Self::Floating),
128 "none" => Ok(Self::None),
129 _ => Err(format!(
130 "invalid table unindent style '{input}' (expected one of: left, auto, floating, none)"
131 )),
132 }
133 }
134}
135
136
137#[non_exhaustive]
138#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(default)]
140struct ParseOptions {
141 start_indent: usize,
142}
143
144#[non_exhaustive]
148#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
149#[serde(default)]
150pub struct TjsonOptions {
151 wrap_width: Option<usize>,
152 start_indent: usize,
153 force_markers: bool,
154 bare_strings: BareStyle,
155 bare_keys: BareStyle,
156 inline_objects: bool,
157 inline_arrays: bool,
158 string_array_style: StringArrayStyle,
159 number_fold_style: FoldStyle,
160 string_bare_fold_style: FoldStyle,
161 string_quoted_fold_style: FoldStyle,
162 string_multiline_fold_style: FoldStyle,
163 tables: bool,
164 table_fold: bool,
165 table_unindent_style: TableUnindentStyle,
166 indent_glyph_style: IndentGlyphStyle,
167 indent_glyph_marker_style: IndentGlyphMarkerStyle,
168 table_min_rows: usize,
169 table_min_columns: usize,
170 table_min_similarity: f32,
171 table_column_max_width: Option<usize>,
172 kv_pack_multiple: usize,
174 multiline_strings: bool,
175 multiline_style: MultilineStyle,
176 multiline_min_lines: usize,
177 multiline_max_lines: usize,
178}
179
180#[non_exhaustive]
186#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
187pub enum FoldStyle {
188 #[default]
191 Auto,
192 Fixed,
195 None,
197}
198
199impl FromStr for FoldStyle {
200 type Err = String;
201
202 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
203 match input {
204 "auto" => Ok(Self::Auto),
205 "fixed" => Ok(Self::Fixed),
206 "none" => Ok(Self::None),
207 _ => Err(format!(
208 "invalid fold style '{input}' (expected one of: auto, fixed, none)"
209 )),
210 }
211 }
212}
213
214#[non_exhaustive]
234#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
235pub enum MultilineStyle {
236 Floating,
239 #[default]
241 Bold,
242 BoldFloating,
245 Transparent,
249 Light,
253 FoldingQuotes,
256}
257
258impl FromStr for MultilineStyle {
259 type Err = String;
260
261 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
262 match input {
263 "bold" => Ok(Self::Bold),
264 "floating" => Ok(Self::Floating),
265 "bold-floating" => Ok(Self::BoldFloating),
266 "transparent" => Ok(Self::Transparent),
267 "light" => Ok(Self::Light),
268 "folding-quotes" => Ok(Self::FoldingQuotes),
269 _ => Err(format!(
270 "invalid multiline style '{input}' (expected one of: bold, floating, bold-floating, transparent, light, folding-quotes)"
271 )),
272 }
273 }
274}
275
276#[non_exhaustive]
281#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
282pub enum BareStyle {
283 #[default]
284 Prefer,
285 None,
286}
287
288impl FromStr for BareStyle {
289 type Err = String;
290
291 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
292 match input {
293 "prefer" => Ok(Self::Prefer),
294 "none" => Ok(Self::None),
295 _ => Err(format!(
296 "invalid bare style '{input}' (expected one of: prefer, none)"
297 )),
298 }
299 }
300}
301
302#[non_exhaustive]
310#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
311pub enum StringArrayStyle {
312 Spaces,
313 PreferSpaces,
314 Comma,
315 #[default]
316 PreferComma,
317 None,
318}
319
320impl FromStr for StringArrayStyle {
321 type Err = String;
322
323 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
324 match input {
325 "spaces" => Ok(Self::Spaces),
326 "prefer-spaces" => Ok(Self::PreferSpaces),
327 "comma" => Ok(Self::Comma),
328 "prefer-comma" => Ok(Self::PreferComma),
329 "none" => Ok(Self::None),
330 _ => Err(format!(
331 "invalid string array style '{input}' (expected one of: spaces, prefer-spaces, comma, prefer-comma, none)"
332 )),
333 }
334 }
335}
336
337impl TjsonOptions {
338 pub fn canonical() -> Self {
341 Self {
342 inline_objects: false,
343 inline_arrays: false,
344 string_array_style: StringArrayStyle::None,
345 tables: false,
346 multiline_strings: false,
347 number_fold_style: FoldStyle::None,
348 string_bare_fold_style: FoldStyle::None,
349 string_quoted_fold_style: FoldStyle::None,
350 string_multiline_fold_style: FoldStyle::None,
351 indent_glyph_style: IndentGlyphStyle::None,
352 ..Self::default()
353 }
354 }
355
356 pub fn force_markers(mut self, force_markers: bool) -> Self {
361 self.force_markers = force_markers;
362 self
363 }
364
365 pub fn bare_strings(mut self, bare_strings: BareStyle) -> Self {
368 self.bare_strings = bare_strings;
369 self
370 }
371
372 pub fn bare_keys(mut self, bare_keys: BareStyle) -> Self {
375 self.bare_keys = bare_keys;
376 self
377 }
378
379 pub fn inline_objects(mut self, inline_objects: bool) -> Self {
381 self.inline_objects = inline_objects;
382 self
383 }
384
385 pub fn inline_arrays(mut self, inline_arrays: bool) -> Self {
387 self.inline_arrays = inline_arrays;
388 self
389 }
390
391 pub fn string_array_style(mut self, string_array_style: StringArrayStyle) -> Self {
394 self.string_array_style = string_array_style;
395 self
396 }
397
398 pub fn tables(mut self, tables: bool) -> Self {
401 self.tables = tables;
402 self
403 }
404
405 pub fn wrap_width(mut self, wrap_width: Option<usize>) -> Self {
409 self.wrap_width = wrap_width.map(|w| w.clamp(MIN_WRAP_WIDTH, usize::MAX));
410 self
411 }
412
413 pub fn wrap_width_checked(self, wrap_width: Option<usize>) -> std::result::Result<Self, String> {
417 if let Some(w) = wrap_width
418 && w < MIN_WRAP_WIDTH {
419 return Err(format!("wrap_width must be at least {MIN_WRAP_WIDTH}, got {w}"));
420 }
421 Ok(self.wrap_width(wrap_width))
422 }
423
424 pub fn table_min_rows(mut self, table_min_rows: usize) -> Self {
426 self.table_min_rows = table_min_rows;
427 self
428 }
429
430 pub fn table_min_columns(mut self, table_min_columns: usize) -> Self {
432 self.table_min_columns = table_min_columns;
433 self
434 }
435
436 pub fn table_min_similarity(mut self, v: f32) -> Self {
442 self.table_min_similarity = v;
443 self
444 }
445
446 pub fn table_column_max_width(mut self, table_column_max_width: Option<usize>) -> Self {
450 self.table_column_max_width = table_column_max_width;
451 self
452 }
453
454 pub fn kv_pack_multiple(mut self, v: usize) -> std::result::Result<Self, String> {
457 if !(1..=4).contains(&v) {
458 return Err(format!("kv_pack_multiple must be 1–4, got {v}"));
459 }
460 self.kv_pack_multiple = v;
461 Ok(self)
462 }
463
464 pub fn kv_pack_multiple_clamped(mut self, v: usize) -> Self {
467 self.kv_pack_multiple = v.clamp(1, 4);
468 self
469 }
470
471 pub fn fold(self, style: FoldStyle) -> Self {
473 self.number_fold_style(style)
474 .string_bare_fold_style(style)
475 .string_quoted_fold_style(style)
476 .string_multiline_fold_style(style)
477 }
478
479 pub fn number_fold_style(mut self, style: FoldStyle) -> Self {
482 self.number_fold_style = style;
483 self
484 }
485
486 pub fn string_bare_fold_style(mut self, style: FoldStyle) -> Self {
489 self.string_bare_fold_style = style;
490 self
491 }
492
493 pub fn string_quoted_fold_style(mut self, style: FoldStyle) -> Self {
496 self.string_quoted_fold_style = style;
497 self
498 }
499
500 pub fn string_multiline_fold_style(mut self, style: FoldStyle) -> Self {
505 self.string_multiline_fold_style = style;
506 self
507 }
508
509 pub fn table_fold(mut self, table_fold: bool) -> Self {
512 self.table_fold = table_fold;
513 self
514 }
515
516 pub fn table_unindent_style(mut self, style: TableUnindentStyle) -> Self {
519 self.table_unindent_style = style;
520 self
521 }
522
523 pub fn indent_glyph_style(mut self, style: IndentGlyphStyle) -> Self {
529 self.indent_glyph_style = style;
530 self
531 }
532
533 pub fn indent_glyph_marker_style(mut self, style: IndentGlyphMarkerStyle) -> Self {
536 self.indent_glyph_marker_style = style;
537 self
538 }
539
540 pub fn multiline_strings(mut self, multiline_strings: bool) -> Self {
543 self.multiline_strings = multiline_strings;
544 self
545 }
546
547 pub fn multiline_style(mut self, multiline_style: MultilineStyle) -> Self {
551 self.multiline_style = multiline_style;
552 self
553 }
554
555 pub fn multiline_min_lines(mut self, multiline_min_lines: usize) -> Self {
558 self.multiline_min_lines = multiline_min_lines;
559 self
560 }
561
562 pub fn multiline_max_lines(mut self, multiline_max_lines: usize) -> Self {
564 self.multiline_max_lines = multiline_max_lines;
565 self
566 }
567}
568
569impl Default for TjsonOptions {
570 fn default() -> Self {
571 Self {
572 start_indent: 0,
573 force_markers: false,
574 bare_strings: BareStyle::Prefer,
575 bare_keys: BareStyle::Prefer,
576 inline_objects: true,
577 inline_arrays: true,
578 string_array_style: StringArrayStyle::PreferComma,
579 tables: true,
580 wrap_width: Some(DEFAULT_WRAP_WIDTH),
581 table_min_rows: 3,
582 table_min_columns: 3,
583 table_min_similarity: 0.8,
584 table_column_max_width: Some(40),
585 kv_pack_multiple: 2,
586 number_fold_style: FoldStyle::Auto,
587 string_bare_fold_style: FoldStyle::Auto,
588 string_quoted_fold_style: FoldStyle::Auto,
589 string_multiline_fold_style: FoldStyle::None,
590 table_fold: false,
591 table_unindent_style: TableUnindentStyle::Auto,
592 indent_glyph_style: IndentGlyphStyle::Auto,
593 indent_glyph_marker_style: IndentGlyphMarkerStyle::Compact,
594 multiline_strings: true,
595 multiline_style: MultilineStyle::Bold,
596 multiline_min_lines: 1,
597 multiline_max_lines: 10,
598 }
599 }
600}
601
602mod camel_de {
605 use serde::{Deserialize, Deserializer};
606
607 fn de_str<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
608 Option::<String>::deserialize(d)
609 }
610
611 macro_rules! camel_option_de {
612 ($fn_name:ident, $Enum:ty, $($camel:literal => $variant:expr),+ $(,)?) => {
613 pub fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$Enum>, D::Error> {
614 let Some(s) = de_str(d)? else { return Ok(None); };
615 match s.as_str() {
616 $($camel => return Ok(Some($variant)),)+
617 _ => {}
618 }
619 serde_json::from_value(serde_json::Value::String(s.clone()))
621 .map(Some)
622 .map_err(|_| serde::de::Error::unknown_variant(&s, &[$($camel),+]))
623 }
624 };
625 }
626
627 camel_option_de!(bare_style, super::BareStyle,
628 "prefer" => super::BareStyle::Prefer,
629 "none" => super::BareStyle::None,
630 );
631
632 camel_option_de!(fold_style, super::FoldStyle,
633 "auto" => super::FoldStyle::Auto,
634 "fixed" => super::FoldStyle::Fixed,
635 "none" => super::FoldStyle::None,
636 );
637
638 camel_option_de!(multiline_style, super::MultilineStyle,
639 "floating" => super::MultilineStyle::Floating,
640 "bold" => super::MultilineStyle::Bold,
641 "boldFloating" => super::MultilineStyle::BoldFloating,
642 "transparent" => super::MultilineStyle::Transparent,
643 "light" => super::MultilineStyle::Light,
644 "foldingQuotes" => super::MultilineStyle::FoldingQuotes,
645 );
646
647 camel_option_de!(table_unindent_style, super::TableUnindentStyle,
648 "left" => super::TableUnindentStyle::Left,
649 "auto" => super::TableUnindentStyle::Auto,
650 "floating" => super::TableUnindentStyle::Floating,
651 "none" => super::TableUnindentStyle::None,
652 );
653
654 camel_option_de!(indent_glyph_style, super::IndentGlyphStyle,
655 "auto" => super::IndentGlyphStyle::Auto,
656 "fixed" => super::IndentGlyphStyle::Fixed,
657 "none" => super::IndentGlyphStyle::None,
658 );
659
660 camel_option_de!(indent_glyph_marker_style, super::IndentGlyphMarkerStyle,
661 "compact" => super::IndentGlyphMarkerStyle::Compact,
662 "separate" => super::IndentGlyphMarkerStyle::Separate,
663 );
664
665 camel_option_de!(string_array_style, super::StringArrayStyle,
666 "spaces" => super::StringArrayStyle::Spaces,
667 "preferSpaces" => super::StringArrayStyle::PreferSpaces,
668 "comma" => super::StringArrayStyle::Comma,
669 "preferComma" => super::StringArrayStyle::PreferComma,
670 "none" => super::StringArrayStyle::None,
671 );
672}
673
674#[doc(hidden)]
677#[derive(Clone, Debug, Default, Deserialize)]
678#[serde(rename_all = "camelCase", default)]
679pub struct TjsonConfig {
680 canonical: bool,
681 force_markers: Option<bool>,
682 wrap_width: Option<usize>,
683 #[serde(deserialize_with = "camel_de::bare_style")]
684 bare_strings: Option<BareStyle>,
685 #[serde(deserialize_with = "camel_de::bare_style")]
686 bare_keys: Option<BareStyle>,
687 inline_objects: Option<bool>,
688 inline_arrays: Option<bool>,
689 multiline_strings: Option<bool>,
690 #[serde(deserialize_with = "camel_de::multiline_style")]
691 multiline_style: Option<MultilineStyle>,
692 multiline_min_lines: Option<usize>,
693 multiline_max_lines: Option<usize>,
694 tables: Option<bool>,
695 table_fold: Option<bool>,
696 #[serde(deserialize_with = "camel_de::table_unindent_style")]
697 table_unindent_style: Option<TableUnindentStyle>,
698 table_min_rows: Option<usize>,
699 table_min_columns: Option<usize>,
700 table_min_similarity: Option<f32>,
701 table_column_max_width: Option<usize>,
702 #[serde(deserialize_with = "camel_de::string_array_style")]
703 string_array_style: Option<StringArrayStyle>,
704 #[serde(deserialize_with = "camel_de::fold_style")]
705 fold: Option<FoldStyle>,
706 #[serde(deserialize_with = "camel_de::fold_style")]
707 number_fold_style: Option<FoldStyle>,
708 #[serde(deserialize_with = "camel_de::fold_style")]
709 string_bare_fold_style: Option<FoldStyle>,
710 #[serde(deserialize_with = "camel_de::fold_style")]
711 string_quoted_fold_style: Option<FoldStyle>,
712 #[serde(deserialize_with = "camel_de::fold_style")]
713 string_multiline_fold_style: Option<FoldStyle>,
714 #[serde(deserialize_with = "camel_de::indent_glyph_style")]
715 indent_glyph_style: Option<IndentGlyphStyle>,
716 #[serde(deserialize_with = "camel_de::indent_glyph_marker_style")]
717 indent_glyph_marker_style: Option<IndentGlyphMarkerStyle>,
718 kv_pack_multiple: Option<usize>,
719}
720
721impl From<TjsonConfig> for TjsonOptions {
722 fn from(c: TjsonConfig) -> Self {
723 let mut opts = if c.canonical { TjsonOptions::canonical() } else { TjsonOptions::default() };
724 if let Some(v) = c.force_markers { opts = opts.force_markers(v); }
725 if let Some(w) = c.wrap_width { opts = opts.wrap_width(if w == 0 { None } else { Some(w) }); }
726 if let Some(v) = c.bare_strings { opts = opts.bare_strings(v); }
727 if let Some(v) = c.bare_keys { opts = opts.bare_keys(v); }
728 if let Some(v) = c.inline_objects { opts = opts.inline_objects(v); }
729 if let Some(v) = c.inline_arrays { opts = opts.inline_arrays(v); }
730 if let Some(v) = c.multiline_strings { opts = opts.multiline_strings(v); }
731 if let Some(v) = c.multiline_style { opts = opts.multiline_style(v); }
732 if let Some(v) = c.multiline_min_lines { opts = opts.multiline_min_lines(v); }
733 if let Some(v) = c.multiline_max_lines { opts = opts.multiline_max_lines(v); }
734 if let Some(v) = c.tables { opts = opts.tables(v); }
735 if let Some(v) = c.table_fold { opts = opts.table_fold(v); }
736 if let Some(v) = c.table_unindent_style { opts = opts.table_unindent_style(v); }
737 if let Some(v) = c.table_min_rows { opts = opts.table_min_rows(v); }
738 if let Some(v) = c.table_min_columns { opts = opts.table_min_columns(v); }
739 if let Some(v) = c.table_min_similarity { opts = opts.table_min_similarity(v); }
740 if let Some(v) = c.table_column_max_width { opts = opts.table_column_max_width(if v == 0 { None } else { Some(v) }); }
741 if let Some(v) = c.string_array_style { opts = opts.string_array_style(v); }
742 if let Some(v) = c.fold { opts = opts.fold(v); }
743 if let Some(v) = c.number_fold_style { opts = opts.number_fold_style(v); }
744 if let Some(v) = c.string_bare_fold_style { opts = opts.string_bare_fold_style(v); }
745 if let Some(v) = c.string_quoted_fold_style { opts = opts.string_quoted_fold_style(v); }
746 if let Some(v) = c.string_multiline_fold_style { opts = opts.string_multiline_fold_style(v); }
747 if let Some(v) = c.indent_glyph_style { opts = opts.indent_glyph_style(v); }
748 if let Some(v) = c.indent_glyph_marker_style { opts = opts.indent_glyph_marker_style(v); }
749 if let Some(v) = c.kv_pack_multiple { opts = opts.kv_pack_multiple_clamped(v); }
750 opts
751 }
752}
753
754#[derive(Clone, Debug, PartialEq, Eq)]
760pub enum TjsonValue {
761 Null,
763 Bool(bool),
765 Number(serde_json::Number),
767 String(String),
769 Array(Vec<TjsonValue>),
771 Object(Vec<(String, TjsonValue)>),
773}
774
775impl From<JsonValue> for TjsonValue {
776 fn from(value: JsonValue) -> Self {
777 match value {
778 JsonValue::Null => Self::Null,
779 JsonValue::Bool(value) => Self::Bool(value),
780 JsonValue::Number(value) => Self::Number(value),
781 JsonValue::String(value) => Self::String(value),
782 JsonValue::Array(values) => {
783 Self::Array(values.into_iter().map(Self::from).collect())
784 }
785 JsonValue::Object(map) => Self::Object(
786 map.into_iter()
787 .map(|(key, value)| (key, Self::from(value)))
788 .collect(),
789 ),
790 }
791 }
792}
793
794impl TjsonValue {
795
796 fn parse_with(input: &str, options: ParseOptions) -> Result<Self> {
797 Parser::parse_document(input, options.start_indent).map_err(Error::Parse)
798 }
799
800 pub fn to_tjson_with(&self, options: TjsonOptions) -> Result<String> {
811 Renderer::render(self, &options)
812 }
813
814 pub fn to_json(&self) -> Result<JsonValue, Error> {
825 Ok(match self {
826 Self::Null => JsonValue::Null,
827 Self::Bool(value) => JsonValue::Bool(*value),
828 Self::Number(value) => JsonValue::Number(value.clone()),
829 Self::String(value) => JsonValue::String(value.clone()),
830 Self::Array(values) => JsonValue::Array(
831 values
832 .iter()
833 .map(TjsonValue::to_json)
834 .collect::<Result<Vec<_>, _>>()?,
835 ),
836 Self::Object(entries) => {
837 let mut map = JsonMap::new();
838 for (key, value) in entries {
839 map.insert(key.clone(), value.to_json()?);
840 }
841 JsonValue::Object(map)
842 }
843 })
844 }
845}
846
847impl serde::Serialize for TjsonValue {
848 fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
849 use serde::ser::{SerializeMap, SerializeSeq};
850 match self {
851 Self::Null => serializer.serialize_unit(),
852 Self::Bool(b) => serializer.serialize_bool(*b),
853 Self::Number(n) => n.serialize(serializer),
854 Self::String(s) => serializer.serialize_str(s),
855 Self::Array(values) => {
856 let mut seq = serializer.serialize_seq(Some(values.len()))?;
857 for v in values {
858 seq.serialize_element(v)?;
859 }
860 seq.end()
861 }
862 Self::Object(entries) => {
863 let mut map = serializer.serialize_map(Some(entries.len()))?;
864 for (k, v) in entries {
865 map.serialize_entry(k, v)?;
866 }
867 map.end()
868 }
869 }
870 }
871}
872
873impl fmt::Display for TjsonValue {
874 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
875 let s = Renderer::render(self, &TjsonOptions::default()).map_err(|_| fmt::Error)?;
876 f.write_str(&s)
877 }
878}
879
880impl std::str::FromStr for TjsonValue {
885 type Err = Error;
886
887 fn from_str(s: &str) -> Result<Self> {
888 Self::parse_with(s, ParseOptions::default())
889 }
890}
891
892#[derive(Clone, Debug, PartialEq, Eq)]
897#[non_exhaustive]
898pub struct ParseError {
899 line: usize,
900 column: usize,
901 message: String,
902 source_line: Option<String>,
903}
904
905impl ParseError {
906 fn new(line: usize, column: usize, message: impl Into<String>, source_line: Option<String>) -> Self {
907 Self {
908 line,
909 column,
910 message: message.into(),
911 source_line,
912 }
913 }
914
915 pub fn line(&self) -> usize { self.line }
917 pub fn column(&self) -> usize { self.column }
919 pub fn message(&self) -> &str { &self.message }
921 pub fn source_line(&self) -> Option<&str> { self.source_line.as_deref() }
923}
924
925impl fmt::Display for ParseError {
926 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
927 write!(f, "line {}, column {}: {}", self.line, self.column, self.message)?;
928 if let Some(src) = &self.source_line {
929 write!(f, "\n {}\n {:>width$}", src, "^", width = self.column)?;
930 }
931 Ok(())
932 }
933}
934
935impl StdError for ParseError {}
936
937#[non_exhaustive]
939#[derive(Debug)]
940pub enum Error {
941 Parse(ParseError),
943 Json(serde_json::Error),
945 Render(String),
947}
948
949impl fmt::Display for Error {
950 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
951 match self {
952 Self::Parse(error) => write!(f, "{error}"),
953 Self::Json(error) => write!(f, "{error}"),
954 Self::Render(message) => write!(f, "{message}"),
955 }
956 }
957}
958
959impl StdError for Error {}
960
961impl From<ParseError> for Error {
962 fn from(error: ParseError) -> Self {
963 Self::Parse(error)
964 }
965}
966
967impl From<serde_json::Error> for Error {
968 fn from(error: serde_json::Error) -> Self {
969 Self::Json(error)
970 }
971}
972
973pub type Result<T, E = Error> = std::result::Result<T, E>;
975
976fn parse_str_with_options(input: &str, options: ParseOptions) -> Result<TjsonValue> {
977 Parser::parse_document(input, options.start_indent).map_err(Error::Parse)
978}
979
980#[cfg(test)]
981fn render_string(value: &TjsonValue) -> Result<String> {
982 render_string_with_options(value, TjsonOptions::default())
983}
984
985fn render_string_with_options(value: &TjsonValue, options: TjsonOptions) -> Result<String> {
986 Renderer::render(value, &options)
987}
988
989pub fn from_str<T: DeserializeOwned>(input: &str) -> Result<T> {
999 from_tjson_str_with_options(input, ParseOptions::default())
1000}
1001
1002fn from_tjson_str_with_options<T: DeserializeOwned>(
1003 input: &str,
1004 options: ParseOptions,
1005) -> Result<T> {
1006 let value = parse_str_with_options(input, options)?;
1007 let json = value.to_json()?;
1008 Ok(serde_json::from_value(json)?)
1009}
1010
1011pub fn to_string<T: Serialize>(value: &T) -> Result<String> {
1021 to_string_with(value, TjsonOptions::default())
1022}
1023
1024pub fn to_string_with<T: Serialize>(
1031 value: &T,
1032 options: TjsonOptions,
1033) -> Result<String> {
1034 let json = serde_json::to_value(value)?;
1035 let value = TjsonValue::from(json);
1036 render_string_with_options(&value, options)
1037}
1038
1039#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1040enum ArrayLineValueContext {
1041 ArrayLine,
1042 ObjectValue,
1043 SingleValue,
1044}
1045
1046#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1047enum ContainerKind {
1048 Array,
1049 Object,
1050}
1051
1052#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1053enum MultilineLocalEol {
1054 Lf,
1055 CrLf,
1056}
1057
1058impl MultilineLocalEol {
1059 fn as_str(self) -> &'static str {
1060 match self {
1061 Self::Lf => "\n",
1062 Self::CrLf => "\r\n",
1063 }
1064 }
1065
1066 fn opener_suffix(self) -> &'static str {
1067 match self {
1068 Self::Lf => "",
1069 Self::CrLf => "\\r\\n",
1070 }
1071 }
1072}
1073
1074struct Parser {
1075 lines: Vec<String>,
1076 line: usize,
1077 start_indent: usize,
1078}
1079
1080impl Parser {
1081 fn parse_document(
1082 input: &str,
1083 start_indent: usize,
1084 ) -> std::result::Result<TjsonValue, ParseError> {
1085 let normalized = normalize_input(input)?;
1086 let expanded = expand_indent_adjustments(&normalized);
1087 let mut parser = Self {
1088 lines: expanded.split('\n').map(str::to_owned).collect(),
1089 line: 0,
1090 start_indent,
1091 };
1092 parser.skip_ignorable_lines()?;
1093 if parser.line >= parser.lines.len() {
1094 return Err(ParseError::new(1, 1, "empty input", None));
1095 }
1096 let value = parser.parse_root_value()?;
1097 parser.skip_ignorable_lines()?;
1098 if parser.line < parser.lines.len() {
1099 let current = parser.current_line().unwrap_or("").trim_start();
1100 let msg = if current.starts_with("/>") {
1101 "unexpected /> indent offset glyph: no previous matching /< indent offset glyph"
1102 } else if current.starts_with("/ ") {
1103 "unexpected fold marker: no open string to fold"
1104 } else {
1105 "unexpected trailing content"
1106 };
1107 return Err(parser.error_current(msg));
1108 }
1109 Ok(value)
1110 }
1111
1112 fn parse_root_value(&mut self) -> std::result::Result<TjsonValue, ParseError> {
1113 let line = self
1114 .current_line()
1115 .ok_or_else(|| ParseError::new(1, 1, "empty input", None))?
1116 .to_owned();
1117 self.ensure_line_has_no_tabs(self.line)?;
1118 let indent = count_leading_spaces(&line);
1119 let content = &line[indent..];
1120
1121 if indent == self.start_indent && starts_with_marker_chain(content) {
1122 return self.parse_marker_chain_line(content, indent);
1123 }
1124
1125 if indent <= self.start_indent + 1 {
1126 return self
1127 .parse_standalone_scalar_line(&line[self.start_indent..], self.start_indent);
1128 }
1129
1130 if indent >= self.start_indent + 2 {
1131 let child_content = &line[self.start_indent + 2..];
1132 if self.looks_like_object_start(child_content, self.start_indent + 2) {
1133 return self.parse_implicit_object(self.start_indent);
1134 }
1135 return self.parse_implicit_array(self.start_indent);
1136 }
1137
1138 Err(self.error_current("expected a value at the starting indent"))
1139 }
1140
1141 fn parse_implicit_object(
1142 &mut self,
1143 parent_indent: usize,
1144 ) -> std::result::Result<TjsonValue, ParseError> {
1145 let mut entries = Vec::new();
1146 self.parse_object_tail(parent_indent + 2, &mut entries)?;
1147 if entries.is_empty() {
1148 return Err(self.error_current("expected at least one object entry"));
1149 }
1150 Ok(TjsonValue::Object(entries))
1151 }
1152
1153 fn parse_implicit_array(
1154 &mut self,
1155 parent_indent: usize,
1156 ) -> std::result::Result<TjsonValue, ParseError> {
1157 self.skip_ignorable_lines()?;
1158 let elem_indent = parent_indent + 2;
1159 let line = self
1160 .current_line()
1161 .ok_or_else(|| self.error_current("expected array contents"))?
1162 .to_owned();
1163 self.ensure_line_has_no_tabs(self.line)?;
1164 let indent = count_leading_spaces(&line);
1165 if indent < elem_indent {
1166 return Err(self.error_current("expected array elements indented by two spaces"));
1167 }
1168 let content = &line[elem_indent..];
1169 if content.starts_with('|') {
1170 return self.parse_table_array(elem_indent);
1171 }
1172 let mut elements = Vec::new();
1173 self.parse_array_tail(parent_indent, &mut elements)?;
1174 if elements.is_empty() {
1175 return Err(self.error_current("expected at least one array element"));
1176 }
1177 Ok(TjsonValue::Array(elements))
1178 }
1179
1180 fn parse_table_array(
1181 &mut self,
1182 elem_indent: usize,
1183 ) -> std::result::Result<TjsonValue, ParseError> {
1184 let header_line = self
1185 .current_line()
1186 .ok_or_else(|| self.error_current("expected a table header"))?
1187 .to_owned();
1188 self.ensure_line_has_no_tabs(self.line)?;
1189 let header = &header_line[elem_indent..];
1190 let columns = self.parse_table_header(header, elem_indent)?;
1191 self.line += 1;
1192 let mut rows = Vec::new();
1193 loop {
1194 self.skip_ignorable_lines()?;
1195 let Some(line) = self.current_line().map(str::to_owned) else {
1196 break;
1197 };
1198 self.ensure_line_has_no_tabs(self.line)?;
1199 let indent = count_leading_spaces(&line);
1200 if indent < elem_indent {
1201 break;
1202 }
1203 if indent != elem_indent {
1204 return Err(self.error_current("expected a table row at the array indent"));
1205 }
1206 let row = &line[elem_indent..];
1207 if !row.starts_with('|') {
1208 return Err(self.error_current("table arrays may only contain table rows"));
1209 }
1210 let pair_indent = elem_indent.saturating_sub(2);
1215 let mut row_owned = row.to_owned();
1216 loop {
1217 let mut offset = 1usize;
1219 loop {
1220 let Some(peek) = self.lines.get(self.line + offset) else { break; };
1221 let trimmed = peek.trim_start_matches(' ');
1222 if trimmed.starts_with("//") {
1223 offset += 1;
1224 } else {
1225 break;
1226 }
1227 }
1228 let Some(next_line) = self.lines.get(self.line + offset) else {
1229 break;
1230 };
1231 let next_indent = count_leading_spaces(next_line);
1232 if next_indent != pair_indent {
1233 break;
1234 }
1235 let next_content = &next_line[pair_indent..];
1236 if !next_content.starts_with("/ ") {
1237 break;
1238 }
1239 for i in 1..offset {
1241 self.ensure_line_has_no_tabs(self.line + i)?;
1242 }
1243 self.line += offset;
1244 self.ensure_line_has_no_tabs(self.line)?;
1245 row_owned.push_str(&next_content[2..]);
1246 }
1247 rows.push(self.parse_table_row(&columns, &row_owned, elem_indent)?);
1248 self.line += 1;
1249 }
1250 if rows.is_empty() {
1251 return Err(self.error_current("table arrays must contain at least one row"));
1252 }
1253 Ok(TjsonValue::Array(rows))
1254 }
1255
1256 fn parse_table_header(&self, row: &str, indent: usize) -> std::result::Result<Vec<String>, ParseError> {
1257 let mut cells = split_pipe_cells(row)
1258 .ok_or_else(|| self.error_at_line(self.line, indent + 1, "invalid table header"))?;
1259 if cells.first().is_some_and(String::is_empty) {
1260 cells.remove(0);
1261 }
1262 if !cells.last().is_some_and(String::is_empty) {
1263 return Err(self.error_at_line(self.line, indent + row.len() + 1, "table header must end with \" |\" (two spaces of padding then pipe)"));
1264 }
1265 cells.pop();
1266 if cells.is_empty() {
1267 return Err(self.error_at_line(self.line, 1, "table headers must list columns"));
1268 }
1269 let mut col = indent + 2; cells
1271 .into_iter()
1272 .map(|cell| {
1273 let cell_col = col;
1274 col += cell.len() + 1; self.parse_table_header_key(cell.trim_end(), cell_col)
1276 })
1277 .collect()
1278 }
1279
1280 fn parse_table_header_key(&self, cell: &str, col: usize) -> std::result::Result<String, ParseError> {
1281 if let Some(end) = parse_bare_key_prefix(cell)
1282 && end == cell.len() {
1283 return Ok(cell.to_owned());
1284 }
1285 if let Some((value, end)) = parse_json_string_prefix(cell)
1286 && end == cell.len() {
1287 return Ok(value);
1288 }
1289 Err(self.error_at_line(self.line, col, "invalid table header key"))
1290 }
1291
1292 fn parse_table_row(
1293 &self,
1294 columns: &[String],
1295 row: &str,
1296 indent: usize,
1297 ) -> std::result::Result<TjsonValue, ParseError> {
1298 let mut cells = split_pipe_cells(row)
1299 .ok_or_else(|| self.error_at_line(self.line, indent + 1, "invalid table row"))?;
1300 if cells.first().is_some_and(String::is_empty) {
1301 cells.remove(0);
1302 }
1303 if !cells.last().is_some_and(String::is_empty) {
1304 return Err(self.error_at_line(self.line, indent + row.len() + 1, "table row must end with \" |\" (two spaces of padding then pipe)"));
1305 }
1306 cells.pop();
1307 if cells.len() != columns.len() {
1308 return Err(self.error_at_line(
1309 self.line,
1310 indent + row.len() + 1,
1311 "table row has wrong number of cells",
1312 ));
1313 }
1314 let mut entries = Vec::new();
1315 for (index, key) in columns.iter().enumerate() {
1316 let cell = cells[index].trim_end();
1317 if cell.is_empty() {
1318 continue;
1319 }
1320 entries.push((key.clone(), self.parse_table_cell_value(cell)?));
1321 }
1322 Ok(TjsonValue::Object(entries))
1323 }
1324
1325 fn parse_table_cell_value(&self, cell: &str) -> std::result::Result<TjsonValue, ParseError> {
1326 if cell.is_empty() {
1327 return Err(self.error_at_line(
1328 self.line,
1329 1,
1330 "empty table cells mean the key is absent",
1331 ));
1332 }
1333 if let Some(value) = cell.strip_prefix(' ') {
1334 if !is_allowed_bare_string(value) {
1335 return Err(self.error_at_line(self.line, 1, "invalid bare string in table cell"));
1336 }
1337 return Ok(TjsonValue::String(value.to_owned()));
1338 }
1339 if let Some((value, end)) = parse_json_string_prefix(cell)
1340 && end == cell.len() {
1341 return Ok(TjsonValue::String(value));
1342 }
1343 if cell == "true" {
1344 return Ok(TjsonValue::Bool(true));
1345 }
1346 if cell == "false" {
1347 return Ok(TjsonValue::Bool(false));
1348 }
1349 if cell == "null" {
1350 return Ok(TjsonValue::Null);
1351 }
1352 if cell == "[]" {
1353 return Ok(TjsonValue::Array(Vec::new()));
1354 }
1355 if cell == "{}" {
1356 return Ok(TjsonValue::Object(Vec::new()));
1357 }
1358 if let Ok(n) = JsonNumber::from_str(cell) {
1359 return Ok(TjsonValue::Number(n));
1360 }
1361 Err(self.error_at_line(self.line, 1, "invalid table cell value"))
1362 }
1363
1364 fn parse_object_tail(
1365 &mut self,
1366 pair_indent: usize,
1367 entries: &mut Vec<(String, TjsonValue)>,
1368 ) -> std::result::Result<(), ParseError> {
1369 loop {
1370 self.skip_ignorable_lines()?;
1371 let Some(line) = self.current_line().map(str::to_owned) else {
1372 break;
1373 };
1374 self.ensure_line_has_no_tabs(self.line)?;
1375 let indent = count_leading_spaces(&line);
1376 if indent < pair_indent {
1377 break;
1378 }
1379 if indent != pair_indent {
1380 let content = line[indent..].to_owned();
1381 let msg = if content.starts_with("/>") {
1382 format!("misplaced /> indent offset glyph: found at column {}, expected at column {}", indent + 1, pair_indent + 1)
1383 } else if content.starts_with("/ ") {
1384 format!("misplaced fold marker: found at column {}, expected at column {}", indent + 1, pair_indent + 1)
1385 } else {
1386 "expected an object entry at this indent".to_owned()
1387 };
1388 return Err(self.error_current(msg));
1389 }
1390 let content = &line[pair_indent..];
1391 if content.is_empty() {
1392 return Err(self.error_current("blank lines are not valid inside objects"));
1393 }
1394 let line_entries = self.parse_object_line_content(content, pair_indent)?;
1395 entries.extend(line_entries);
1396 }
1397 Ok(())
1398 }
1399
1400 fn parse_object_line_content(
1401 &mut self,
1402 content: &str,
1403 pair_indent: usize,
1404 ) -> std::result::Result<Vec<(String, TjsonValue)>, ParseError> {
1405 let mut rest = content.to_owned();
1406 let mut entries = Vec::new();
1407 loop {
1408 let (key, after_colon) = self.parse_key(&rest, pair_indent)?;
1409 rest = after_colon;
1410
1411 if rest.is_empty() {
1412 self.line += 1;
1413 let value = self.parse_value_after_key(pair_indent)?;
1414 entries.push((key, value));
1415 return Ok(entries);
1416 }
1417
1418 let (value, consumed) =
1419 self.parse_inline_value(&rest, pair_indent, ArrayLineValueContext::ObjectValue)?;
1420 entries.push((key, value));
1421
1422 let Some(consumed) = consumed else {
1423 return Ok(entries);
1424 };
1425
1426 rest = rest[consumed..].to_owned();
1427 if rest.is_empty() {
1428 self.line += 1;
1429 return Ok(entries);
1430 }
1431 if !rest.starts_with(" ") {
1432 return Err(self
1433 .error_current("expected at least two spaces between object entries on the same line"));
1434 }
1435 let space_count = rest.bytes().take_while(|&b| b == b' ').count();
1438 rest = rest[space_count..].to_owned();
1439 if rest.is_empty() {
1440 return Err(self.error_current("object lines cannot end with a separator"));
1441 }
1442 }
1443 }
1444
1445 fn parse_value_after_key(
1446 &mut self,
1447 pair_indent: usize,
1448 ) -> std::result::Result<TjsonValue, ParseError> {
1449 self.skip_ignorable_lines()?;
1450 let child_indent = pair_indent + 2;
1451 let line = self
1452 .current_line()
1453 .ok_or_else(|| self.error_at_line(self.line, 1, "expected a nested value"))?
1454 .to_owned();
1455 self.ensure_line_has_no_tabs(self.line)?;
1456 let indent = count_leading_spaces(&line);
1457 let content = &line[indent..];
1458 if starts_with_marker_chain(content) && (indent == pair_indent || indent == child_indent) {
1459 return self.parse_marker_chain_line(content, indent);
1460 }
1461 if indent == pair_indent && content.starts_with("/ ") {
1465 let continuation_content = &content[2..];
1466 let (value, consumed) = self.parse_inline_value(
1467 continuation_content, pair_indent, ArrayLineValueContext::ObjectValue,
1468 )?;
1469 if consumed.is_some() {
1470 self.line += 1;
1471 }
1472 return Ok(value);
1473 }
1474 if indent < child_indent {
1475 return Err(self.error_current("nested values must be indented by two spaces"));
1476 }
1477 let content = &line[child_indent..];
1478 if is_minimal_json_candidate(content) {
1479 let value = self.parse_minimal_json_line(content)?;
1480 self.line += 1;
1481 return Ok(value);
1482 }
1483 if self.looks_like_object_start(content, pair_indent) {
1484 self.parse_implicit_object(pair_indent)
1485 } else {
1486 self.parse_implicit_array(pair_indent)
1487 }
1488 }
1489
1490 fn parse_standalone_scalar_line(
1491 &mut self,
1492 content: &str,
1493 line_indent: usize,
1494 ) -> std::result::Result<TjsonValue, ParseError> {
1495 if is_minimal_json_candidate(content) {
1496 let value = self.parse_minimal_json_line(content)?;
1497 self.line += 1;
1498 return Ok(value);
1499 }
1500 let (value, consumed) =
1501 self.parse_inline_value(content, line_indent, ArrayLineValueContext::SingleValue)?;
1502 if let Some(consumed) = consumed {
1503 if consumed != content.len() {
1504 return Err(self.error_current("only one value may appear here"));
1505 }
1506 self.line += 1;
1507 }
1508 Ok(value)
1509 }
1510
1511 fn parse_array_tail(
1512 &mut self,
1513 parent_indent: usize,
1514 elements: &mut Vec<TjsonValue>,
1515 ) -> std::result::Result<(), ParseError> {
1516 let elem_indent = parent_indent + 2;
1517 loop {
1518 self.skip_ignorable_lines()?;
1519 let Some(line) = self.current_line().map(str::to_owned) else {
1520 break;
1521 };
1522 self.ensure_line_has_no_tabs(self.line)?;
1523 let indent = count_leading_spaces(&line);
1524 let content = &line[indent..];
1525 if indent < parent_indent {
1526 break;
1527 }
1528 if starts_with_marker_chain(content) && indent == elem_indent {
1529 elements.push(self.parse_marker_chain_line(content, indent)?);
1530 continue;
1531 }
1532 if indent < elem_indent {
1533 break;
1534 }
1535 if indent == elem_indent + 1 && line.as_bytes().get(elem_indent) == Some(&b' ') {
1537 let content = &line[elem_indent..];
1538 self.parse_array_line_content(content, elem_indent, elements)?;
1539 continue;
1540 }
1541 if indent != elem_indent {
1542 return Err(self.error_current("invalid indent level: array elements must be indented by exactly two spaces"));
1543 }
1544 let content = &line[elem_indent..];
1545 if content.is_empty() {
1546 return Err(self.error_current("blank lines are not valid inside arrays"));
1547 }
1548 if content.starts_with('|') {
1549 return Err(self.error_current("table arrays are only valid as the entire array"));
1550 }
1551 if is_minimal_json_candidate(content) {
1552 elements.push(self.parse_minimal_json_line(content)?);
1553 self.line += 1;
1554 continue;
1555 }
1556 self.parse_array_line_content(content, elem_indent, elements)?;
1557 }
1558 Ok(())
1559 }
1560
1561 fn parse_array_line_content(
1562 &mut self,
1563 content: &str,
1564 elem_indent: usize,
1565 elements: &mut Vec<TjsonValue>,
1566 ) -> std::result::Result<(), ParseError> {
1567 let mut rest = content;
1568 let mut string_only_mode = false;
1569 loop {
1570 let (value, consumed) =
1571 self.parse_inline_value(rest, elem_indent, ArrayLineValueContext::ArrayLine)?;
1572 let is_string = matches!(value, TjsonValue::String(_));
1573 if string_only_mode && !is_string {
1574 return Err(self.error_current(
1575 "two-space array packing is only allowed when all values are strings",
1576 ));
1577 }
1578 elements.push(value);
1579 let Some(consumed) = consumed else {
1580 return Ok(());
1581 };
1582 rest = &rest[consumed..];
1583 if rest.is_empty() {
1584 self.line += 1;
1585 return Ok(());
1586 }
1587 if rest == "," {
1588 self.line += 1;
1589 return Ok(());
1590 }
1591 if let Some(next) = rest.strip_prefix(", ") {
1592 rest = next;
1593 string_only_mode = false;
1594 if rest.is_empty() {
1595 return Err(self.error_current("array lines cannot end with a separator"));
1596 }
1597 continue;
1598 }
1599 if let Some(next) = rest.strip_prefix(" ") {
1600 rest = next;
1601 string_only_mode = true;
1602 if rest.is_empty() {
1603 return Err(self.error_current("array lines cannot end with a separator"));
1604 }
1605 continue;
1606 }
1607 return Err(self.error_current(
1608 "array elements on the same line are separated by ', ' or by two spaces in string-only arrays",
1609 ));
1610 }
1611 }
1612
1613 fn parse_marker_chain_line(
1614 &mut self,
1615 content: &str,
1616 line_indent: usize,
1617 ) -> std::result::Result<TjsonValue, ParseError> {
1618 let mut rest = content;
1619 let mut markers = Vec::new();
1620 loop {
1621 if let Some(next) = rest.strip_prefix("[ ") {
1622 markers.push(ContainerKind::Array);
1623 rest = next;
1624 continue;
1625 }
1626 if let Some(next) = rest.strip_prefix("{ ") {
1627 markers.push(ContainerKind::Object);
1628 rest = next;
1629 break;
1630 }
1631 break;
1632 }
1633 if markers.is_empty() {
1634 return Err(self.error_current("expected an explicit nesting marker"));
1635 }
1636 if markers[..markers.len().saturating_sub(1)]
1637 .iter()
1638 .any(|kind| *kind != ContainerKind::Array)
1639 {
1640 return Err(
1641 self.error_current("only the final explicit nesting marker on a line may be '{'")
1642 );
1643 }
1644 if rest.is_empty() {
1645 return Err(self.error_current("a nesting marker must be followed by content"));
1646 }
1647 let deepest_parent_indent = line_indent + 2 * markers.len().saturating_sub(1);
1648
1649 if *markers.last().unwrap() == ContainerKind::Array {
1652 let rest_trimmed = rest.trim_start_matches(' ');
1653 if rest_trimmed.starts_with('|') {
1654 let leading_spaces = rest.len() - rest_trimmed.len();
1655 let table_elem_indent = deepest_parent_indent + 2 + leading_spaces;
1656 let mut value = self.parse_table_array(table_elem_indent)?;
1657 for level in (0..markers.len().saturating_sub(1)).rev() {
1658 let parent_indent = line_indent + 2 * level;
1659 let mut wrapped = vec![value];
1660 self.parse_array_tail(parent_indent, &mut wrapped)?;
1661 value = TjsonValue::Array(wrapped);
1662 }
1663 return Ok(value);
1664 }
1665 }
1666
1667 let mut value = match *markers.last().unwrap() {
1668 ContainerKind::Array => {
1669 let mut elements = Vec::new();
1670 if is_minimal_json_candidate(rest) {
1671 elements.push(self.parse_minimal_json_line(rest)?);
1672 self.line += 1;
1673 self.parse_array_tail(deepest_parent_indent, &mut elements)?;
1674 } else {
1675 self.parse_array_line_content(rest, deepest_parent_indent + 2, &mut elements)?;
1676 self.parse_array_tail(deepest_parent_indent, &mut elements)?;
1677 }
1678 TjsonValue::Array(elements)
1679 }
1680 ContainerKind::Object => {
1681 let pair_indent = line_indent + 2 * markers.len();
1682 let mut entries = self.parse_object_line_content(rest, pair_indent)?;
1683 self.parse_object_tail(pair_indent, &mut entries)?;
1684 TjsonValue::Object(entries)
1685 }
1686 };
1687 for level in (0..markers.len().saturating_sub(1)).rev() {
1688 let parent_indent = line_indent + 2 * level;
1689 let mut wrapped = vec![value];
1690 self.parse_array_tail(parent_indent, &mut wrapped)?;
1691 value = TjsonValue::Array(wrapped);
1692 }
1693 Ok(value)
1694 }
1695
1696 fn parse_key(
1699 &mut self,
1700 content: &str,
1701 fold_indent: usize,
1702 ) -> std::result::Result<(String, String), ParseError> {
1703 if let Some(end) = parse_bare_key_prefix(content) {
1705 if content.get(end..).is_some_and(|rest| rest.starts_with(':')) {
1706 return Ok((content[..end].to_owned(), content[end + ':'.len_utf8()..].to_owned()));
1707 }
1708 if end == content.len() {
1710 let mut key_acc = content[..end].to_owned();
1711 let mut next = self.line + 1;
1712 loop {
1713 let Some(fold_line) = self.lines.get(next).cloned() else {
1714 break;
1715 };
1716 let fi = count_leading_spaces(&fold_line);
1717 if fi != fold_indent {
1718 break;
1719 }
1720 let rest = &fold_line[fi..];
1721 if !rest.starts_with("/ ") {
1722 break;
1723 }
1724 let cont = &rest[2..];
1725 next += 1;
1726 if let Some(colon_pos) = cont.find(':') {
1727 key_acc.push_str(&cont[..colon_pos]);
1728 self.line = next - 1; return Ok((key_acc, cont[colon_pos + ':'.len_utf8()..].to_owned()));
1730 }
1731 key_acc.push_str(cont);
1732 }
1733 }
1734 }
1735 if let Some((value, end)) = parse_json_string_prefix(content)
1737 && content.get(end..).is_some_and(|rest| rest.starts_with(':')) {
1738 return Ok((value, content[end + ':'.len_utf8()..].to_owned()));
1739 }
1740 if content.starts_with('"') && parse_json_string_prefix(content).is_none() {
1742 let mut json_acc = content.to_owned();
1743 let mut next = self.line + 1;
1744 loop {
1745 let Some(fold_line) = self.lines.get(next).cloned() else {
1746 break;
1747 };
1748 let fi = count_leading_spaces(&fold_line);
1749 if fi != fold_indent {
1750 break;
1751 }
1752 let rest = &fold_line[fi..];
1753 if !rest.starts_with("/ ") {
1754 break;
1755 }
1756 json_acc.push_str(&rest[2..]);
1757 next += 1;
1758 if let Some((value, end)) = parse_json_string_prefix(&json_acc)
1759 && json_acc.get(end..).is_some_and(|rest| rest.starts_with(':')) {
1760 self.line = next - 1; return Ok((value, json_acc[end + ':'.len_utf8()..].to_owned()));
1762 }
1763 }
1764 }
1765 Err(self.error_at_line(self.line, fold_indent + 1, "invalid object key"))
1766 }
1767
1768 fn parse_inline_value(
1769 &mut self,
1770 content: &str,
1771 line_indent: usize,
1772 context: ArrayLineValueContext,
1773 ) -> std::result::Result<(TjsonValue, Option<usize>), ParseError> {
1774 let first = content
1775 .chars()
1776 .next()
1777 .ok_or_else(|| self.error_current("expected a value"))?;
1778 match first {
1779 ' ' => {
1780 if context == ArrayLineValueContext::ObjectValue {
1781 if content.starts_with(" []") {
1782 return Ok((TjsonValue::Array(Vec::new()), Some(3)));
1783 }
1784 if content.starts_with(" {}") {
1785 return Ok((TjsonValue::Object(Vec::new()), Some(3)));
1786 }
1787 if let Some(rest) = content.strip_prefix(" ") {
1788 let value = self.parse_inline_array(rest, line_indent)?;
1789 return Ok((value, None));
1790 }
1791 }
1792 if content.starts_with(" `") {
1793 let value = self.parse_multiline_string(content, line_indent)?;
1794 return Ok((TjsonValue::String(value), None));
1795 }
1796 let end = bare_string_end(content, context);
1797 if end == 0 {
1798 return Err(self.error_current("bare strings cannot start with a forbidden character"));
1799 }
1800 let value = &content[' '.len_utf8()..end]; if !is_allowed_bare_string(value) {
1802 return Err(self.error_current("invalid bare string"));
1803 }
1804 if end == content.len() {
1806 let mut acc = value.to_owned();
1807 let mut next = self.line + 1;
1808 let mut fold_count = 0usize;
1809 loop {
1810 let Some(fold_line) = self.lines.get(next) else {
1811 break;
1812 };
1813 let fi = count_leading_spaces(fold_line);
1814 if fi != line_indent {
1815 break;
1816 }
1817 let rest = &fold_line[fi..];
1818 if !rest.starts_with("/ ") {
1819 break;
1820 }
1821 acc.push_str(&rest[2..]);
1822 next += 1;
1823 fold_count += 1;
1824 }
1825 if fold_count > 0 {
1826 self.line = next;
1827 return Ok((TjsonValue::String(acc), None));
1828 }
1829 }
1830 Ok((TjsonValue::String(value.to_owned()), Some(end)))
1831 }
1832 '"' => {
1833 if let Some((value, end)) = parse_json_string_prefix(content) {
1834 return Ok((TjsonValue::String(value), Some(end)));
1835 }
1836 let value = self.parse_folded_json_string(content, line_indent)?;
1837 Ok((TjsonValue::String(value), None))
1838 }
1839 '[' => {
1840 if content.starts_with("[]") {
1841 return Ok((TjsonValue::Array(Vec::new()), Some(2)));
1842 }
1843 Err(self.error_current("nonempty arrays require container context"))
1844 }
1845 '{' => {
1846 if content.starts_with("{}") {
1847 return Ok((TjsonValue::Object(Vec::new()), Some(2)));
1848 }
1849 Err(self.error_current("nonempty objects require object or array context"))
1850 }
1851 't' if content.starts_with("true") => Ok((TjsonValue::Bool(true), Some(4))),
1852 'f' if content.starts_with("false") => Ok((TjsonValue::Bool(false), Some(5))),
1853 'n' if content.starts_with("null") => Ok((TjsonValue::Null, Some(4))),
1854 '-' | '0'..='9' => {
1855 let end = simple_token_end(content, context);
1856 let token = &content[..end];
1857 if end == content.len() {
1859 let mut acc = token.to_owned();
1860 let mut next = self.line + 1;
1861 let mut fold_count = 0usize;
1862 loop {
1863 let Some(fold_line) = self.lines.get(next) else { break; };
1864 let fi = count_leading_spaces(fold_line);
1865 if fi != line_indent { break; }
1866 let rest = &fold_line[fi..];
1867 if !rest.starts_with("/ ") { break; }
1868 acc.push_str(&rest[2..]);
1869 next += 1;
1870 fold_count += 1;
1871 }
1872 if fold_count > 0 {
1873 let n = JsonNumber::from_str(&acc)
1874 .map_err(|_| self.error_current(format!("invalid JSON number after folding: \"{acc}\"")))?;
1875 self.line = next;
1876 return Ok((TjsonValue::Number(n), None));
1877 }
1878 }
1879 let n = JsonNumber::from_str(token)
1880 .map_err(|_| self.error_current(format!("invalid JSON number: \"{token}\"")))?;
1881 Ok((TjsonValue::Number(n), Some(end)))
1882 }
1883 '.' if content[1..].starts_with(|c: char| c.is_ascii_digit()) => {
1884 let end = simple_token_end(content, context);
1885 let token = &content[..end];
1886 Err(self.error_current(format!("invalid JSON number: \"{token}\" (numbers must start with a digit)")))
1887 }
1888 _ => Err(self.error_current("invalid value start")),
1889 }
1890 }
1891
1892 fn parse_inline_array(
1893 &mut self,
1894 content: &str,
1895 parent_indent: usize,
1896 ) -> std::result::Result<TjsonValue, ParseError> {
1897 let mut values = Vec::new();
1898 self.parse_array_line_content(content, parent_indent + 2, &mut values)?;
1899 self.parse_array_tail(parent_indent, &mut values)?;
1900 Ok(TjsonValue::Array(values))
1901 }
1902
1903 fn parse_multiline_string(
1904 &mut self,
1905 content: &str,
1906 line_indent: usize,
1907 ) -> std::result::Result<String, ParseError> {
1908 let (glyph, suffix) = if let Some(rest) = content.strip_prefix(" ```") {
1909 ("```", rest)
1910 } else if let Some(rest) = content.strip_prefix(" ``") {
1911 ("``", rest)
1912 } else if let Some(rest) = content.strip_prefix(" `") {
1913 ("`", rest)
1914 } else {
1915 return Err(self.error_current("invalid multiline string opener"));
1916 };
1917
1918 let local_eol = match suffix {
1919 "" | "\\n" => MultilineLocalEol::Lf,
1920 "\\r\\n" => MultilineLocalEol::CrLf,
1921 _ => {
1922 return Err(self.error_current(
1923 "multiline string opener only allows \\n or \\r\\n after the backticks",
1924 ));
1925 }
1926 };
1927
1928 let closer = format!("{} {}{}", spaces(line_indent), glyph, suffix);
1930 let opener_line = self.line;
1931 self.line += 1;
1932
1933 match glyph {
1934 "```" => self.parse_triple_backtick_body(local_eol, &closer, opener_line),
1935 "``" => self.parse_double_backtick_body(local_eol, &closer, opener_line),
1936 "`" => self.parse_single_backtick_body(line_indent, local_eol, &closer, opener_line),
1937 _ => unreachable!(),
1938 }
1939 }
1940
1941 fn parse_triple_backtick_body(
1942 &mut self,
1943 local_eol: MultilineLocalEol,
1944 closer: &str,
1945 opener_line: usize,
1946 ) -> std::result::Result<String, ParseError> {
1947 let mut value = String::new();
1948 let mut line_count = 0usize;
1949 loop {
1950 let Some(line) = self.current_line().map(str::to_owned) else {
1951 return Err(self.error_at_line(
1952 opener_line,
1953 1,
1954 "unterminated multiline string: reached end of file without closing ``` glyph",
1955 ));
1956 };
1957 if line == closer {
1958 self.line += 1;
1959 break;
1960 }
1961 if line_count > 0 {
1962 value.push_str(local_eol.as_str());
1963 }
1964 value.push_str(&line);
1965 line_count += 1;
1966 self.line += 1;
1967 }
1968 if line_count < 2 {
1969 return Err(self.error_at_line(
1970 self.line - 1,
1971 1,
1972 "multiline strings must contain at least one real linefeed",
1973 ));
1974 }
1975 Ok(value)
1976 }
1977
1978 fn parse_double_backtick_body(
1979 &mut self,
1980 local_eol: MultilineLocalEol,
1981 closer: &str,
1982 opener_line: usize,
1983 ) -> std::result::Result<String, ParseError> {
1984 let mut value = String::new();
1985 let mut line_count = 0usize;
1986 loop {
1987 let Some(line) = self.current_line().map(str::to_owned) else {
1988 return Err(self.error_at_line(
1989 opener_line,
1990 1,
1991 "unterminated multiline string: reached end of file without closing `` glyph",
1992 ));
1993 };
1994 if line == closer {
1995 self.line += 1;
1996 break;
1997 }
1998 let trimmed = line.trim_start_matches(' ');
1999 if let Some(content_part) = trimmed.strip_prefix("| ") {
2000 if line_count > 0 {
2001 value.push_str(local_eol.as_str());
2002 }
2003 value.push_str(content_part);
2004 line_count += 1;
2005 } else if let Some(cont_part) = trimmed.strip_prefix("/ ") {
2006 if line_count == 0 {
2007 return Err(self.error_current(
2008 "fold continuation cannot appear before any content in a `` multiline string",
2009 ));
2010 }
2011 value.push_str(cont_part);
2012 } else {
2013 return Err(self.error_current(
2014 "`` multiline string body lines must start with '| ' or '/ '",
2015 ));
2016 }
2017 self.line += 1;
2018 }
2019 if line_count < 2 {
2020 return Err(self.error_at_line(
2021 self.line - 1,
2022 1,
2023 "multiline strings must contain at least one real linefeed",
2024 ));
2025 }
2026 Ok(value)
2027 }
2028
2029 fn parse_single_backtick_body(
2030 &mut self,
2031 n: usize,
2032 local_eol: MultilineLocalEol,
2033 closer: &str,
2034 opener_line: usize,
2035 ) -> std::result::Result<String, ParseError> {
2036 let content_indent = n + 2;
2037 let fold_marker = format!("{}{}", spaces(n), "/ ");
2038 let mut value = String::new();
2039 let mut line_count = 0usize;
2040 loop {
2041 let Some(line) = self.current_line().map(str::to_owned) else {
2042 return Err(self.error_at_line(
2043 opener_line,
2044 1,
2045 "unterminated multiline string: reached end of file without closing ` glyph",
2046 ));
2047 };
2048 if line == closer {
2049 self.line += 1;
2050 break;
2051 }
2052 if line.starts_with(&fold_marker) {
2053 if line_count == 0 {
2054 return Err(self.error_current(
2055 "fold continuation cannot appear before any content in a ` multiline string",
2056 ));
2057 }
2058 value.push_str(&line[content_indent..]);
2059 self.line += 1;
2060 continue;
2061 }
2062 if count_leading_spaces(&line) < content_indent {
2063 return Err(self.error_current(
2064 "` multiline string content lines must be indented at n+2 spaces",
2065 ));
2066 }
2067 if line_count > 0 {
2068 value.push_str(local_eol.as_str());
2069 }
2070 value.push_str(&line[content_indent..]);
2071 line_count += 1;
2072 self.line += 1;
2073 }
2074 if line_count < 2 {
2075 return Err(self.error_at_line(
2076 self.line - 1,
2077 1,
2078 "multiline strings must contain at least one real linefeed",
2079 ));
2080 }
2081 Ok(value)
2082 }
2083
2084 fn parse_folded_json_string(
2085 &mut self,
2086 content: &str,
2087 fold_indent: usize,
2088 ) -> std::result::Result<String, ParseError> {
2089 let mut json = content.to_owned();
2090 let start_line = self.line;
2091 self.line += 1;
2092 loop {
2093 let line = self
2094 .current_line()
2095 .ok_or_else(|| self.error_at_line(start_line, fold_indent + 1, "unterminated JSON string"))?
2096 .to_owned();
2097 self.ensure_line_has_no_tabs(self.line)?;
2098 let fi = count_leading_spaces(&line);
2099 if fi != fold_indent {
2100 return Err(self.error_at_line(start_line, fold_indent + 1, "unterminated JSON string"));
2101 }
2102 let rest = &line[fi..];
2103 if !rest.starts_with("/ ") {
2104 return Err(self.error_at_line(start_line, fold_indent + 1, "unterminated JSON string"));
2105 }
2106 json.push_str(&rest[2..]);
2107 self.line += 1;
2108 if let Some((value, end)) = parse_json_string_prefix(&json) {
2109 if end != json.len() {
2110 return Err(self.error_current(
2111 "folded JSON strings may not have trailing content after the closing quote",
2112 ));
2113 }
2114 return Ok(value);
2115 }
2116 }
2117 }
2118
2119 fn parse_minimal_json_line(
2120 &self,
2121 content: &str,
2122 ) -> std::result::Result<TjsonValue, ParseError> {
2123 if let Err(col) = is_valid_minimal_json(content) {
2124 return Err(self.error_at_line(
2125 self.line,
2126 col + 1,
2127 "invalid MINIMAL JSON (whitespace outside strings is forbidden)",
2128 ));
2129 }
2130 let value: JsonValue = serde_json::from_str(content).map_err(|error| {
2131 let col = error.column();
2132 self.error_at_line(self.line, col, format!("minimal JSON error: {error}"))
2133 })?;
2134 Ok(TjsonValue::from(value))
2135 }
2136
2137 fn current_line(&self) -> Option<&str> {
2138 self.lines.get(self.line).map(String::as_str)
2139 }
2140
2141 fn skip_ignorable_lines(&mut self) -> std::result::Result<(), ParseError> {
2142 while let Some(line) = self.current_line() {
2143 self.ensure_line_has_no_tabs(self.line)?;
2144 let trimmed = line.trim_start_matches(' ');
2145 if line.is_empty() || trimmed.starts_with("//") {
2146 self.line += 1;
2147 continue;
2148 }
2149 break;
2150 }
2151 Ok(())
2152 }
2153
2154 fn ensure_line_has_no_tabs(&self, line_index: usize) -> std::result::Result<(), ParseError> {
2155 let Some(line) = self.lines.get(line_index) else {
2156 return Ok(());
2157 };
2158 let indent_end = line.len() - line.trim_start_matches(' ').len();
2160 if let Some(column) = line[..indent_end].find('\t') {
2161 return Err(self.error_at_line(
2162 line_index,
2163 column + 1,
2164 "tab characters are not allowed as indentation",
2165 ));
2166 }
2167 Ok(())
2168 }
2169
2170 fn looks_like_object_start(&self, content: &str, fold_indent: usize) -> bool {
2171 if content.starts_with('|') || starts_with_marker_chain(content) {
2172 return false;
2173 }
2174 if let Some(end) = parse_bare_key_prefix(content) {
2175 if content.get(end..).is_some_and(|rest| rest.starts_with(':')) {
2176 return true;
2177 }
2178 if end == content.len() && self.next_line_is_fold_continuation(fold_indent) {
2180 return true;
2181 }
2182 }
2183 if let Some((_, end)) = parse_json_string_prefix(content) {
2184 return content.get(end..).is_some_and(|rest| rest.starts_with(':'));
2185 }
2186 if content.starts_with('"')
2188 && parse_json_string_prefix(content).is_none()
2189 && self.next_line_is_fold_continuation(fold_indent)
2190 {
2191 return true;
2192 }
2193 false
2194 }
2195
2196 fn next_line_is_fold_continuation(&self, expected_indent: usize) -> bool {
2197 self.lines.get(self.line + 1).is_some_and(|l| {
2198 let fi = count_leading_spaces(l);
2199 fi == expected_indent && l[fi..].starts_with("/ ")
2200 })
2201 }
2202
2203 fn error_current(&self, message: impl Into<String>) -> ParseError {
2204 let column = self
2205 .current_line()
2206 .map(|line| count_leading_spaces(line) + 1)
2207 .unwrap_or(1);
2208 self.error_at_line(self.line, column, message)
2209 }
2210
2211 fn error_at_line(
2212 &self,
2213 line_index: usize,
2214 column: usize,
2215 message: impl Into<String>,
2216 ) -> ParseError {
2217 ParseError::new(line_index + 1, column, message, self.lines.get(line_index).map(|l| l.to_owned()))
2218 }
2219}
2220
2221enum PackedToken {
2222 Inline(String, TjsonValue),
2225 Block(TjsonValue),
2228}
2229
2230struct Renderer;
2231
2232impl Renderer {
2233 fn render(value: &TjsonValue, options: &TjsonOptions) -> Result<String> {
2234 let lines = Self::render_root(value, options, options.start_indent)?;
2235 Ok(lines.join("\n"))
2236 }
2237
2238 fn render_root(
2239 value: &TjsonValue,
2240 options: &TjsonOptions,
2241 start_indent: usize,
2242 ) -> Result<Vec<String>> {
2243 match value {
2244 TjsonValue::Null
2245 | TjsonValue::Bool(_)
2246 | TjsonValue::Number(_)
2247 | TjsonValue::String(_) => Ok(Self::render_scalar_lines(value, start_indent, options)?),
2248 TjsonValue::Array(values) if values.is_empty() => {
2249 Ok(Self::render_scalar_lines(value, start_indent, options)?)
2250 }
2251 TjsonValue::Object(entries) if entries.is_empty() => {
2252 Ok(Self::render_scalar_lines(value, start_indent, options)?)
2253 }
2254 TjsonValue::Array(values) if effective_force_markers(options) => {
2255 Self::render_explicit_array(values, start_indent, options)
2256 }
2257 TjsonValue::Array(values) => Self::render_implicit_array(values, start_indent, options),
2258 TjsonValue::Object(entries) if effective_force_markers(options) => {
2259 Self::render_explicit_object(entries, start_indent, options)
2260 }
2261 TjsonValue::Object(entries) => {
2262 Self::render_implicit_object(entries, start_indent, options)
2263 }
2264 }
2265 }
2266
2267 fn render_implicit_object(
2268 entries: &[(String, TjsonValue)],
2269 parent_indent: usize,
2270 options: &TjsonOptions,
2271 ) -> Result<Vec<String>> {
2272 let pair_indent = parent_indent + 2;
2273 let mut lines = Vec::new();
2274 let mut packed_line = String::new();
2275
2276 for (key, value) in entries {
2277 if effective_inline_objects(options)
2278 && let Some(token) = Self::render_inline_object_token(key, value, options)? {
2279 let candidate = if packed_line.is_empty() {
2280 format!("{}{}", spaces(pair_indent), token)
2281 } else {
2282 format!("{packed_line}{}{token}", spaces(options.kv_pack_multiple * 2))
2283 };
2284 if fits_wrap(options, &candidate) {
2285 packed_line = candidate;
2286 continue;
2287 }
2288 if !packed_line.is_empty() {
2289 lines.push(std::mem::take(&mut packed_line));
2290 }
2291 }
2294
2295 if !packed_line.is_empty() {
2296 lines.push(std::mem::take(&mut packed_line));
2297 }
2298 lines.extend(Self::render_object_entry(key, value, pair_indent, options)?);
2299 }
2300
2301 if !packed_line.is_empty() {
2302 lines.push(packed_line);
2303 }
2304 Ok(lines)
2305 }
2306
2307 fn render_object_entry(
2308 key: &str,
2309 value: &TjsonValue,
2310 pair_indent: usize,
2311 options: &TjsonOptions,
2312 ) -> Result<Vec<String>> {
2313 let is_bare = options.bare_keys == BareStyle::Prefer
2314 && parse_bare_key_prefix(key).is_some_and(|end| end == key.len());
2315 let key_text = render_key(key, options);
2316
2317 let key_fold_enabled = if is_bare {
2318 options.string_bare_fold_style != FoldStyle::None
2319 } else {
2320 options.string_quoted_fold_style != FoldStyle::None
2321 };
2322
2323 let key_fold: Option<Vec<String>> =
2328 if is_bare && options.string_bare_fold_style != FoldStyle::None {
2329 fold_bare_key(&key_text, pair_indent, options.string_bare_fold_style, options.wrap_width)
2330 } else if !is_bare && options.string_quoted_fold_style != FoldStyle::None {
2331 fold_json_string(key, pair_indent, 0, options.string_quoted_fold_style, options.wrap_width)
2332 } else {
2333 None
2334 };
2335
2336 if let Some(mut fold_lines) = key_fold {
2337 let last_fold_line = fold_lines.last().unwrap();
2340 let after_colon_avail = options.wrap_width
2343 .map(|w| w.saturating_sub(last_fold_line.len() + 1))
2344 .unwrap_or(usize::MAX);
2345
2346 let normal = Self::render_object_entry_body(&key_text, value, pair_indent, key_fold_enabled, options)?;
2347 let key_prefix = format!("{}{}:", spaces(pair_indent), key_text);
2348 let suffix = normal[0].strip_prefix(&key_prefix).unwrap_or("").to_owned();
2349
2350 if suffix.is_empty() || after_colon_avail >= suffix.len() {
2352 let last = fold_lines.pop().unwrap();
2354 fold_lines.push(format!("{}:{}", last, suffix));
2355 fold_lines.extend(normal.into_iter().skip(1));
2356 } else {
2357 let cont_lines = Self::render_scalar_value_continuation_lines(value, pair_indent, options)?;
2359 let last = fold_lines.pop().unwrap();
2360 fold_lines.push(format!("{}:", last));
2361 let first_cont = &cont_lines[0][pair_indent..];
2362 fold_lines.push(format!("{}/ {}", spaces(pair_indent), first_cont));
2363 fold_lines.extend(cont_lines.into_iter().skip(1));
2364 }
2365 return Ok(fold_lines);
2366 }
2367
2368 Self::render_object_entry_body(&key_text, value, pair_indent, key_fold_enabled, options)
2369 }
2370
2371 fn render_scalar_value_continuation_lines(
2376 value: &TjsonValue,
2377 pair_indent: usize,
2378 options: &TjsonOptions,
2379 ) -> Result<Vec<String>> {
2380 match value {
2381 TjsonValue::String(s) => Self::render_string_lines(s, pair_indent, 2, options),
2382 TjsonValue::Number(n) => {
2383 let ns = n.to_string();
2384 if let Some(folds) = fold_number(&ns, pair_indent, 2, options.number_fold_style, options.wrap_width) {
2385 Ok(folds)
2386 } else {
2387 Ok(vec![format!("{}{}", spaces(pair_indent), ns)])
2388 }
2389 }
2390 _ => Self::render_scalar_lines(value, pair_indent, options),
2391 }
2392 }
2393
2394 fn render_object_entry_body(
2395 key_text: &str,
2396 value: &TjsonValue,
2397 pair_indent: usize,
2398 key_fold_enabled: bool,
2399 options: &TjsonOptions,
2400 ) -> Result<Vec<String>> {
2401 match value {
2402 TjsonValue::Array(values) if !values.is_empty() => {
2403 if effective_tables(options)
2404 && let Some(table_lines) = Self::render_table(values, pair_indent, options)? {
2405 if let Some(target_indent) = table_unindent_target(pair_indent, &table_lines, options) {
2406 let Some(offset_lines) = Self::render_table(values, target_indent, options)? else {
2407 return Err(crate::Error::Render(
2408 "table eligible at natural indent failed to re-render at offset indent".into(),
2409 ));
2410 };
2411 let key_line = format!("{}{}", spaces(pair_indent), key_text);
2412 let mut lines = indent_glyph_open_lines(&key_line, pair_indent, options);
2413 if effective_force_markers(options) {
2414 let elem_indent = target_indent + 2;
2415 let first = offset_lines.first().ok_or_else(|| Error::Render("empty table".to_owned()))?;
2416 let stripped = first.get(elem_indent..).ok_or_else(|| Error::Render("failed to align table marker".to_owned()))?;
2417 lines.push(format!("{}[ {}", spaces(target_indent), stripped));
2418 lines.extend(offset_lines.into_iter().skip(1));
2419 } else {
2420 lines.extend(offset_lines);
2421 }
2422 lines.push(format!("{} />", spaces(pair_indent)));
2423 return Ok(lines);
2424 }
2425 let mut lines = vec![format!("{}{}:", spaces(pair_indent), key_text)];
2426 if effective_force_markers(options) {
2427 let elem_indent = pair_indent + 2;
2428 let first = table_lines.first().ok_or_else(|| Error::Render("empty table".to_owned()))?;
2429 let stripped = first.get(elem_indent..).ok_or_else(|| Error::Render("failed to align table marker".to_owned()))?;
2430 lines.push(format!("{}[ {}", spaces(pair_indent), stripped));
2431 lines.extend(table_lines.into_iter().skip(1));
2432 } else {
2433 lines.extend(table_lines);
2434 }
2435 return Ok(lines);
2436 }
2437
2438 if should_use_indent_glyph(value, pair_indent, options) {
2439 let key_line = format!("{}{}", spaces(pair_indent), key_text);
2440 let mut lines = indent_glyph_open_lines(&key_line, pair_indent, options);
2441 if values.first().is_some_and(needs_explicit_array_marker) {
2442 lines.extend(Self::render_explicit_array(values, 2, options)?);
2443 } else {
2444 lines.extend(Self::render_array_children(values, 2, options)?);
2445 }
2446 lines.push(format!("{} />", spaces(pair_indent)));
2447 return Ok(lines);
2448 }
2449
2450 if effective_inline_arrays(options) {
2451 let all_simple = values.iter().all(|v| match v {
2452 TjsonValue::Array(a) => a.is_empty(),
2453 TjsonValue::Object(o) => o.is_empty(),
2454 _ => true,
2455 });
2456 if all_simple
2457 && let Some(lines) = Self::render_packed_array_lines(
2458 values,
2459 format!("{}{}: ", spaces(pair_indent), key_text),
2460 pair_indent + 2,
2461 options,
2462 )? {
2463 return Ok(lines);
2464 }
2465 }
2466
2467 let mut lines = vec![format!("{}{}:", spaces(pair_indent), key_text)];
2468 if values.first().is_some_and(needs_explicit_array_marker) || effective_force_markers(options) {
2469 lines.extend(Self::render_explicit_array(
2470 values,
2471 pair_indent,
2472 options,
2473 )?);
2474 } else {
2475 lines.extend(Self::render_array_children(
2476 values,
2477 pair_indent + 2,
2478 options,
2479 )?);
2480 }
2481 Ok(lines)
2482 }
2483 TjsonValue::Object(entries) if !entries.is_empty() => {
2484 if should_use_indent_glyph(value, pair_indent, options) {
2485 let key_line = format!("{}{}", spaces(pair_indent), key_text);
2486 let mut lines = indent_glyph_open_lines(&key_line, pair_indent, options);
2487 lines.extend(Self::render_implicit_object(entries, 0, options)?);
2488 lines.push(format!("{} />", spaces(pair_indent)));
2489 return Ok(lines);
2490 }
2491
2492 let mut lines = vec![format!("{}{}:", spaces(pair_indent), key_text)];
2493 if effective_force_markers(options) {
2494 lines.extend(Self::render_explicit_object(entries, pair_indent, options)?);
2495 } else {
2496 lines.extend(Self::render_implicit_object(entries, pair_indent, options)?);
2497 }
2498 Ok(lines)
2499 }
2500 _ => {
2501 let scalar_lines = if let TjsonValue::String(s) = value {
2502 Self::render_string_lines(s, pair_indent, key_text.len() + 1, options)?
2503 } else {
2504 Self::render_scalar_lines(value, pair_indent, options)?
2505 };
2506 let first = scalar_lines[0].clone();
2507 let value_suffix = &first[pair_indent..]; let assembled_len = pair_indent + key_text.len() + 1 + value_suffix.len();
2513 if key_fold_enabled
2514 && let Some(w) = options.wrap_width
2515 && assembled_len > w {
2516 let cont_lines = Self::render_scalar_value_continuation_lines(value, pair_indent, options)?;
2517 let key_line = format!("{}{}:", spaces(pair_indent), key_text);
2518 let first_cont = &cont_lines[0][pair_indent..];
2519 let mut lines = vec![key_line, format!("{}/ {}", spaces(pair_indent), first_cont)];
2520 lines.extend(cont_lines.into_iter().skip(1));
2521 return Ok(lines);
2522 }
2523
2524 let mut lines = vec![format!(
2525 "{}{}:{}",
2526 spaces(pair_indent),
2527 key_text,
2528 value_suffix
2529 )];
2530 lines.extend(scalar_lines.into_iter().skip(1));
2531 Ok(lines)
2532 }
2533 }
2534 }
2535
2536 fn render_implicit_array(
2537 values: &[TjsonValue],
2538 parent_indent: usize,
2539 options: &TjsonOptions,
2540 ) -> Result<Vec<String>> {
2541 if effective_tables(options)
2542 && let Some(lines) = Self::render_table(values, parent_indent, options)? {
2543 return Ok(lines);
2544 }
2545
2546 if effective_inline_arrays(options) && !values.first().is_some_and(needs_explicit_array_marker)
2547 && let Some(lines) = Self::render_packed_array_lines(
2548 values,
2549 spaces(parent_indent + 2),
2550 parent_indent + 2,
2551 options,
2552 )? {
2553 return Ok(lines);
2554 }
2555
2556 let elem_indent = parent_indent + 2;
2557 let element_lines = values
2558 .iter()
2559 .map(|value| Self::render_array_element(value, elem_indent, options))
2560 .collect::<Result<Vec<_>>>()?;
2561 if values.first().is_some_and(needs_explicit_array_marker) {
2562 let mut lines = Vec::new();
2563 let first = &element_lines[0];
2564 let first_line = first.first().ok_or_else(|| {
2565 Error::Render("expected at least one array element line".to_owned())
2566 })?;
2567 let stripped = first_line.get(elem_indent..).ok_or_else(|| {
2568 Error::Render("failed to align the explicit outer array marker".to_owned())
2569 })?;
2570 lines.push(format!("{}[ {}", spaces(parent_indent), stripped));
2571 lines.extend(first.iter().skip(1).cloned());
2572 for extra in element_lines.iter().skip(1) {
2573 lines.extend(extra.clone());
2574 }
2575 Ok(lines)
2576 } else {
2577 Ok(element_lines.into_iter().flatten().collect())
2578 }
2579 }
2580
2581 fn render_array_children(
2582 values: &[TjsonValue],
2583 elem_indent: usize,
2584 options: &TjsonOptions,
2585 ) -> Result<Vec<String>> {
2586 let mut lines = Vec::new();
2587 let table_row_prefix = format!("{}|", spaces(elem_indent));
2588 for value in values {
2589 let prev_was_table = lines.last().map(|l: &String| l.starts_with(&table_row_prefix)).unwrap_or(false);
2590 let elem_lines = Self::render_array_element(value, elem_indent, options)?;
2591 let curr_is_table = elem_lines.first().map(|l| l.starts_with(&table_row_prefix)).unwrap_or(false);
2592 if prev_was_table && curr_is_table {
2593 let first = elem_lines.first().unwrap();
2595 let stripped = &first[elem_indent..]; lines.push(format!("{}[ {}", spaces(elem_indent.saturating_sub(2)), stripped));
2597 lines.extend(elem_lines.into_iter().skip(1));
2598 } else {
2599 lines.extend(elem_lines);
2600 }
2601 }
2602 Ok(lines)
2603 }
2604
2605 fn render_explicit_array(
2606 values: &[TjsonValue],
2607 marker_indent: usize,
2608 options: &TjsonOptions,
2609 ) -> Result<Vec<String>> {
2610 if effective_tables(options)
2611 && let Some(lines) = Self::render_table(values, marker_indent, options)? {
2612 let elem_indent = marker_indent + 2;
2615 let first = lines.first().ok_or_else(|| Error::Render("empty table".to_owned()))?;
2616 let stripped = first.get(elem_indent..).ok_or_else(|| Error::Render("failed to align table marker".to_owned()))?;
2617 let mut out = vec![format!("{}[ {}", spaces(marker_indent), stripped)];
2618 out.extend(lines.into_iter().skip(1));
2619 return Ok(out);
2620 }
2621
2622 if effective_inline_arrays(options)
2623 && let Some(lines) = Self::render_packed_array_lines(
2624 values,
2625 format!("{}[ ", spaces(marker_indent)),
2626 marker_indent + 2,
2627 options,
2628 )? {
2629 return Ok(lines);
2630 }
2631
2632 let elem_indent = marker_indent + 2;
2633 let mut element_lines = Vec::new();
2634 for value in values {
2635 element_lines.push(Self::render_array_element(value, elem_indent, options)?);
2636 }
2637 let first = element_lines
2638 .first()
2639 .ok_or_else(|| Error::Render("explicit arrays must be nonempty".to_owned()))?;
2640 let first_line = first
2641 .first()
2642 .ok_or_else(|| Error::Render("expected at least one explicit array line".to_owned()))?;
2643 let stripped = first_line
2644 .get(elem_indent..)
2645 .ok_or_else(|| Error::Render("failed to align an explicit array marker".to_owned()))?;
2646 let mut lines = vec![format!("{}[ {}", spaces(marker_indent), stripped)];
2647 lines.extend(first.iter().skip(1).cloned());
2648 for extra in element_lines.iter().skip(1) {
2649 lines.extend(extra.clone());
2650 }
2651 Ok(lines)
2652 }
2653
2654 fn render_explicit_object(
2655 entries: &[(String, TjsonValue)],
2656 marker_indent: usize,
2657 options: &TjsonOptions,
2658 ) -> Result<Vec<String>> {
2659 let pair_indent = marker_indent + 2;
2660 let implicit_lines = Self::render_implicit_object(entries, marker_indent, options)?;
2661 let first_line = implicit_lines.first().ok_or_else(|| {
2662 Error::Render("expected at least one explicit object line".to_owned())
2663 })?;
2664 let stripped = first_line
2665 .get(pair_indent..)
2666 .ok_or_else(|| Error::Render("failed to align an explicit object marker".to_owned()))?;
2667 let mut lines = vec![format!("{}{{ {}", spaces(marker_indent), stripped)];
2668 lines.extend(implicit_lines.into_iter().skip(1));
2669 Ok(lines)
2670 }
2671
2672 fn render_array_element(
2673 value: &TjsonValue,
2674 elem_indent: usize,
2675 options: &TjsonOptions,
2676 ) -> Result<Vec<String>> {
2677 match value {
2678 TjsonValue::Array(values) if !values.is_empty() => {
2679 if should_use_indent_glyph(value, elem_indent, options) {
2680 let mut lines = vec![format!("{}[ /<", spaces(elem_indent))];
2681 if values.first().is_some_and(needs_explicit_array_marker) {
2682 lines.extend(Self::render_explicit_array(values, 2, options)?);
2683 } else {
2684 lines.extend(Self::render_array_children(values, 2, options)?);
2685 }
2686 lines.push(format!("{} />", spaces(elem_indent)));
2687 return Ok(lines);
2688 }
2689 Self::render_explicit_array(values, elem_indent, options)
2690 }
2691 TjsonValue::Object(entries) if !entries.is_empty() => {
2692 Self::render_explicit_object(entries, elem_indent, options)
2693 }
2694 _ => Self::render_scalar_lines(value, elem_indent, options),
2695 }
2696 }
2697
2698 fn render_scalar_lines(
2699 value: &TjsonValue,
2700 indent: usize,
2701 options: &TjsonOptions,
2702 ) -> Result<Vec<String>> {
2703 match value {
2704 TjsonValue::Null => Ok(vec![format!("{}null", spaces(indent))]),
2705 TjsonValue::Bool(value) => Ok(vec![format!(
2706 "{}{}",
2707 spaces(indent),
2708 if *value { "true" } else { "false" }
2709 )]),
2710 TjsonValue::Number(value) => {
2711 let s = value.to_string();
2712 if let Some(lines) = fold_number(&s, indent, 0, options.number_fold_style, options.wrap_width) {
2713 return Ok(lines);
2714 }
2715 Ok(vec![format!("{}{}", spaces(indent), s)])
2716 }
2717 TjsonValue::String(value) => Self::render_string_lines(value, indent, 0, options),
2718 TjsonValue::Array(values) => {
2719 if values.is_empty() {
2720 Ok(vec![format!("{}[]", spaces(indent))])
2721 } else {
2722 Err(Error::Render(
2723 "nonempty arrays must be rendered through array context".to_owned(),
2724 ))
2725 }
2726 }
2727 TjsonValue::Object(entries) => {
2728 if entries.is_empty() {
2729 Ok(vec![format!("{}{{}}", spaces(indent))])
2730 } else {
2731 Err(Error::Render(
2732 "nonempty objects must be rendered through object or array context"
2733 .to_owned(),
2734 ))
2735 }
2736 }
2737 }
2738 }
2739
2740 fn render_string_lines(
2741 value: &str,
2742 indent: usize,
2743 first_line_extra: usize,
2744 options: &TjsonOptions,
2745 ) -> Result<Vec<String>> {
2746 if value.is_empty() {
2747 return Ok(vec![format!("{}\"\"", spaces(indent))]);
2748 }
2749 if matches!(options.multiline_style, MultilineStyle::FoldingQuotes)
2752 && detect_multiline_local_eol(value).is_some()
2753 {
2754 return Ok(render_folding_quotes(value, indent, options));
2755 }
2756
2757 if options.multiline_strings
2758 && !value.chars().any(is_forbidden_literal_tjson_char)
2759 && let Some(local_eol) = detect_multiline_local_eol(value)
2760 {
2761 let suffix = local_eol.opener_suffix();
2762 let parts: Vec<&str> = match local_eol {
2763 MultilineLocalEol::Lf => value.split('\n').collect(),
2764 MultilineLocalEol::CrLf => value.split("\r\n").collect(),
2765 };
2766 let min_eols = options.multiline_min_lines.max(1);
2767 if parts.len().saturating_sub(1) >= min_eols {
2769 let fold_style = options.string_multiline_fold_style;
2770 let wrap = options.wrap_width;
2771
2772 let pipe_heavy = {
2774 let pipe_count = parts
2775 .iter()
2776 .filter(|p| line_starts_with_ws_then(p, '|'))
2777 .count();
2778 !parts.is_empty() && pipe_count * 10 > parts.len()
2779 };
2780 let backtick_start = parts.iter().any(|p| line_starts_with_ws_then(p, '`'));
2781 let forced_bold = pipe_heavy || backtick_start;
2782
2783 let overflows_at_natural = wrap
2785 .map(|w| parts.iter().any(|p| indent + 2 + p.len() > w))
2786 .unwrap_or(false);
2787
2788 let too_many_lines = options.multiline_max_lines > 0
2790 && parts.len() > options.multiline_max_lines;
2791
2792 let bold = |body_indent: usize| {
2793 Self::render_multiline_double_backtick(
2794 &parts, indent, body_indent, suffix, fold_style, wrap,
2795 )
2796 };
2797
2798 return Ok(match options.multiline_style {
2799 MultilineStyle::Floating => {
2800 if forced_bold || overflows_at_natural || too_many_lines {
2802 bold(0)
2803 } else {
2804 Self::render_multiline_single_backtick(
2805 &parts, indent, suffix, fold_style, wrap,
2806 )
2807 }
2808 }
2809 MultilineStyle::Light => {
2810 if forced_bold {
2814 bold(0)
2815 } else {
2816 Self::render_multiline_single_backtick(
2817 &parts, indent, suffix, fold_style, wrap,
2818 )
2819 }
2820 }
2821 MultilineStyle::Bold => bold(0),
2822 MultilineStyle::BoldFloating => {
2823 let body = if forced_bold || overflows_at_natural { 0 } else { indent };
2824 bold(body)
2825 }
2826 MultilineStyle::Transparent => {
2827 if forced_bold {
2828 bold(0)
2829 } else {
2830 Self::render_multiline_triple_backtick(&parts, indent, suffix)
2831 }
2832 }
2833 MultilineStyle::FoldingQuotes => unreachable!(),
2834 });
2835 }
2836 }
2837 if options.bare_strings == BareStyle::Prefer && is_allowed_bare_string(value) {
2838 if options.string_bare_fold_style != FoldStyle::None
2839 && let Some(lines) =
2840 fold_bare_string(value, indent, first_line_extra, options.string_bare_fold_style, options.wrap_width)
2841 {
2842 return Ok(lines);
2843 }
2844 return Ok(vec![format!("{} {}", spaces(indent), value)]);
2845 }
2846 if options.string_quoted_fold_style != FoldStyle::None
2847 && let Some(lines) =
2848 fold_json_string(value, indent, first_line_extra, options.string_quoted_fold_style, options.wrap_width)
2849 {
2850 return Ok(lines);
2851 }
2852 Ok(vec![format!("{}{}", spaces(indent), render_json_string(value))])
2853 }
2854
2855 fn render_multiline_single_backtick(
2859 parts: &[&str],
2860 indent: usize,
2861 suffix: &str,
2862 fold_style: FoldStyle,
2863 wrap_width: Option<usize>,
2864 ) -> Vec<String> {
2865 let glyph = format!("{} `{}", spaces(indent), suffix);
2866 let body_indent = indent + 2;
2867 let fold_prefix = format!("{}/ ", spaces(indent));
2868 let avail = wrap_width.map(|w| w.saturating_sub(body_indent));
2869 let mut lines = vec![glyph.clone()];
2870 for part in parts {
2871 if fold_style != FoldStyle::None
2872 && let Some(avail_w) = avail
2873 && part.len() > avail_w {
2874 let segments = split_multiline_fold(part, avail_w, fold_style);
2875 let mut first = true;
2876 for seg in segments {
2877 if first {
2878 lines.push(format!("{}{}", spaces(body_indent), seg));
2879 first = false;
2880 } else {
2881 lines.push(format!("{}{}", fold_prefix, seg));
2882 }
2883 }
2884 continue;
2885 }
2886 lines.push(format!("{}{}", spaces(body_indent), part));
2887 }
2888 lines.push(glyph);
2889 lines
2890 }
2891
2892 fn render_multiline_double_backtick(
2895 parts: &[&str],
2896 indent: usize,
2897 body_indent: usize,
2898 suffix: &str,
2899 fold_style: FoldStyle,
2900 wrap_width: Option<usize>,
2901 ) -> Vec<String> {
2902 let glyph = format!("{} ``{}", spaces(indent), suffix);
2903 let fold_prefix = format!("{}/ ", spaces(body_indent.saturating_sub(2)));
2904 let avail = wrap_width.map(|w| w.saturating_sub(body_indent + 2));
2906 let mut lines = vec![glyph.clone()];
2907 for part in parts {
2908 if fold_style != FoldStyle::None
2909 && let Some(avail_w) = avail
2910 && part.len() > avail_w {
2911 let segments = split_multiline_fold(part, avail_w, fold_style);
2912 let mut first = true;
2913 for seg in segments {
2914 if first {
2915 lines.push(format!("{}| {}", spaces(body_indent), seg));
2916 first = false;
2917 } else {
2918 lines.push(format!("{}{}", fold_prefix, seg));
2919 }
2920 }
2921 continue;
2922 }
2923 lines.push(format!("{}| {}", spaces(body_indent), part));
2924 }
2925 lines.push(glyph);
2926 lines
2927 }
2928
2929 #[allow(dead_code)]
2933 fn render_multiline_triple_backtick(parts: &[&str], indent: usize, suffix: &str) -> Vec<String> {
2934 let glyph = format!("{} ```{}", spaces(indent), suffix);
2935 let mut lines = vec![glyph.clone()];
2936 for part in parts {
2937 lines.push((*part).to_owned());
2938 }
2939 lines.push(glyph);
2940 lines
2941 }
2942
2943 fn render_inline_object_token(
2944 key: &str,
2945 value: &TjsonValue,
2946 options: &TjsonOptions,
2947 ) -> Result<Option<String>> {
2948 let Some(value_text) = Self::render_scalar_token(value, options)? else {
2949 return Ok(None);
2950 };
2951 Ok(Some(format!("{}:{}", render_key(key, options), value_text)))
2952 }
2953
2954 fn render_scalar_token(value: &TjsonValue, options: &TjsonOptions) -> Result<Option<String>> {
2955 let rendered = match value {
2956 TjsonValue::Null => "null".to_owned(),
2957 TjsonValue::Bool(value) => {
2958 if *value {
2959 "true".to_owned()
2960 } else {
2961 "false".to_owned()
2962 }
2963 }
2964 TjsonValue::Number(value) => value.to_string(),
2965 TjsonValue::String(value) => {
2966 if value.contains('\n') || value.contains('\r') {
2967 return Ok(None);
2968 }
2969 if options.bare_strings == BareStyle::Prefer && is_allowed_bare_string(value) {
2970 format!(" {}", value)
2971 } else {
2972 render_json_string(value)
2973 }
2974 }
2975 TjsonValue::Array(values) if values.is_empty() => "[]".to_owned(),
2976 TjsonValue::Object(entries) if entries.is_empty() => "{}".to_owned(),
2977 TjsonValue::Array(_) | TjsonValue::Object(_) => return Ok(None),
2978 };
2979
2980 Ok(Some(rendered))
2981 }
2982
2983 fn render_packed_array_lines(
2984 values: &[TjsonValue],
2985 first_prefix: String,
2986 continuation_indent: usize,
2987 options: &TjsonOptions,
2988 ) -> Result<Option<Vec<String>>> {
2989 if values.is_empty() {
2990 return Ok(Some(vec![format!("{first_prefix}[]")]));
2991 }
2992
2993 if values
2994 .iter()
2995 .all(|value| matches!(value, TjsonValue::String(_)))
2996 {
2997 return Self::render_string_array_lines(
2998 values,
2999 first_prefix,
3000 continuation_indent,
3001 options,
3002 );
3003 }
3004
3005 let tokens = Self::render_packed_array_tokens(values, options)?;
3006 Self::render_packed_token_lines(tokens, first_prefix, continuation_indent, false, options)
3007 }
3008
3009 fn render_string_array_lines(
3010 values: &[TjsonValue],
3011 first_prefix: String,
3012 continuation_indent: usize,
3013 options: &TjsonOptions,
3014 ) -> Result<Option<Vec<String>>> {
3015 match options.string_array_style {
3016 StringArrayStyle::None => Ok(None),
3017 StringArrayStyle::Spaces => {
3018 let tokens = Self::render_packed_array_tokens(values, options)?;
3019 Self::render_packed_token_lines(
3020 tokens,
3021 first_prefix,
3022 continuation_indent,
3023 true,
3024 options,
3025 )
3026 }
3027 StringArrayStyle::PreferSpaces => {
3028 let preferred = Self::render_packed_token_lines(
3029 Self::render_packed_array_tokens(values, options)?,
3030 first_prefix.clone(),
3031 continuation_indent,
3032 true,
3033 options,
3034 )?;
3035 let fallback = Self::render_packed_token_lines(
3036 Self::render_packed_array_tokens(values, options)?,
3037 first_prefix,
3038 continuation_indent,
3039 false,
3040 options,
3041 )?;
3042 Ok(pick_preferred_string_array_layout(
3043 preferred, fallback, options,
3044 ))
3045 }
3046 StringArrayStyle::Comma => {
3047 let tokens = Self::render_packed_array_tokens(values, options)?;
3048 Self::render_packed_token_lines(
3049 tokens,
3050 first_prefix,
3051 continuation_indent,
3052 false,
3053 options,
3054 )
3055 }
3056 StringArrayStyle::PreferComma => {
3057 let preferred = Self::render_packed_token_lines(
3058 Self::render_packed_array_tokens(values, options)?,
3059 first_prefix.clone(),
3060 continuation_indent,
3061 false,
3062 options,
3063 )?;
3064 let fallback = Self::render_packed_token_lines(
3065 Self::render_packed_array_tokens(values, options)?,
3066 first_prefix,
3067 continuation_indent,
3068 true,
3069 options,
3070 )?;
3071 Ok(pick_preferred_string_array_layout(
3072 preferred, fallback, options,
3073 ))
3074 }
3075 }
3076 }
3077
3078 fn render_packed_array_tokens(
3079 values: &[TjsonValue],
3080 options: &TjsonOptions,
3081 ) -> Result<Vec<PackedToken>> {
3082 let mut tokens = Vec::new();
3083 for value in values {
3084 let token = match value {
3085 TjsonValue::String(text) if text.contains('\n') || text.contains('\r') => {
3087 PackedToken::Block(value.clone())
3088 }
3089 TjsonValue::Array(vals) if !vals.is_empty() => PackedToken::Block(value.clone()),
3091 TjsonValue::Object(entries) if !entries.is_empty() => {
3092 PackedToken::Block(value.clone())
3093 }
3094 TjsonValue::String(text) => {
3096 let token_str = if text.chars().any(is_comma_like) {
3097 render_json_string(text)
3098 } else {
3099 Self::render_scalar_token(value, options)?
3100 .expect("non-multiline string always renders as scalar token")
3101 };
3102 PackedToken::Inline(token_str, value.clone())
3103 }
3104 _ => {
3106 let token_str = Self::render_scalar_token(value, options)?
3107 .expect("scalar always renders as inline token");
3108 PackedToken::Inline(token_str, value.clone())
3109 }
3110 };
3111 tokens.push(token);
3112 }
3113 Ok(tokens)
3114 }
3115
3116 fn fold_packed_inline(
3120 value: &TjsonValue,
3121 continuation_indent: usize,
3122 first_line_extra: usize,
3123 options: &TjsonOptions,
3124 ) -> Result<Option<Vec<String>>> {
3125 match value {
3126 TjsonValue::String(s) => {
3127 let lines =
3128 Self::render_string_lines(s, continuation_indent, first_line_extra, options)?;
3129 Ok(if lines.len() > 1 { Some(lines) } else { None })
3130 }
3131 TjsonValue::Number(n) => {
3132 let ns = n.to_string();
3133 Ok(
3134 fold_number(
3135 &ns,
3136 continuation_indent,
3137 first_line_extra,
3138 options.number_fold_style,
3139 options.wrap_width,
3140 )
3141 .filter(|l| l.len() > 1),
3142 )
3143 }
3144 _ => Ok(None),
3145 }
3146 }
3147
3148 fn render_packed_token_lines(
3149 tokens: Vec<PackedToken>,
3150 first_prefix: String,
3151 continuation_indent: usize,
3152 string_spaces_mode: bool,
3153 options: &TjsonOptions,
3154 ) -> Result<Option<Vec<String>>> {
3155 if tokens.is_empty() {
3156 return Ok(Some(vec![first_prefix]));
3157 }
3158
3159 if string_spaces_mode && tokens.iter().any(|t| matches!(t, PackedToken::Block(_))) {
3161 return Ok(None);
3162 }
3163
3164 let separator = if string_spaces_mode { " " } else { ", " };
3165 let continuation_prefix = spaces(continuation_indent);
3166
3167 let mut current = first_prefix.clone();
3170 let mut current_is_fresh = true;
3171 let mut lines: Vec<String> = Vec::new();
3172
3173 for token in tokens {
3174 match token {
3175 PackedToken::Block(value) => {
3176 if !current_is_fresh {
3178 if !string_spaces_mode {
3179 current.push(',');
3180 }
3181 lines.push(current);
3182 }
3183
3184 let block_lines = match &value {
3185 TjsonValue::String(s) => {
3186 Self::render_string_lines(s, continuation_indent, 0, options)?
3187 }
3188 TjsonValue::Array(vals) if !vals.is_empty() => {
3189 Self::render_explicit_array(vals, continuation_indent, options)?
3190 }
3191 TjsonValue::Object(entries) if !entries.is_empty() => {
3192 Self::render_explicit_object(entries, continuation_indent, options)?
3193 }
3194 _ => unreachable!("PackedToken::Block must contain a block value"),
3195 };
3196
3197 let current_prefix_str = if lines.is_empty() {
3201 first_prefix.clone()
3202 } else {
3203 continuation_prefix.clone()
3204 };
3205 let first_block_content =
3206 block_lines[0].get(continuation_indent..).unwrap_or("");
3207 lines.push(format!("{}{}", current_prefix_str, first_block_content));
3208 for bl in block_lines.into_iter().skip(1) {
3209 lines.push(bl);
3210 }
3211
3212 current = continuation_prefix.clone();
3213 current_is_fresh = true;
3214 }
3215 PackedToken::Inline(token_str, value) => {
3216 if current_is_fresh {
3217 current.push_str(&token_str);
3219 current_is_fresh = false;
3220
3221 if !fits_wrap(options, ¤t) {
3223 let first_line_extra = if lines.is_empty() {
3224 first_prefix.len().saturating_sub(continuation_indent)
3225 } else {
3226 0
3227 };
3228 if let Some(fold_lines) = Self::fold_packed_inline(
3229 &value,
3230 continuation_indent,
3231 first_line_extra,
3232 options,
3233 )? {
3234 let actual_prefix = if lines.is_empty() {
3236 first_prefix.clone()
3237 } else {
3238 continuation_prefix.clone()
3239 };
3240 let first_content =
3241 fold_lines[0].get(continuation_indent..).unwrap_or("");
3242 lines.push(format!("{}{}", actual_prefix, first_content));
3243 for fl in fold_lines.into_iter().skip(1) {
3244 lines.push(fl);
3245 }
3246 current = continuation_prefix.clone();
3247 current_is_fresh = true;
3248 }
3249 }
3251 } else {
3252 let candidate = format!("{current}{separator}{token_str}");
3254 if fits_wrap(options, &candidate) {
3255 current = candidate;
3256 } else {
3257 if !string_spaces_mode {
3259 current.push(',');
3260 }
3261 lines.push(current);
3262 current = format!("{}{}", continuation_prefix, token_str);
3263 current_is_fresh = false;
3264
3265 if !fits_wrap(options, ¤t)
3267 && let Some(fold_lines) = Self::fold_packed_inline(
3268 &value,
3269 continuation_indent,
3270 0,
3271 options,
3272 )? {
3273 let first_content =
3274 fold_lines[0].get(continuation_indent..).unwrap_or("");
3275 lines.push(format!(
3276 "{}{}",
3277 continuation_prefix, first_content
3278 ));
3279 for fl in fold_lines.into_iter().skip(1) {
3280 lines.push(fl);
3281 }
3282 current = continuation_prefix.clone();
3283 current_is_fresh = true;
3284 }
3285 }
3287 }
3288 }
3289 }
3290 }
3291
3292 if !current_is_fresh {
3293 lines.push(current);
3294 }
3295
3296 Ok(Some(lines))
3297 }
3298
3299 fn render_table(
3300 values: &[TjsonValue],
3301 parent_indent: usize,
3302 options: &TjsonOptions,
3303 ) -> Result<Option<Vec<String>>> {
3304 if values.len() < options.table_min_rows {
3305 return Ok(None);
3306 }
3307
3308 let mut columns = Vec::<String>::new();
3309 let mut present_cells = 0usize;
3310
3311 let mut first_row_keys: Option<Vec<&str>> = None;
3315
3316 for value in values {
3317 let TjsonValue::Object(entries) = value else {
3318 return Ok(None);
3319 };
3320 present_cells += entries.len();
3321 for (key, cell) in entries {
3322 if matches!(cell, TjsonValue::Array(inner) if !inner.is_empty())
3323 || matches!(cell, TjsonValue::Object(inner) if !inner.is_empty())
3324 || matches!(cell, TjsonValue::String(text) if text.contains('\n') || text.contains('\r'))
3325 {
3326 return Ok(None);
3327 }
3328 if !columns.iter().any(|column| column == key) {
3329 columns.push(key.clone());
3330 }
3331 }
3332 let row_keys: Vec<&str> = entries.iter().map(|(k, _)| k.as_str()).collect();
3334 if let Some(ref first) = first_row_keys {
3335 let shared_in_first: Vec<&str> = first.iter().copied().filter(|k| row_keys.contains(k)).collect();
3336 let shared_in_row: Vec<&str> = row_keys.iter().copied().filter(|k| first.contains(k)).collect();
3337 if shared_in_first != shared_in_row {
3338 return Ok(None);
3339 }
3340 } else {
3341 first_row_keys = Some(row_keys);
3342 }
3343 }
3344
3345 if columns.len() < options.table_min_columns {
3346 return Ok(None);
3347 }
3348
3349 let similarity = present_cells as f32 / (values.len() * columns.len()) as f32;
3350 if similarity < options.table_min_similarity {
3351 return Ok(None);
3352 }
3353
3354 let mut header_cells = Vec::new();
3355 let mut rows = Vec::new();
3356 for column in &columns {
3357 header_cells.push(render_key(column, options));
3358 }
3359
3360 for value in values {
3361 let TjsonValue::Object(entries) = value else {
3362 return Ok(None);
3363 };
3364 let mut row: Vec<String> = Vec::new();
3365 for column in &columns {
3366 let token = if let Some((_, value)) = entries.iter().find(|(key, _)| key == column)
3367 {
3368 Self::render_table_cell_token(value, options)?
3369 } else {
3370 None
3371 };
3372 row.push(token.unwrap_or_default());
3373 }
3374 rows.push(row);
3375 }
3376
3377 let mut widths = vec![0usize; columns.len()];
3378 for (index, header) in header_cells.iter().enumerate() {
3379 widths[index] = header.len();
3380 }
3381 for row in &rows {
3382 for (index, cell) in row.iter().enumerate() {
3383 widths[index] = widths[index].max(cell.len());
3384 }
3385 }
3386 if let Some(col_max) = options.table_column_max_width
3388 && widths.iter().any(|w| *w > col_max) {
3389 return Ok(None);
3390 }
3391 for width in &mut widths {
3392 *width += 2;
3393 }
3394
3395 if let Some(w) = options.wrap_width {
3400 let min_row_width = 2 + widths.iter().sum::<usize>() + widths.len() + 1;
3404 if min_row_width > w {
3405 return Ok(None);
3406 }
3407 }
3408
3409 let indent = spaces(parent_indent + 2);
3410 let mut lines = Vec::new();
3411 lines.push(format!(
3412 "{}{}",
3413 indent,
3414 header_cells
3415 .iter()
3416 .zip(widths.iter())
3417 .map(|(cell, width)| format!("|{cell:<width$}", width = *width))
3418 .collect::<String>()
3419 + "|"
3420 ));
3421
3422 let pair_indent = parent_indent; let fold_prefix = spaces(pair_indent);
3425
3426 for row in rows {
3427 let row_line = format!(
3428 "{}{}",
3429 indent,
3430 row.iter()
3431 .zip(widths.iter())
3432 .map(|(cell, width)| format!("|{cell:<width$}", width = *width))
3433 .collect::<String>()
3434 + "|"
3435 );
3436
3437 if options.table_fold {
3438 let fold_avail = options
3443 .wrap_width
3444 .unwrap_or(usize::MAX)
3445 .saturating_sub(pair_indent + 2); if row_line.len() > fold_avail + pair_indent + 2 {
3447 if let Some((before, after)) = split_table_row_for_fold(&row_line, fold_avail + pair_indent + 2) {
3451 lines.push(before);
3452 lines.push(format!("{}\\ {}", fold_prefix, after));
3453 continue;
3454 }
3455 }
3456 }
3457
3458 lines.push(row_line);
3459 }
3460
3461 Ok(Some(lines))
3462 }
3463
3464 fn render_table_cell_token(
3465 value: &TjsonValue,
3466 options: &TjsonOptions,
3467 ) -> Result<Option<String>> {
3468 Ok(match value {
3469 TjsonValue::Null => Some("null".to_owned()),
3470 TjsonValue::Bool(value) => Some(if *value {
3471 "true".to_owned()
3472 } else {
3473 "false".to_owned()
3474 }),
3475 TjsonValue::Number(value) => Some(value.to_string()),
3476 TjsonValue::String(value) => {
3477 if value.contains('\n') || value.contains('\r') {
3478 None
3479 } else if options.bare_strings == BareStyle::Prefer
3480 && is_allowed_bare_string(value)
3481 && !is_reserved_word(value) && !value.contains('|')
3484 && value.chars().find(|c| is_pipe_like(*c)).is_none()
3485 {
3486 Some(format!(" {}", value))
3487 } else {
3488 Some(render_json_string(value))
3489 }
3490 }
3491 TjsonValue::Array(values) if values.is_empty() => Some("[]".to_owned()),
3492 TjsonValue::Object(entries) if entries.is_empty() => Some("{}".to_owned()),
3493 _ => None,
3494 })
3495 }
3496}
3497
3498fn normalize_input(input: &str) -> std::result::Result<String, ParseError> {
3499 let mut normalized = String::with_capacity(input.len());
3500 let mut line = 1;
3501 let mut column = 1;
3502 let mut chars = input.chars().peekable();
3503 while let Some(ch) = chars.next() {
3504 if ch == '\r' {
3505 if chars.peek() == Some(&'\n') {
3506 chars.next();
3507 normalized.push('\n');
3508 line += 1;
3509 column = 1;
3510 continue;
3511 }
3512 return Err(ParseError::new(
3513 line,
3514 column,
3515 "bare carriage returns are not valid",
3516 None,
3517 ));
3518 }
3519 if is_forbidden_literal_tjson_char(ch) {
3520 return Err(ParseError::new(
3521 line,
3522 column,
3523 format!("forbidden character U+{:04X} must be escaped", ch as u32),
3524 None,
3525 ));
3526 }
3527 normalized.push(ch);
3528 if ch == '\n' {
3529 line += 1;
3530 column = 1;
3531 } else {
3532 column += 1;
3533 }
3534 }
3535 Ok(normalized)
3536}
3537
3538fn expand_indent_adjustments(input: &str) -> String {
3550 if !input.contains(" /<") {
3551 return input.to_owned();
3552 }
3553
3554 let mut output_lines: Vec<String> = Vec::with_capacity(input.lines().count() + 4);
3555 let mut offset_stack: Vec<(usize, usize)> = vec![(0, usize::MAX)];
3559 let mut pending_key_line: Option<String> = None;
3562
3563 for raw_line in input.split('\n') {
3564 let (current_offset, expected_close) = *offset_stack.last().unwrap();
3565
3566 if offset_stack.len() > 1
3569 && raw_line.len() == expected_close + 3
3570 && raw_line[..expected_close].bytes().all(|b| b == b' ')
3571 && &raw_line[expected_close..] == " />"
3572 {
3573 if let Some(held) = pending_key_line.take() { output_lines.push(held); }
3574 offset_stack.pop();
3575 continue; }
3577
3578 let trimmed = raw_line.trim_end();
3581 if let Some(ref held) = pending_key_line {
3582 let key_file_indent = count_leading_spaces(held);
3583 if trimmed.len() == key_file_indent + 3
3584 && trimmed[..key_file_indent].bytes().all(|b| b == b' ')
3585 && &trimmed[key_file_indent..] == " /<"
3586 {
3587 let eff_indent = key_file_indent + current_offset;
3589 let content = &held[key_file_indent..]; output_lines.push(format!("{}{}", spaces(eff_indent), content));
3591 offset_stack.push((eff_indent, key_file_indent));
3592 pending_key_line = None;
3593 continue;
3594 }
3595 output_lines.push(pending_key_line.take().unwrap());
3597 }
3598
3599 let trimmed_end = trimmed;
3603 if let Some(without_glyph) = trimmed_end.strip_suffix(" /<")
3604 && without_glyph.trim_end().ends_with(':') {
3605 let file_indent = count_leading_spaces(raw_line);
3606 let eff_indent = file_indent + current_offset;
3607 let content = &without_glyph[file_indent..];
3608 output_lines.push(format!("{}{}", spaces(eff_indent), content));
3609 offset_stack.push((eff_indent, file_indent));
3610 continue;
3611 }
3612
3613 if trimmed_end.ends_with(':') && !trimmed_end.trim_start().contains(' ') {
3616 let held = if current_offset == 0 || raw_line.trim().is_empty() {
3618 raw_line.to_owned()
3619 } else {
3620 let file_indent = count_leading_spaces(raw_line);
3621 let eff_indent = file_indent + current_offset;
3622 let content = &raw_line[file_indent..];
3623 format!("{}{}", spaces(eff_indent), content)
3624 };
3625 pending_key_line = Some(held);
3626 continue;
3627 }
3628
3629 if current_offset == 0 || raw_line.trim().is_empty() {
3631 output_lines.push(raw_line.to_owned());
3632 } else {
3633 let file_indent = count_leading_spaces(raw_line);
3634 let eff_indent = file_indent + current_offset;
3635 let content = &raw_line[file_indent..];
3636 output_lines.push(format!("{}{}", spaces(eff_indent), content));
3637 }
3638 }
3639 if let Some(held) = pending_key_line.take() { output_lines.push(held); }
3641
3642 output_lines.join("\n")
3646}
3647
3648fn count_leading_spaces(line: &str) -> usize {
3649 line.bytes().take_while(|byte| *byte == b' ').count()
3650}
3651
3652fn spaces(count: usize) -> String {
3653 " ".repeat(count)
3654}
3655
3656fn effective_inline_objects(options: &TjsonOptions) -> bool {
3657 options.inline_objects
3658}
3659
3660fn effective_inline_arrays(options: &TjsonOptions) -> bool {
3661 options.inline_arrays
3662}
3663
3664fn effective_force_markers(options: &TjsonOptions) -> bool {
3665 options.force_markers
3666}
3667
3668fn effective_tables(options: &TjsonOptions) -> bool {
3669 options.tables
3670}
3671
3672fn table_unindent_target(pair_indent: usize, natural_lines: &[String], options: &TjsonOptions) -> Option<usize> {
3677 let n = pair_indent;
3678 let max_natural = natural_lines.iter().map(|l| l.len()).max().unwrap_or(0);
3679 let data_width = max_natural.saturating_sub(n + 2);
3681
3682 match options.table_unindent_style {
3683 TableUnindentStyle::None => None,
3684
3685 TableUnindentStyle::Left => {
3686 if n == 0 { None } else {
3688 let fits = options.wrap_width.map(|w| data_width <= w).unwrap_or(true);
3690 if fits { Some(0) } else { None }
3691 }
3692 }
3693
3694 TableUnindentStyle::Auto => {
3695 let w = options.wrap_width?;
3698 let overflows_natural = max_natural > w;
3699 let fits_at_zero = data_width <= w;
3700 if overflows_natural && fits_at_zero { Some(0) } else { None }
3701 }
3702
3703 TableUnindentStyle::Floating => {
3704 let w = options.wrap_width?;
3707 if max_natural <= w {
3708 return None; }
3710 if data_width + 2 <= w {
3714 let target = w.saturating_sub(data_width + 2);
3716 if target < n { Some(target) } else { None }
3718 } else {
3719 None }
3721 }
3722 }
3723}
3724
3725fn subtree_line_count(value: &TjsonValue) -> usize {
3728 match value {
3729 TjsonValue::Array(v) if !v.is_empty() => v.iter().map(subtree_line_count).sum::<usize>() + 1,
3730 TjsonValue::Object(e) if !e.is_empty() => {
3731 e.iter().map(|(_, v)| subtree_line_count(v) + 1).sum()
3732 }
3733 _ => 1,
3734 }
3735}
3736
3737fn subtree_byte_count(value: &TjsonValue) -> usize {
3739 match value {
3740 TjsonValue::String(s) => s.len(),
3741 TjsonValue::Number(n) => n.to_string().len(),
3742 TjsonValue::Bool(b) => if *b { 4 } else { 5 },
3743 TjsonValue::Null => 4,
3744 TjsonValue::Array(v) => v.iter().map(subtree_byte_count).sum(),
3745 TjsonValue::Object(e) => e.iter().map(|(k, v)| k.len() + subtree_byte_count(v)).sum(),
3746 }
3747}
3748
3749fn subtree_max_depth(value: &TjsonValue) -> usize {
3752 match value {
3753 TjsonValue::Array(v) if !v.is_empty() => {
3754 1 + v.iter().map(subtree_max_depth).max().unwrap_or(0)
3755 }
3756 TjsonValue::Object(e) if !e.is_empty() => {
3757 1 + e.iter().map(|(_, v)| subtree_max_depth(v)).max().unwrap_or(0)
3758 }
3759 _ => 0,
3760 }
3761}
3762
3763fn should_use_indent_glyph(value: &TjsonValue, pair_indent: usize, options: &TjsonOptions) -> bool {
3765 let Some(w) = options.wrap_width else { return false; };
3766 let fold_floor = || {
3767 let max_depth = subtree_max_depth(value);
3768 pair_indent + max_depth * 2 >= w.saturating_sub(MIN_FOLD_CONTINUATION + 2)
3769 };
3770 match indent_glyph_mode(options) {
3771 IndentGlyphMode::None => false,
3772 IndentGlyphMode::Fixed => pair_indent >= w / 2,
3773 IndentGlyphMode::IndentWeighted(threshold) => {
3774 if fold_floor() { return true; }
3775 let line_count = subtree_line_count(value);
3776 (pair_indent * line_count) as f64 >= threshold * (w * w) as f64
3777 }
3778 IndentGlyphMode::ByteWeighted(threshold) => {
3779 if fold_floor() { return true; }
3780 let byte_count = subtree_byte_count(value);
3781 (pair_indent * byte_count) as f64 >= threshold * (w * w) as f64
3782 }
3783 }
3784}
3785
3786fn indent_glyph_open_lines(key_line: &str, pair_indent: usize, options: &TjsonOptions) -> Vec<String> {
3789 match options.indent_glyph_marker_style {
3790 IndentGlyphMarkerStyle::Compact => vec![format!("{}: /<", key_line)],
3791 IndentGlyphMarkerStyle::Separate => vec![
3792 format!("{}:", key_line),
3793 format!("{} /<", spaces(pair_indent)),
3794 ],
3795 }
3796}
3797
3798fn fits_wrap(options: &TjsonOptions, line: &str) -> bool {
3799 match options.wrap_width {
3800 Some(0) | None => true,
3801 Some(width) => line.chars().count() <= width,
3802 }
3803}
3804
3805fn pick_preferred_string_array_layout(
3806 preferred: Option<Vec<String>>,
3807 fallback: Option<Vec<String>>,
3808 options: &TjsonOptions,
3809) -> Option<Vec<String>> {
3810 match (preferred, fallback) {
3811 (Some(preferred), Some(fallback))
3812 if string_array_layout_score(&fallback, options)
3813 < string_array_layout_score(&preferred, options) =>
3814 {
3815 Some(fallback)
3816 }
3817 (Some(preferred), _) => Some(preferred),
3818 (None, fallback) => fallback,
3819 }
3820}
3821
3822fn string_array_layout_score(lines: &[String], options: &TjsonOptions) -> (usize, usize, usize) {
3823 let overflow = match options.wrap_width {
3824 Some(0) | None => 0,
3825 Some(width) => lines
3826 .iter()
3827 .map(|line| line.chars().count().saturating_sub(width))
3828 .sum(),
3829 };
3830 let max_width = lines
3831 .iter()
3832 .map(|line| line.chars().count())
3833 .max()
3834 .unwrap_or(0);
3835 (overflow, lines.len(), max_width)
3836}
3837
3838fn starts_with_marker_chain(content: &str) -> bool {
3839 content.starts_with("[ ") || content.starts_with("{ ")
3840}
3841
3842fn parse_json_string_prefix(content: &str) -> Option<(String, usize)> {
3843 if !content.starts_with('"') {
3844 return None;
3845 }
3846 let mut escaped = false;
3847 let mut end = None;
3848 for (index, ch) in content.char_indices().skip(1) {
3849 if escaped {
3850 escaped = false;
3851 continue;
3852 }
3853 match ch {
3854 '\\' => escaped = true,
3855 '"' => {
3856 end = Some(index + '"'.len_utf8());
3857 break;
3858 }
3859 '\n' | '\r' => return None,
3860 _ => {}
3861 }
3862 }
3863 let end = end?;
3864 let json_src = if content[..end].contains('\t') {
3866 std::borrow::Cow::Owned(content[..end].replace('\t', "\\t"))
3867 } else {
3868 std::borrow::Cow::Borrowed(&content[..end])
3869 };
3870 let parsed = serde_json::from_str(&json_src).ok()?;
3871 Some((parsed, end))
3872}
3873
3874fn split_pipe_cells(row: &str) -> Option<Vec<String>> {
3875 if !row.starts_with('|') {
3876 return None;
3877 }
3878 let mut cells = Vec::new();
3879 let mut current = String::new();
3880 let mut in_string = false;
3881 let mut escaped = false;
3882
3883 for ch in row.chars() {
3884 if in_string {
3885 current.push(ch);
3886 if escaped {
3887 escaped = false;
3888 continue;
3889 }
3890 match ch {
3891 '\\' => escaped = true,
3892 '"' => in_string = false,
3893 _ => {}
3894 }
3895 continue;
3896 }
3897
3898 match ch {
3899 '"' => {
3900 in_string = true;
3901 current.push(ch);
3902 }
3903 '|' => {
3904 cells.push(std::mem::take(&mut current));
3905 }
3906 _ => current.push(ch),
3907 }
3908 }
3909
3910 if in_string || escaped {
3911 return None;
3912 }
3913
3914 cells.push(current);
3915 Some(cells)
3916}
3917
3918fn is_minimal_json_candidate(content: &str) -> bool {
3919 let bytes = content.as_bytes();
3920 if bytes.len() < 2 {
3921 return false;
3922 }
3923 (bytes[0] == b'{' && bytes[1] != b'}' && bytes[1] != b' ')
3924 || (bytes[0] == b'[' && bytes[1] != b']' && bytes[1] != b' ')
3925}
3926
3927fn is_valid_minimal_json(content: &str) -> Result<(), usize> {
3928 let mut in_string = false;
3929 let mut escaped = false;
3930
3931 for (col, ch) in content.chars().enumerate() {
3932 if in_string {
3933 if escaped {
3934 escaped = false;
3935 continue;
3936 }
3937 match ch {
3938 '\\' => escaped = true,
3939 '"' => in_string = false,
3940 _ => {}
3941 }
3942 continue;
3943 }
3944
3945 match ch {
3946 '"' => in_string = true,
3947 ch if ch.is_whitespace() => return Err(col),
3948 _ => {}
3949 }
3950 }
3951
3952 if in_string || escaped { Err(content.len()) } else { Ok(()) }
3953}
3954
3955fn bare_string_end(content: &str, context: ArrayLineValueContext) -> usize {
3956 match context {
3957 ArrayLineValueContext::ArrayLine => {
3958 let mut end = content.len();
3959 if let Some(index) = content.find(" ") {
3960 end = end.min(index);
3961 }
3962 if let Some(index) = content.find(", ") {
3963 end = end.min(index);
3964 }
3965 if content.ends_with(',') {
3966 end = end.min(content.len() - 1);
3967 }
3968 end
3969 }
3970 ArrayLineValueContext::ObjectValue => content.find(" ").unwrap_or(content.len()),
3971 ArrayLineValueContext::SingleValue => content.len(),
3972 }
3973}
3974
3975fn simple_token_end(content: &str, context: ArrayLineValueContext) -> usize {
3976 match context {
3977 ArrayLineValueContext::ArrayLine => {
3978 let mut end = content.len();
3979 if let Some(index) = content.find(", ") {
3980 end = end.min(index);
3981 }
3982 if let Some(index) = content.find(" ") {
3983 end = end.min(index);
3984 }
3985 if content.ends_with(',') {
3986 end = end.min(content.len() - 1);
3987 }
3988 end
3989 }
3990 ArrayLineValueContext::ObjectValue => content.find(" ").unwrap_or(content.len()),
3991 ArrayLineValueContext::SingleValue => content.len(),
3992 }
3993}
3994
3995fn detect_multiline_local_eol(value: &str) -> Option<MultilineLocalEol> {
3996 let bytes = value.as_bytes();
3997 let mut index = 0usize;
3998 let mut saw_lf = false;
3999 let mut saw_crlf = false;
4000
4001 while index < bytes.len() {
4002 match bytes[index] {
4003 b'\r' => {
4004 if bytes.get(index + 1) == Some(&b'\n') {
4005 saw_crlf = true;
4006 index += 2;
4007 } else {
4008 return None;
4009 }
4010 }
4011 b'\n' => {
4012 saw_lf = true;
4013 index += 1;
4014 }
4015 _ => index += 1,
4016 }
4017 }
4018
4019 match (saw_lf, saw_crlf) {
4020 (false, false) => None,
4021 (true, false) => Some(MultilineLocalEol::Lf),
4022 (false, true) => Some(MultilineLocalEol::CrLf),
4023 (true, true) => None,
4024 }
4025}
4026
4027fn parse_bare_key_prefix(content: &str) -> Option<usize> {
4028 let mut chars = content.char_indices().peekable();
4029 let (_, first) = chars.next()?;
4030 if !is_unicode_letter_or_number(first) {
4031 return None;
4032 }
4033 let mut end = first.len_utf8();
4034
4035 let mut previous_space = false;
4036 for (index, ch) in chars {
4037 if is_unicode_letter_or_number(ch)
4038 || matches!(
4039 ch,
4040 '_' | '(' | ')' | '/' | '\'' | '.' | '!' | '%' | '&' | ',' | '-'
4041 )
4042 {
4043 previous_space = false;
4044 end = index + ch.len_utf8();
4045 continue;
4046 }
4047 if ch == ' ' && !previous_space {
4048 previous_space = true;
4049 end = index + ch.len_utf8();
4050 continue;
4051 }
4052 break;
4053 }
4054
4055 let candidate = &content[..end];
4056 let last = candidate.chars().next_back()?;
4057 if last == ' ' || is_comma_like(last) || is_quote_like(last) {
4058 return None;
4059 }
4060 Some(end)
4061}
4062
4063fn render_key(key: &str, options: &TjsonOptions) -> String {
4064 if options.bare_keys == BareStyle::Prefer
4065 && parse_bare_key_prefix(key).is_some_and(|end| end == key.len())
4066 {
4067 key.to_owned()
4068 } else {
4069 render_json_string(key)
4070 }
4071}
4072
4073fn is_allowed_bare_string(value: &str) -> bool {
4074 if value.is_empty() {
4075 return false;
4076 }
4077 let first = value.chars().next().unwrap();
4078 let last = value.chars().next_back().unwrap();
4079 if first == ' '
4080 || last == ' '
4081 || first == '/'
4082 || is_pipe_like(first)
4084 || is_quote_like(first)
4085 || is_quote_like(last)
4086 || is_comma_like(first)
4087 || is_comma_like(last)
4088 {
4089 return false;
4090 }
4091 let mut previous_space = false;
4092 for ch in value.chars() {
4093 if ch != ' ' && is_forbidden_bare_char(ch) {
4094 return false;
4095 }
4096 if ch == ' ' {
4097 if previous_space {
4098 return false;
4099 }
4100 previous_space = true;
4101 } else {
4102 previous_space = false;
4103 }
4104 }
4105 true
4106}
4107
4108fn needs_explicit_array_marker(value: &TjsonValue) -> bool {
4109 matches!(value, TjsonValue::Array(values) if !values.is_empty())
4110 || matches!(value, TjsonValue::Object(entries) if !entries.is_empty())
4111}
4112
4113fn is_unicode_letter_or_number(ch: char) -> bool {
4114 matches!(
4115 get_general_category(ch),
4116 GeneralCategory::UppercaseLetter
4117 | GeneralCategory::LowercaseLetter
4118 | GeneralCategory::TitlecaseLetter
4119 | GeneralCategory::ModifierLetter
4120 | GeneralCategory::OtherLetter
4121 | GeneralCategory::DecimalNumber
4122 | GeneralCategory::LetterNumber
4123 | GeneralCategory::OtherNumber
4124 )
4125}
4126
4127fn is_forbidden_literal_tjson_char(ch: char) -> bool {
4128 is_forbidden_control_char(ch)
4129 || is_default_ignorable_code_point(ch)
4130 || is_private_use_code_point(ch)
4131 || is_noncharacter_code_point(ch)
4132 || matches!(ch, '\u{2028}' | '\u{2029}')
4133}
4134
4135fn is_forbidden_bare_char(ch: char) -> bool {
4136 if is_forbidden_literal_tjson_char(ch) {
4137 return true;
4138 }
4139 matches!(
4140 get_general_category(ch),
4141 GeneralCategory::Control
4142 | GeneralCategory::Format
4143 | GeneralCategory::Unassigned
4144 | GeneralCategory::SpaceSeparator
4145 | GeneralCategory::LineSeparator
4146 | GeneralCategory::ParagraphSeparator
4147 | GeneralCategory::NonspacingMark
4148 | GeneralCategory::SpacingMark
4149 | GeneralCategory::EnclosingMark
4150 )
4151}
4152
4153fn is_forbidden_control_char(ch: char) -> bool {
4154 matches!(
4155 ch,
4156 '\u{0000}'..='\u{0008}'
4157 | '\u{000B}'..='\u{000C}'
4158 | '\u{000E}'..='\u{001F}'
4159 | '\u{007F}'..='\u{009F}'
4160 )
4161}
4162
4163fn is_default_ignorable_code_point(ch: char) -> bool {
4164 matches!(get_general_category(ch), GeneralCategory::Format)
4165 || matches!(
4166 ch,
4167 '\u{034F}'
4168 | '\u{115F}'..='\u{1160}'
4169 | '\u{17B4}'..='\u{17B5}'
4170 | '\u{180B}'..='\u{180F}'
4171 | '\u{3164}'
4172 | '\u{FE00}'..='\u{FE0F}'
4173 | '\u{FFA0}'
4174 | '\u{1BCA0}'..='\u{1BCA3}'
4175 | '\u{1D173}'..='\u{1D17A}'
4176 | '\u{E0000}'
4177 | '\u{E0001}'
4178 | '\u{E0020}'..='\u{E007F}'
4179 | '\u{E0100}'..='\u{E01EF}'
4180 )
4181}
4182
4183fn is_private_use_code_point(ch: char) -> bool {
4184 matches!(get_general_category(ch), GeneralCategory::PrivateUse)
4185}
4186
4187fn is_noncharacter_code_point(ch: char) -> bool {
4188 let code_point = ch as u32;
4189 (0xFDD0..=0xFDEF).contains(&code_point)
4190 || (code_point <= 0x10FFFF && (code_point & 0xFFFE) == 0xFFFE)
4191}
4192
4193fn render_json_string(value: &str) -> String {
4194 let mut rendered = String::with_capacity(value.len() + 2);
4195 rendered.push('"');
4196 for ch in value.chars() {
4197 match ch {
4198 '"' => rendered.push_str("\\\""),
4199 '\\' => rendered.push_str("\\\\"),
4200 '\u{0008}' => rendered.push_str("\\b"),
4201 '\u{000C}' => rendered.push_str("\\f"),
4202 '\n' => rendered.push_str("\\n"),
4203 '\r' => rendered.push_str("\\r"),
4204 '\t' => rendered.push_str("\\t"),
4205 ch if ch <= '\u{001F}' || is_forbidden_literal_tjson_char(ch) => {
4206 push_json_unicode_escape(&mut rendered, ch);
4207 }
4208 _ => rendered.push(ch),
4209 }
4210 }
4211 rendered.push('"');
4212 rendered
4213}
4214
4215fn push_json_unicode_escape(rendered: &mut String, ch: char) {
4216 let code_point = ch as u32;
4217 if code_point <= 0xFFFF {
4218 rendered.push_str(&format!("\\u{:04x}", code_point));
4219 return;
4220 }
4221
4222 let scalar = code_point - 0x1_0000;
4223 let high = 0xD800 + ((scalar >> 10) & 0x3FF);
4224 let low = 0xDC00 + (scalar & 0x3FF);
4225 rendered.push_str(&format!("\\u{:04x}\\u{:04x}", high, low));
4226}
4227
4228fn line_starts_with_ws_then(line: &str, ch: char) -> bool {
4230 let trimmed = line.trim_start_matches(|c: char| c.is_whitespace());
4231 trimmed.starts_with(ch)
4232}
4233
4234fn split_multiline_fold(text: &str, avail: usize, style: FoldStyle) -> Vec<&str> {
4238 if text.len() <= avail || avail == 0 {
4239 return vec![text];
4240 }
4241 let mut segments = Vec::new();
4242 let mut rest = text;
4243 loop {
4244 if rest.len() <= avail {
4245 segments.push(rest);
4246 break;
4247 }
4248 let split_at = match style {
4249 FoldStyle::Auto => {
4250 let candidate = &rest[..avail.min(rest.len())];
4254 if let Some(pos) = candidate.rfind(' ') {
4256 if pos > 0 { pos } else { avail.min(rest.len()) }
4257 } else {
4258 avail.min(rest.len())
4259 }
4260 }
4261 FoldStyle::Fixed | FoldStyle::None => avail.min(rest.len()),
4262 };
4263 let safe = safe_json_split(rest, split_at);
4266 segments.push(&rest[..safe]);
4267 rest = &rest[safe..];
4268 if rest.is_empty() {
4269 break;
4270 }
4271 }
4272 segments
4273}
4274
4275fn safe_json_split(s: &str, split_at: usize) -> usize {
4279 let bytes = s.as_bytes();
4281 let pos = split_at.min(bytes.len());
4282 let mut backslashes = 0usize;
4284 let mut i = pos;
4285 while i > 0 && bytes[i - 1] == b'\\' {
4286 backslashes += 1;
4287 i -= 1;
4288 }
4289 if backslashes % 2 == 1 {
4290 pos.saturating_sub(1)
4292 } else {
4293 pos
4294 }
4295}
4296
4297fn fold_bare_string(
4302 value: &str,
4303 indent: usize,
4304 first_line_extra: usize,
4305 style: FoldStyle,
4306 wrap_width: Option<usize>,
4307) -> Option<Vec<String>> {
4308 let w = wrap_width?;
4309 let first_avail = w.saturating_sub(indent + 1 + first_line_extra);
4312 if value.len() <= first_avail {
4313 return None; }
4315 let cont_avail = w.saturating_sub(indent + 2);
4317 if cont_avail < MIN_FOLD_CONTINUATION {
4318 return None; }
4320 let mut lines = Vec::new();
4321 let mut rest = value;
4322 let mut first = true;
4323 let avail = if first { first_avail } else { cont_avail };
4324 let _ = avail;
4325 let mut current_avail = first_avail;
4326 loop {
4327 if rest.is_empty() {
4328 break;
4329 }
4330 if rest.len() <= current_avail {
4331 if first {
4332 lines.push(format!("{} {}", spaces(indent), rest));
4333 } else {
4334 lines.push(format!("{}/ {}", spaces(indent), rest));
4335 }
4336 break;
4337 }
4338 let split_at = match style {
4340 FoldStyle::Auto => {
4341 let candidate = &rest[..current_avail.min(rest.len())];
4344 let lookahead = rest[candidate.len()..].chars().next();
4345 find_bare_fold_point(candidate, lookahead)
4346 }
4347 FoldStyle::Fixed | FoldStyle::None => current_avail.min(rest.len()),
4348 };
4349 let split_at = if split_at == 0 && !first && matches!(style, FoldStyle::Auto) {
4350 current_avail.min(rest.len())
4352 } else if split_at == 0 {
4353 if first {
4355 lines.push(format!("{} {}", spaces(indent), rest));
4356 } else {
4357 lines.push(format!("{}/ {}", spaces(indent), rest));
4358 }
4359 break;
4360 } else {
4361 split_at
4362 };
4363 let segment = &rest[..split_at];
4364 if first {
4365 lines.push(format!("{} {}", spaces(indent), segment));
4366 first = false;
4367 } else {
4368 lines.push(format!("{}/ {}", spaces(indent), segment));
4369 }
4370 rest = &rest[split_at..];
4371 current_avail = cont_avail;
4372 }
4373 if lines.len() <= 1 {
4374 None } else {
4376 Some(lines)
4377 }
4378}
4379
4380fn fold_bare_key(
4384 key: &str,
4385 pair_indent: usize,
4386 style: FoldStyle,
4387 wrap_width: Option<usize>,
4388) -> Option<Vec<String>> {
4389 let w = wrap_width?;
4390 if matches!(style, FoldStyle::None) { return None; }
4391 if key.len() < w.saturating_sub(pair_indent) { return None; }
4393 let first_avail = w.saturating_sub(pair_indent);
4394 let cont_avail = w.saturating_sub(pair_indent + 2); if cont_avail < MIN_FOLD_CONTINUATION { return None; }
4396 let ind = spaces(pair_indent);
4397 let mut lines: Vec<String> = Vec::new();
4398 let mut rest = key;
4399 let mut first = true;
4400 let mut current_avail = first_avail;
4401 loop {
4402 if rest.is_empty() { break; }
4403 if rest.len() <= current_avail {
4404 lines.push(if first { format!("{}{}", ind, rest) } else { format!("{}/ {}", ind, rest) });
4405 break;
4406 }
4407 let split_at = match style {
4408 FoldStyle::Auto => {
4409 let candidate = &rest[..current_avail.min(rest.len())];
4410 let lookahead = rest[candidate.len()..].chars().next();
4411 find_bare_fold_point(candidate, lookahead)
4412 }
4413 FoldStyle::Fixed | FoldStyle::None => current_avail.min(rest.len()),
4414 };
4415 if split_at == 0 {
4416 lines.push(if first { format!("{}{}", ind, rest) } else { format!("{}/ {}", ind, rest) });
4417 break;
4418 }
4419 lines.push(if first { format!("{}{}", ind, &rest[..split_at]) } else { format!("{}/ {}", ind, &rest[..split_at]) });
4420 rest = &rest[split_at..];
4421 first = false;
4422 current_avail = cont_avail;
4423 }
4424 if lines.len() <= 1 { None } else { Some(lines) }
4425}
4426
4427fn find_number_fold_point(s: &str, avail: usize, auto_mode: bool) -> usize {
4432 let avail = avail.min(s.len());
4433 if avail == 0 || avail >= s.len() {
4434 return 0;
4435 }
4436 if auto_mode {
4437 let candidate = &s[..avail];
4439 if let Some(pos) = candidate.rfind(['.', 'e', 'E'])
4440 && pos > 0 {
4441 return pos; }
4443 }
4444 let bytes = s.as_bytes();
4447 let mut pos = avail;
4448 while pos > 1 {
4449 if bytes[pos - 1].is_ascii_digit() && bytes[pos].is_ascii_digit() {
4450 return pos;
4451 }
4452 pos -= 1;
4453 }
4454 0
4455}
4456
4457fn fold_number(
4460 value: &str,
4461 indent: usize,
4462 first_line_extra: usize,
4463 style: FoldStyle,
4464 wrap_width: Option<usize>,
4465) -> Option<Vec<String>> {
4466 if matches!(style, FoldStyle::None) {
4467 return None;
4468 }
4469 let w = wrap_width?;
4470 let first_avail = w.saturating_sub(indent + first_line_extra);
4471 if value.len() <= first_avail {
4472 return None; }
4474 let cont_avail = w.saturating_sub(indent + 2);
4475 if cont_avail < MIN_FOLD_CONTINUATION {
4476 return None;
4477 }
4478 let auto_mode = matches!(style, FoldStyle::Auto);
4479 let mut lines: Vec<String> = Vec::new();
4480 let mut rest = value;
4481 let mut current_avail = first_avail;
4482 let ind = spaces(indent);
4483 loop {
4484 if rest.len() <= current_avail {
4485 lines.push(format!("{}{}", ind, rest));
4486 break;
4487 }
4488 let split_at = find_number_fold_point(rest, current_avail, auto_mode);
4489 if split_at == 0 {
4490 lines.push(format!("{}{}", ind, rest));
4491 break;
4492 }
4493 lines.push(format!("{}{}", ind, &rest[..split_at]));
4494 rest = &rest[split_at..];
4495 current_avail = cont_avail;
4496 let last = lines.last_mut().unwrap();
4498 let _ = last; }
4505 lines.clear();
4508 let mut rest = value;
4509 let mut first = true;
4510 let mut current_avail = first_avail;
4511 loop {
4512 if rest.len() <= current_avail {
4513 if first {
4514 lines.push(format!("{}{}", ind, rest));
4515 } else {
4516 lines.push(format!("{}/ {}", ind, rest));
4517 }
4518 break;
4519 }
4520 let split_at = find_number_fold_point(rest, current_avail, auto_mode);
4521 if split_at == 0 {
4522 if first {
4523 lines.push(format!("{}{}", ind, rest));
4524 } else {
4525 lines.push(format!("{}/ {}", ind, rest));
4526 }
4527 break;
4528 }
4529 if first {
4530 lines.push(format!("{}{}", ind, &rest[..split_at]));
4531 first = false;
4532 } else {
4533 lines.push(format!("{}/ {}", ind, &rest[..split_at]));
4534 }
4535 rest = &rest[split_at..];
4536 current_avail = cont_avail;
4537 }
4538 Some(lines)
4539}
4540
4541#[derive(Clone, Copy, PartialEq, Eq)]
4543enum CharClass {
4544 Space,
4545 Letter,
4546 Digit,
4547 StickyEnd,
4549 Other,
4550}
4551
4552fn char_class(ch: char) -> CharClass {
4553 if ch == ' ' {
4554 return CharClass::Space;
4555 }
4556 if matches!(ch, '.' | ',' | '/' | '-' | '_' | '~' | '@' | ':') {
4557 return CharClass::StickyEnd;
4558 }
4559 match get_general_category(ch) {
4560 GeneralCategory::UppercaseLetter
4561 | GeneralCategory::LowercaseLetter
4562 | GeneralCategory::TitlecaseLetter
4563 | GeneralCategory::ModifierLetter
4564 | GeneralCategory::OtherLetter
4565 | GeneralCategory::LetterNumber => CharClass::Letter,
4566 GeneralCategory::DecimalNumber | GeneralCategory::OtherNumber => CharClass::Digit,
4567 _ => CharClass::Other,
4568 }
4569}
4570
4571fn find_bare_fold_point(s: &str, lookahead: Option<char>) -> usize {
4583 let mut best = [0usize; 4];
4585 let mut prev: Option<(usize, CharClass)> = None;
4586
4587 for (byte_pos, ch) in s.char_indices() {
4588 let cur = char_class(ch);
4589 if let Some((_, p)) = prev {
4590 match (p, cur) {
4591 (_, CharClass::Space) if byte_pos > 0 => best[0] = byte_pos,
4593 (CharClass::StickyEnd, CharClass::Letter | CharClass::Digit) => best[1] = byte_pos,
4595 (CharClass::Letter, CharClass::Digit) | (CharClass::Digit, CharClass::Letter) => {
4597 best[2] = byte_pos
4598 }
4599 (CharClass::Letter | CharClass::Digit, CharClass::StickyEnd | CharClass::Other) => {
4601 best[3] = byte_pos
4602 }
4603 _ => {}
4604 }
4605 }
4606 prev = Some((byte_pos, cur));
4607 }
4608
4609 if let (Some((_, last_class)), Some(next_ch)) = (prev, lookahead) {
4612 let next_class = char_class(next_ch);
4613 let edge = s.len();
4614 match (last_class, next_class) {
4615 (_, CharClass::Space) => best[0] = best[0].max(edge),
4616 (CharClass::StickyEnd, CharClass::Letter | CharClass::Digit) => {
4617 best[1] = best[1].max(edge)
4618 }
4619 (CharClass::Letter, CharClass::Digit) | (CharClass::Digit, CharClass::Letter) => {
4620 best[2] = best[2].max(edge)
4621 }
4622 (CharClass::Letter | CharClass::Digit, CharClass::StickyEnd | CharClass::Other) => {
4623 best[3] = best[3].max(edge)
4624 }
4625 _ => {}
4626 }
4627 }
4628
4629 best.into_iter().find(|&p| p > 0).unwrap_or(0)
4631}
4632
4633fn fold_json_string(
4637 value: &str,
4638 indent: usize,
4639 first_line_extra: usize,
4640 style: FoldStyle,
4641 wrap_width: Option<usize>,
4642) -> Option<Vec<String>> {
4643 let w = wrap_width?;
4644 let encoded = render_json_string(value);
4645 let first_avail = w.saturating_sub(indent + first_line_extra);
4647 if encoded.len() <= first_avail {
4648 return None; }
4650 let cont_avail = w.saturating_sub(indent + 2);
4651 if cont_avail < MIN_FOLD_CONTINUATION {
4652 return None; }
4654 let inner = &encoded[1..encoded.len() - 1]; let mut lines: Vec<String> = Vec::new();
4658 let mut rest = inner;
4659 let mut first = true;
4660 let mut current_avail = first_avail.saturating_sub(1); loop {
4662 if rest.is_empty() {
4663 if let Some(last) = lines.last_mut() {
4665 last.push('"');
4666 }
4667 break;
4668 }
4669 let segment_avail = if rest.len() <= current_avail {
4671 current_avail.saturating_sub(1)
4673 } else {
4674 current_avail
4675 };
4676 if rest.len() <= segment_avail {
4677 let segment = rest;
4678 if first {
4679 lines.push(format!("{}\"{}\"", spaces(indent), segment));
4680 } else {
4681 lines.push(format!("{}/ {}\"", spaces(indent), segment));
4682 }
4683 break;
4684 }
4685 let split_at = match style {
4687 FoldStyle::Auto => {
4688 let candidate = &rest[..segment_avail.min(rest.len())];
4689 find_json_fold_point(candidate)
4691 }
4692 FoldStyle::Fixed | FoldStyle::None => {
4693 safe_json_split(rest, segment_avail.min(rest.len()))
4694 }
4695 };
4696 if split_at == 0 {
4697 if first {
4699 lines.push(format!("{}\"{}\"", spaces(indent), rest));
4700 } else {
4701 lines.push(format!("{}/ {}\"", spaces(indent), rest));
4702 }
4703 break;
4704 }
4705 let segment = &rest[..split_at];
4706 if first {
4707 lines.push(format!("{}\"{}\"", spaces(indent), segment));
4708 let last = lines.last_mut().unwrap();
4710 last.pop(); first = false;
4712 } else {
4713 lines.push(format!("{}/ {}", spaces(indent), segment));
4714 }
4715 rest = &rest[split_at..];
4716 current_avail = cont_avail;
4717 }
4718 if lines.len() <= 1 {
4719 None
4720 } else {
4721 Some(lines)
4722 }
4723}
4724
4725fn count_preceding_backslashes(bytes: &[u8], pos: usize) -> usize {
4727 let mut count = 0;
4728 let mut p = pos;
4729 while p > 0 {
4730 p -= 1;
4731 if bytes[p] == b'\\' { count += 1; } else { break; }
4732 }
4733 count
4734}
4735
4736fn find_json_fold_point(s: &str) -> usize {
4746 let bytes = s.as_bytes();
4747
4748 let mut i = bytes.len();
4753 while i > 1 {
4754 i -= 1;
4755 if bytes[i] == b'n' && bytes[i - 1] == b'\\' {
4756 let bs = count_preceding_backslashes(bytes, i) + 1; if bs % 2 == 1 {
4759 return (i + 1).min(bytes.len());
4761 }
4762 }
4763 }
4764
4765 let mut i = bytes.len();
4767 while i > 1 {
4768 i -= 1;
4769 if bytes[i] == b' ' {
4770 let safe = safe_json_split(s, i);
4771 if safe == i {
4772 return i;
4773 }
4774 }
4775 }
4776
4777 let mut last_boundary = 0usize;
4782 let mut prev_is_word: Option<bool> = None;
4783 let mut i = 0usize;
4784 while i < bytes.len() {
4785 let cur_is_word = bytes[i].is_ascii_alphanumeric();
4786 if let Some(prev) = prev_is_word
4787 && prev != cur_is_word {
4788 let safe = safe_json_split(s, i);
4789 if safe == i {
4790 last_boundary = i;
4791 }
4792 }
4793 prev_is_word = Some(cur_is_word);
4794 i += 1;
4795 }
4796 if last_boundary > 0 {
4797 return last_boundary;
4798 }
4799
4800 safe_json_split(s, s.len())
4802}
4803
4804fn render_folding_quotes(value: &str, indent: usize, options: &TjsonOptions) -> Vec<String> {
4809 let ind = spaces(indent);
4810 let pieces: Vec<&str> = value.split('\n').collect();
4811 let mut lines: Vec<String> = Vec::new();
4813 for (i, piece) in pieces.iter().enumerate() {
4814 let is_last = i == pieces.len() - 1;
4815 let encoded = render_json_string(piece);
4816 let inner = &encoded[1..encoded.len() - 1]; let nl = if is_last { "" } else { "\\n" };
4818 if i == 0 {
4819 lines.push(format!("{}\"{}{}", ind, inner, nl));
4820 if !is_last {
4821 } else {
4823 lines.last_mut().unwrap().push('"');
4824 }
4825 } else if is_last {
4826 lines.push(format!("{}/ {}\"", ind, inner));
4827 } else {
4828 lines.push(format!("{}/ {}{}", ind, inner, nl));
4829 }
4830 if !matches!(options.string_multiline_fold_style, FoldStyle::None)
4833 && let Some(w) = options.wrap_width {
4834 let last = lines.last().unwrap();
4835 if last.len() > w {
4836 }
4840 }
4841 }
4842 lines
4843}
4844
4845fn split_table_row_for_fold(row: &str, max_len: usize) -> Option<(String, String)> {
4850 if row.len() <= max_len {
4851 return None;
4852 }
4853 let bytes = row.as_bytes();
4854 let scan_end = max_len.min(bytes.len());
4858 let mut pos = scan_end;
4860 while pos > 0 {
4861 pos -= 1;
4862 if bytes[pos] == b' ' && pos > 0 && bytes[pos - 1] != b'|' && bytes[pos - 1] != b' ' {
4863 let before = row[..pos].to_owned();
4864 let after = row[pos + ' '.len_utf8()..].to_owned(); return Some((before, after));
4866 }
4867 }
4868 None
4869}
4870
4871fn is_comma_like(ch: char) -> bool {
4872 matches!(ch, ',' | '\u{FF0C}' | '\u{FE50}')
4873}
4874
4875fn is_quote_like(ch: char) -> bool {
4876 matches!(
4877 get_general_category(ch),
4878 GeneralCategory::InitialPunctuation | GeneralCategory::FinalPunctuation
4879 ) || matches!(ch, '"' | '\'' | '`')
4880}
4881
4882fn is_pipe_like(ch: char) -> bool {
4885 matches!(
4886 ch, '|' | '\u{00a6}' | '\u{01c0}' | '\u{2016}' | '\u{2223}' | '\u{2225}' | '\u{254e}' | '\u{2502}' | '\u{2503}' | '\u{2551}' | '\u{ff5c}' | '\u{ffe4}'
4887 )
4888}
4889fn is_reserved_word(s: &str) -> bool {
4890 matches!(s, "true" | "false" | "null" | "[]" | "{}" | "\"\"") }
4892#[cfg(test)]
4893mod tests {
4894 use super::*;
4895
4896 fn json(input: &str) -> JsonValue {
4897 serde_json::from_str(input).unwrap()
4898 }
4899
4900 fn tjson_value(input: &str) -> TjsonValue {
4901 TjsonValue::from(json(input))
4902 }
4903
4904 fn parse_str(input: &str) -> Result<TjsonValue> {
4905 input.parse()
4906 }
4907
4908 #[test]
4909 fn parses_basic_scalar_examples() {
4910 assert_eq!(
4911 parse_str("null").unwrap().to_json().unwrap(),
4912 json("null")
4913 );
4914 assert_eq!(
4915 parse_str("5").unwrap().to_json().unwrap(),
4916 json("5")
4917 );
4918 assert_eq!(
4919 parse_str(" a").unwrap().to_json().unwrap(),
4920 json("\"a\"")
4921 );
4922 assert_eq!(
4923 parse_str("[]").unwrap().to_json().unwrap(),
4924 json("[]")
4925 );
4926 assert_eq!(
4927 parse_str("{}").unwrap().to_json().unwrap(),
4928 json("{}")
4929 );
4930 }
4931
4932 #[test]
4933 fn parses_comments_and_marker_examples() {
4934 let input = "// comment\n a:5\n// comment\n x:\n [ [ 1\n { b: text";
4935 let expected = json("{\"a\":5,\"x\":[[1],{\"b\":\"text\"}]}");
4936 assert_eq!(
4937 parse_str(input).unwrap().to_json().unwrap(),
4938 expected
4939 );
4940 }
4941
4942 #[test]
4947 fn parses_folded_json_string_example() {
4948 let input =
4949 "\"foldingat\n/ onlyafew\\r\\n\n/ characters\n/ hereusing\n/ somejson\n/ escapes\\\\\"";
4950 let expected = json("\"foldingatonlyafew\\r\\ncharactershereusingsomejsonescapes\\\\\"");
4951 assert_eq!(
4952 parse_str(input).unwrap().to_json().unwrap(),
4953 expected
4954 );
4955 }
4956
4957 #[test]
4958 fn parses_folded_json_string_as_object_value() {
4959 let input = " note:\"hello \n / world\"";
4961 let expected = json("{\"note\":\"hello world\"}");
4962 assert_eq!(
4963 parse_str(input).unwrap().to_json().unwrap(),
4964 expected
4965 );
4966 }
4967
4968 #[test]
4969 fn parses_folded_json_string_multiple_continuations() {
4970 let input = "\"one\n/ two\n/ three\n/ four\"";
4972 let expected = json("\"onetwothreefour\"");
4973 assert_eq!(
4974 parse_str(input).unwrap().to_json().unwrap(),
4975 expected
4976 );
4977 }
4978
4979 #[test]
4980 fn parses_folded_json_string_with_indent() {
4981 let input = " key:\"hello \n / world\"";
4983 let expected = json("{\"key\":\"hello world\"}");
4984 assert_eq!(
4985 parse_str(input).unwrap().to_json().unwrap(),
4986 expected
4987 );
4988 }
4989
4990 #[test]
4993 fn parses_folded_bare_string_root() {
4994 let input = " hello\n/ world";
4996 let expected = json("\"helloworld\"");
4997 assert_eq!(
4998 parse_str(input).unwrap().to_json().unwrap(),
4999 expected
5000 );
5001 }
5002
5003 #[test]
5004 fn parses_folded_bare_string_as_object_value() {
5005 let input = " note: hello\n / world";
5007 let expected = json("{\"note\":\"helloworld\"}");
5008 assert_eq!(
5009 parse_str(input).unwrap().to_json().unwrap(),
5010 expected
5011 );
5012 }
5013
5014 #[test]
5015 fn parses_folded_bare_string_multiple_continuations() {
5016 let input = " note: one\n / two\n / three";
5017 let expected = json("{\"note\":\"onetwothree\"}");
5018 assert_eq!(
5019 parse_str(input).unwrap().to_json().unwrap(),
5020 expected
5021 );
5022 }
5023
5024 #[test]
5025 fn parses_folded_bare_string_preserves_space_after_fold_marker() {
5026 let input = " note: hello\n / world";
5028 let expected = json("{\"note\":\"hello world\"}");
5029 assert_eq!(
5030 parse_str(input).unwrap().to_json().unwrap(),
5031 expected
5032 );
5033 }
5034
5035 #[test]
5038 fn parses_folded_bare_key() {
5039 let input = " averylongkey\n / continuation: value";
5041 let expected = json("{\"averylongkeycontinuation\":\"value\"}");
5042 assert_eq!(
5043 parse_str(input).unwrap().to_json().unwrap(),
5044 expected
5045 );
5046 }
5047
5048 #[test]
5049 fn parses_folded_json_key() {
5050 let input = " \"averylongkey\n / continuation\": value";
5052 let expected = json("{\"averylongkeycontinuation\":\"value\"}");
5053 assert_eq!(
5054 parse_str(input).unwrap().to_json().unwrap(),
5055 expected
5056 );
5057 }
5058
5059 #[test]
5062 fn parses_table_with_folded_cell() {
5063 let input = concat!(
5065 " |name |score |\n",
5066 " | Alice |100 |\n",
5067 " | Bob with a very long\n",
5068 "/ name |200 |\n",
5069 " | Carol |300 |",
5070 );
5071 let expected = json(
5072 "[{\"name\":\"Alice\",\"score\":100},{\"name\":\"Bob with a very longname\",\"score\":200},{\"name\":\"Carol\",\"score\":300}]"
5073 );
5074 assert_eq!(
5075 parse_str(input).unwrap().to_json().unwrap(),
5076 expected
5077 );
5078 }
5079
5080 #[test]
5081 fn parses_table_with_folded_cell_no_trailing_pipe() {
5082 let input = concat!(
5084 " |name |value |\n",
5085 " | short |1 |\n",
5086 " | this is really long\n",
5087 "/ continuation|2 |",
5088 );
5089 let expected = json(
5090 "[{\"name\":\"short\",\"value\":1},{\"name\":\"this is really longcontinuation\",\"value\":2}]"
5091 );
5092 assert_eq!(
5093 parse_str(input).unwrap().to_json().unwrap(),
5094 expected
5095 );
5096 }
5097
5098 #[test]
5099 fn parses_triple_backtick_multiline_string() {
5100 let input = " note: ```\nfirst\nsecond\n indented\n ```";
5102 let expected = json("{\"note\":\"first\\nsecond\\n indented\"}");
5103 assert_eq!(
5104 parse_str(input).unwrap().to_json().unwrap(),
5105 expected
5106 );
5107 }
5108
5109 #[test]
5110 fn parses_triple_backtick_crlf_multiline_string() {
5111 let input = " note: ```\\r\\n\nfirst\nsecond\n indented\n ```\\r\\n";
5113 let expected = json("{\"note\":\"first\\r\\nsecond\\r\\n indented\"}");
5114 assert_eq!(
5115 parse_str(input).unwrap().to_json().unwrap(),
5116 expected
5117 );
5118 }
5119
5120 #[test]
5121 fn parses_double_backtick_multiline_string() {
5122 let input = " ``\n| first\n| second\n ``";
5124 let expected = json("\"first\\nsecond\"");
5125 assert_eq!(
5126 parse_str(input).unwrap().to_json().unwrap(),
5127 expected
5128 );
5129 }
5130
5131 #[test]
5132 fn parses_double_backtick_with_explicit_lf_indicator() {
5133 let input = " ``\\n\n| first\n| second\n ``\\n";
5134 let expected = json("\"first\\nsecond\"");
5135 assert_eq!(
5136 parse_str(input).unwrap().to_json().unwrap(),
5137 expected
5138 );
5139 }
5140
5141 #[test]
5142 fn parses_double_backtick_crlf_multiline_string() {
5143 let input = " ``\\r\\n\n| first\n| second\n ``\\r\\n";
5145 let expected = json("\"first\\r\\nsecond\"");
5146 assert_eq!(
5147 parse_str(input).unwrap().to_json().unwrap(),
5148 expected
5149 );
5150 }
5151
5152 #[test]
5153 fn parses_double_backtick_with_fold() {
5154 let input = " ``\n| first line that is \n/ continued here\n| second\n ``";
5156 let expected = json("\"first line that is continued here\\nsecond\"");
5157 assert_eq!(
5158 parse_str(input).unwrap().to_json().unwrap(),
5159 expected
5160 );
5161 }
5162
5163 #[test]
5164 fn parses_single_backtick_multiline_string() {
5165 let input = " note: `\n first\n second\n indented\n `";
5167 let expected = json("{\"note\":\"first\\nsecond\\nindented\"}");
5168 assert_eq!(
5169 parse_str(input).unwrap().to_json().unwrap(),
5170 expected
5171 );
5172 }
5173
5174 #[test]
5175 fn parses_single_backtick_with_fold() {
5176 let input = " note: `\n first line that is \n / continued here\n second\n `";
5178 let expected = json("{\"note\":\"first line that is continued here\\nsecond\"}");
5179 assert_eq!(
5180 parse_str(input).unwrap().to_json().unwrap(),
5181 expected
5182 );
5183 }
5184
5185 #[test]
5186 fn parses_single_backtick_with_leading_spaces_in_content() {
5187 let input = " `\n first\n indented two extra\n last\n `";
5189 let expected = json("\"first\\n indented two extra\\nlast\"");
5190 assert_eq!(
5191 parse_str(input).unwrap().to_json().unwrap(),
5192 expected
5193 );
5194 }
5195
5196 #[test]
5197 fn rejects_triple_backtick_without_closing_glyph() {
5198 let input = " note: ```\nfirst\nsecond";
5199 assert!(parse_str(input).is_err());
5200 }
5201
5202 #[test]
5203 fn rejects_double_backtick_without_closing_glyph() {
5204 let input = " ``\n| first\n| second";
5205 assert!(parse_str(input).is_err());
5206 }
5207
5208 #[test]
5209 fn rejects_single_backtick_without_closing_glyph() {
5210 let input = " note: `\n first\n second";
5211 assert!(parse_str(input).is_err());
5212 }
5213
5214 #[test]
5215 fn rejects_double_backtick_body_without_pipe() {
5216 let input = " ``\njust some text\n| second\n ``";
5217 assert!(parse_str(input).is_err());
5218 }
5219
5220 #[test]
5221 fn parses_table_array_example() {
5222 let input = " |a |b |c |\n |1 | x |true |\n |2 | y |false |\n |3 | z |null |";
5223 let expected = json(
5224 "[{\"a\":1,\"b\":\"x\",\"c\":true},{\"a\":2,\"b\":\"y\",\"c\":false},{\"a\":3,\"b\":\"z\",\"c\":null}]",
5225 );
5226 assert_eq!(
5227 parse_str(input).unwrap().to_json().unwrap(),
5228 expected
5229 );
5230 }
5231
5232 #[test]
5233 fn parses_minimal_json_inside_array_example() {
5234 let input = " [{\"a\":{\"b\":null},\"c\":3}]";
5235 let expected = json("[[{\"a\":{\"b\":null},\"c\":3}]]");
5236 assert_eq!(
5237 parse_str(input).unwrap().to_json().unwrap(),
5238 expected
5239 );
5240 }
5241
5242 #[test]
5243 fn renders_basic_scalar_examples() {
5244 assert_eq!(render_string(&tjson_value("null")).unwrap(), "null");
5245 assert_eq!(render_string(&tjson_value("5")).unwrap(), "5");
5246 assert_eq!(render_string(&tjson_value("\"a\"")).unwrap(), " a");
5247 assert_eq!(render_string(&tjson_value("[]")).unwrap(), "[]");
5248 assert_eq!(render_string(&tjson_value("{}")).unwrap(), "{}");
5249 }
5250
5251 #[test]
5252 fn renders_multiline_string_example() {
5253 let rendered =
5255 render_string(&tjson_value("{\"note\":\"first\\nsecond\\n indented\"}")).unwrap();
5256 assert_eq!(
5257 rendered,
5258 " note: ``\n| first\n| second\n| indented\n ``"
5259 );
5260 }
5261
5262 #[test]
5263 fn renders_crlf_multiline_string_example() {
5264 let rendered = render_string(&tjson_value(
5266 "{\"note\":\"first\\r\\nsecond\\r\\n indented\"}",
5267 ))
5268 .unwrap();
5269 assert_eq!(
5270 rendered,
5271 " note: ``\\r\\n\n| first\n| second\n| indented\n ``\\r\\n"
5272 );
5273 }
5274
5275 #[test]
5276 fn renders_single_backtick_root_string() {
5277 let value = TjsonValue::String("line one\nline two".to_owned());
5279 let rendered = render_string_with_options(
5280 &value,
5281 TjsonOptions { multiline_style: MultilineStyle::Floating, ..TjsonOptions::default() },
5282 ).unwrap();
5283 assert_eq!(rendered, " `\n line one\n line two\n `");
5284 }
5285
5286 #[test]
5287 fn renders_single_backtick_shallow_key() {
5288 let rendered = render_string_with_options(
5290 &tjson_value("{\"note\":\"line one\\nline two\"}"),
5291 TjsonOptions { multiline_style: MultilineStyle::Floating, ..TjsonOptions::default() },
5292 ).unwrap();
5293 assert_eq!(rendered, " note: `\n line one\n line two\n `");
5294 }
5295
5296 #[test]
5297 fn renders_single_backtick_deep_key() {
5298 let rendered = render_string_with_options(
5300 &tjson_value("{\"outer\":{\"inner\":\"line one\\nline two\"}}"),
5301 TjsonOptions { multiline_style: MultilineStyle::Floating, ..TjsonOptions::default() },
5302 ).unwrap();
5303 assert_eq!(
5304 rendered,
5305 " outer:\n inner: `\n line one\n line two\n `"
5306 );
5307 }
5308
5309 #[test]
5310 fn renders_single_backtick_three_lines() {
5311 let rendered = render_string_with_options(
5313 &tjson_value("{\"a\":{\"b\":{\"c\":\"x\\ny\\nz\"}}}"),
5314 TjsonOptions { multiline_style: MultilineStyle::Floating, ..TjsonOptions::default() },
5315 ).unwrap();
5316 assert_eq!(
5317 rendered,
5318 " a:\n b:\n c: `\n x\n y\n z\n `"
5319 );
5320 }
5321
5322 #[test]
5323 fn renders_double_backtick_with_bold_style() {
5324 let value = TjsonValue::String("line one\nline two".to_owned());
5326 let rendered = render_string_with_options(
5327 &value,
5328 TjsonOptions {
5329 multiline_style: MultilineStyle::Bold,
5330 ..TjsonOptions::default()
5331 },
5332 )
5333 .unwrap();
5334 assert_eq!(rendered, " ``\n| line one\n| line two\n ``");
5335 }
5336
5337 #[test]
5338 fn renders_triple_backtick_with_fullwidth_style() {
5339 let value = TjsonValue::String("normal line\nsecond line".to_owned());
5341 let rendered = render_string_with_options(
5342 &value,
5343 TjsonOptions {
5344 multiline_style: MultilineStyle::Transparent,
5345 ..TjsonOptions::default()
5346 },
5347 )
5348 .unwrap();
5349 assert_eq!(rendered, " ```\nnormal line\nsecond line\n ```");
5350 }
5351
5352 #[test]
5353 fn renders_triple_backtick_falls_back_to_bold_when_pipe_heavy() {
5354 let value = TjsonValue::String("| piped\n| also piped\nnormal".to_owned());
5356 let rendered = render_string_with_options(
5357 &value,
5358 TjsonOptions {
5359 multiline_style: MultilineStyle::Transparent,
5360 ..TjsonOptions::default()
5361 },
5362 )
5363 .unwrap();
5364 assert!(rendered.contains(" ``"), "expected `` fallback, got: {rendered}");
5365 }
5366
5367 #[test]
5368 fn transparent_never_folds_body_lines_regardless_of_wrap() {
5369 let long_line = "a".repeat(200);
5372 let value = TjsonValue::String(format!("{long_line}\nsecond line"));
5373 let rendered = render_string_with_options(
5374 &value,
5375 TjsonOptions::default()
5376 .wrap_width(Some(20))
5377 .multiline_style(MultilineStyle::Transparent)
5378 .string_multiline_fold_style(FoldStyle::Auto),
5379 ).unwrap();
5380 let body_lines: Vec<&str> = rendered.lines()
5383 .filter(|l| !l.trim_start().starts_with("```") && !l.trim_start().starts_with("``"))
5384 .collect();
5385 for line in &body_lines {
5386 assert!(!line.trim_start().starts_with("/ "), "``` body must not have fold continuations: {rendered}");
5387 }
5388 }
5389
5390 #[test]
5391 fn transparent_with_string_multiline_fold_style_auto_still_no_fold() {
5392 let value = TjsonValue::String("short\nsecond".to_owned());
5395 let rendered = render_string_with_options(
5396 &value,
5397 TjsonOptions::default()
5398 .multiline_style(MultilineStyle::Transparent)
5399 .string_multiline_fold_style(FoldStyle::Auto),
5400 ).unwrap();
5401 assert!(rendered.contains("```"), "should use triple backtick: {rendered}");
5402 assert!(!rendered.contains("/ "), "Transparent must never fold: {rendered}");
5403 }
5404
5405 #[test]
5406 fn floating_falls_back_to_bold_when_line_count_exceeds_max() {
5407 let value = TjsonValue::String("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk".to_owned());
5409 let rendered = render_string_with_options(
5410 &value,
5411 TjsonOptions { multiline_style: MultilineStyle::Floating, ..TjsonOptions::default() },
5412 ).unwrap();
5413 assert!(rendered.starts_with(" ``"), "expected `` fallback for >10 lines, got: {rendered}");
5414 }
5415
5416 #[test]
5417 fn floating_falls_back_to_bold_when_line_overflows_width() {
5418 let long_line = "x".repeat(80); let value = TjsonValue::String(format!("short\n{long_line}"));
5421 let rendered = render_string_with_options(
5422 &value,
5423 TjsonOptions { multiline_style: MultilineStyle::Floating, ..TjsonOptions::default() },
5424 ).unwrap();
5425 assert!(rendered.starts_with(" ``"), "expected `` fallback for overflow, got: {rendered}");
5426 }
5427
5428 #[test]
5429 fn floating_renders_single_backtick_when_lines_fit() {
5430 let value = TjsonValue::String("normal line\nsecond line".to_owned());
5432 let rendered = render_string_with_options(
5433 &value,
5434 TjsonOptions { multiline_style: MultilineStyle::Floating, ..TjsonOptions::default() },
5435 ).unwrap();
5436 assert!(rendered.starts_with(" `\n"), "expected ` glyph, got: {rendered}");
5437 assert!(!rendered.contains("| "), "should not have pipe markers");
5438 }
5439
5440 #[test]
5441 fn light_uses_single_backtick_when_safe() {
5442 let value = TjsonValue::String("short\nsecond".to_owned());
5443 let rendered = render_string_with_options(
5444 &value,
5445 TjsonOptions { multiline_style: MultilineStyle::Light, ..TjsonOptions::default() },
5446 )
5447 .unwrap();
5448 assert!(rendered.starts_with(" `\n"), "expected ` glyph, got: {rendered}");
5449 }
5450
5451 #[test]
5452 fn light_stays_single_backtick_on_overflow() {
5453 let long = "x".repeat(80);
5455 let value = TjsonValue::String(format!("short\n{long}"));
5456 let rendered = render_string_with_options(
5457 &value,
5458 TjsonOptions { multiline_style: MultilineStyle::Light, ..TjsonOptions::default() },
5459 )
5460 .unwrap();
5461 assert!(rendered.starts_with(" `\n"), "Light should stay as `, got: {rendered}");
5462 assert!(!rendered.contains("``"), "Light must not escalate to `` on overflow");
5463 }
5464
5465 #[test]
5466 fn light_stays_single_backtick_on_too_many_lines() {
5467 let value = TjsonValue::String("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk".to_owned());
5469 let rendered = render_string_with_options(
5470 &value,
5471 TjsonOptions { multiline_style: MultilineStyle::Light, ..TjsonOptions::default() },
5472 )
5473 .unwrap();
5474 assert!(rendered.starts_with(" `\n"), "Light should stay as `, got: {rendered}");
5475 assert!(!rendered.contains("``"), "Light must not escalate to `` on line count");
5476 }
5477
5478 #[test]
5479 fn light_falls_back_to_bold_on_dangerous_content() {
5480 let value = TjsonValue::String("| piped\n| also piped\nnormal".to_owned());
5482 let rendered = render_string_with_options(
5483 &value,
5484 TjsonOptions { multiline_style: MultilineStyle::Light, ..TjsonOptions::default() },
5485 )
5486 .unwrap();
5487 assert!(rendered.starts_with(" ``"), "Light should fall back to `` for pipe-heavy content, got: {rendered}");
5488 }
5489
5490 #[test]
5491 fn folding_quotes_uses_json_string_for_eol_strings() {
5492 let value = TjsonValue::String("first line\nsecond line".to_owned());
5493 let rendered = render_string_with_options(
5494 &value,
5495 TjsonOptions { multiline_style: MultilineStyle::FoldingQuotes, ..TjsonOptions::default() },
5496 )
5497 .unwrap();
5498 assert!(rendered.starts_with(" \"") || rendered.starts_with("\""),
5499 "expected JSON string, got: {rendered}");
5500 assert!(!rendered.contains('`'), "FoldingQuotes must not use multiline glyphs");
5501 }
5502
5503 #[test]
5504 fn folding_quotes_single_line_strings_unchanged() {
5505 let value = TjsonValue::String("hello world".to_owned());
5507 let rendered = render_string_with_options(
5508 &value,
5509 TjsonOptions { multiline_style: MultilineStyle::FoldingQuotes, ..TjsonOptions::default() },
5510 )
5511 .unwrap();
5512 assert_eq!(rendered, " hello world");
5513 }
5514
5515 #[test]
5516 fn folding_quotes_folds_long_eol_string() {
5517 let value = TjsonValue::String("long string with spaces that needs folding\nsecond".to_owned());
5521 let rendered = render_string_with_options(
5522 &value,
5523 TjsonOptions {
5524 multiline_style: MultilineStyle::FoldingQuotes,
5525 wrap_width: Some(40),
5526 ..TjsonOptions::default()
5527 },
5528 )
5529 .unwrap();
5530 assert!(rendered.contains("/ "), "expected fold continuation, got: {rendered}");
5531 assert!(!rendered.contains('`'), "must not use multiline glyphs");
5532 }
5533
5534 #[test]
5535 fn folding_quotes_skips_fold_when_overrun_within_25_percent() {
5536 let value = TjsonValue::String("abcdefghijklmnopqrstuvwxyz123456\nsecond".to_owned());
5539 let rendered = render_string_with_options(
5540 &value,
5541 TjsonOptions {
5542 multiline_style: MultilineStyle::FoldingQuotes,
5543 wrap_width: Some(40),
5544 ..TjsonOptions::default()
5545 },
5546 )
5547 .unwrap();
5548 assert_eq!(rendered, "\"abcdefghijklmnopqrstuvwxyz123456\\n\n/ second\"");
5549 }
5550
5551 #[test]
5552 fn mixed_newlines_fall_back_to_json_string() {
5553 let rendered =
5554 render_string(&tjson_value("{\"note\":\"first\\r\\nsecond\\nthird\"}")).unwrap();
5555 assert_eq!(rendered, " note:\"first\\r\\nsecond\\nthird\"");
5556 }
5557
5558 #[test]
5559 fn escapes_forbidden_characters_in_json_strings() {
5560 let rendered = render_string(&tjson_value("{\"note\":\"a\\u200Db\"}")).unwrap();
5561 assert_eq!(rendered, " note:\"a\\u200db\"");
5562 }
5563
5564 #[test]
5565 fn forbidden_characters_force_multiline_fallback_to_json_string() {
5566 let rendered = render_string(&tjson_value("{\"lines\":\"x\\ny\\u200Dz\"}")).unwrap();
5567 assert_eq!(rendered, " lines:\"x\\ny\\u200dz\"");
5568 }
5569
5570 #[test]
5571 fn pipe_heavy_content_falls_back_to_double_backtick() {
5572 let value = TjsonValue::String("| line one\n| line two\nnormal line".to_owned());
5575 let rendered = render_string(&value).unwrap();
5576 assert!(rendered.contains(" ``"), "expected `` glyph, got: {rendered}");
5577 assert!(rendered.contains("| | line one"), "expected piped body");
5578 }
5579
5580 #[test]
5581 fn triple_backtick_collision_falls_back_to_double_backtick() {
5582 let value = TjsonValue::String(" ```\nsecond line".to_owned());
5585 let rendered = render_string(&value).unwrap();
5586 assert!(rendered.contains(" ``"), "expected `` glyph, got: {rendered}");
5587 }
5588
5589 #[test]
5590 fn backtick_content_falls_back_to_double_backtick() {
5591 let value = TjsonValue::String("normal line\n `` something".to_owned());
5594 let rendered = render_string(&value).unwrap();
5595 assert!(rendered.contains(" ``"), "expected `` glyph, got: {rendered}");
5596 assert!(rendered.contains("| normal line"), "expected pipe-guarded body");
5597 }
5598
5599 #[test]
5600 fn rejects_raw_forbidden_characters() {
5601 let input = format!(" note:\"a{}b\"", '\u{200D}');
5602 let error = parse_str(&input).unwrap_err();
5603 assert!(error.to_string().contains("U+200D"));
5604 }
5605
5606 #[test]
5607 fn renders_table_when_eligible() {
5608 let value = tjson_value(
5609 "[{\"a\":1,\"b\":\"x\",\"c\":true},{\"a\":2,\"b\":\"y\",\"c\":false},{\"a\":3,\"b\":\"z\",\"c\":null}]",
5610 );
5611 let rendered = render_string(&value).unwrap();
5612 assert_eq!(
5613 rendered,
5614 " |a |b |c |\n |1 | x |true |\n |2 | y |false |\n |3 | z |null |"
5615 );
5616 }
5617
5618 #[test]
5619 fn table_rejected_when_shared_keys_have_different_order() {
5620 let value = tjson_value(
5623 "[{\"a\":1,\"b\":2,\"c\":3},{\"b\":4,\"a\":5,\"c\":6},{\"a\":7,\"b\":8,\"c\":9}]",
5624 );
5625 let rendered = render_string(&value).unwrap();
5626 assert!(!rendered.contains('|'), "should not render as table when key order differs: {rendered}");
5627 }
5628
5629 #[test]
5630 fn table_allowed_when_rows_have_subset_of_keys() {
5631 let value = tjson_value(
5633 "[{\"a\":1,\"b\":2,\"c\":3},{\"a\":4,\"b\":5},{\"a\":6,\"b\":7,\"c\":8}]",
5634 );
5635 let rendered = render_string_with_options(
5636 &value,
5637 TjsonOptions::default().table_min_similarity(0.5),
5638 ).unwrap();
5639 assert!(rendered.contains('|'), "should render as table when rows are a subset: {rendered}");
5640 }
5641
5642 #[test]
5643 fn renders_table_for_array_object_values() {
5644 let value = tjson_value(
5645 "{\"people\":[{\"name\":\"Alice\",\"age\":30,\"active\":true},{\"name\":\"Bob\",\"age\":25,\"active\":false},{\"name\":\"Carol\",\"age\":35,\"active\":true}]}",
5646 );
5647 let rendered = render_string(&value).unwrap();
5648 assert_eq!(
5649 rendered,
5650 " people:\n |name |age |active |\n | Alice |30 |true |\n | Bob |25 |false |\n | Carol |35 |true |"
5651 );
5652 }
5653
5654 #[test]
5655 fn packs_explicit_nested_arrays_and_objects_kv1() {
5656 let value = tjson_value(
5657 "{\"nested\":[[1,2],[3,4]],\"rows\":[{\"a\":1,\"b\":2},{\"c\":3,\"d\":4}]}",
5658 );
5659 let rendered = render_string_with_options(&value, TjsonOptions::default().kv_pack_multiple(1).unwrap()).unwrap();
5660 assert_eq!(
5661 rendered,
5662 " nested:\n [ [ 1, 2\n [ 3, 4\n rows:\n [ { a:1 b:2\n { c:3 d:4"
5663 );
5664 }
5665
5666 #[test]
5667 fn packs_explicit_nested_arrays_and_objects() {
5668 let value = tjson_value(
5669 "{\"nested\":[[1,2],[3,4]],\"rows\":[{\"a\":1,\"b\":2},{\"c\":3,\"d\":4}]}",
5670 );
5671 let rendered = render_string(&value).unwrap();
5672 assert_eq!(
5673 rendered,
5674 " nested:\n [ [ 1, 2\n [ 3, 4\n rows:\n [ { a:1 b:2\n { c:3 d:4"
5675 );
5676 }
5677
5678 #[test]
5679 fn wraps_long_packed_arrays_before_falling_back_to_multiline() {
5680 let value =
5681 tjson_value("{\"data\":[100,200,300,400,500,600,700,800,900,1000,1100,1200,1300]}");
5682 let rendered = render_string_with_options(
5683 &value,
5684 TjsonOptions {
5685 wrap_width: Some(40),
5686 ..TjsonOptions::default()
5687 },
5688 )
5689 .unwrap();
5690 assert_eq!(
5691 rendered,
5692 " data: 100, 200, 300, 400, 500, 600,\n 700, 800, 900, 1000, 1100, 1200,\n 1300"
5693 );
5694 }
5695
5696 #[test]
5697 fn default_string_array_style_is_prefer_comma() {
5698 let value = tjson_value("{\"items\":[\"alpha\",\"beta\",\"gamma\"]}");
5699 let rendered = render_string(&value).unwrap();
5700 assert_eq!(rendered, " items: alpha, beta, gamma");
5701 }
5702
5703 #[test]
5704 fn bare_strings_none_quotes_single_line_strings() {
5705 let value = tjson_value("{\"greeting\":\"hello world\",\"items\":[\"alpha\",\"beta\"]}");
5706 let rendered = render_string_with_options(
5707 &value,
5708 TjsonOptions {
5709 bare_strings: BareStyle::None,
5710 ..TjsonOptions::default()
5711 },
5712 )
5713 .unwrap();
5714 assert_eq!(
5715 rendered,
5716 " greeting:\"hello world\"\n items: \"alpha\", \"beta\""
5717 );
5718 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
5719 assert_eq!(reparsed, value.to_json().unwrap());
5720 }
5721
5722 #[test]
5723 fn bare_keys_none_quotes_keys_in_objects_and_tables_kv1() {
5724 let object_value = tjson_value("{\"alpha\":1,\"beta key\":2}");
5725 let rendered_object = render_string_with_options(
5726 &object_value,
5727 TjsonOptions {
5728 bare_keys: BareStyle::None,
5729 kv_pack_multiple: 1,
5730 ..TjsonOptions::default()
5731 },
5732 )
5733 .unwrap();
5734 assert_eq!(rendered_object, " \"alpha\":1 \"beta key\":2");
5735 }
5736
5737 #[test]
5738 fn bare_keys_none_quotes_keys_in_objects_and_tables() {
5739 let object_value = tjson_value("{\"alpha\":1,\"beta key\":2}");
5740 let rendered_object = render_string_with_options(
5741 &object_value,
5742 TjsonOptions {
5743 bare_keys: BareStyle::None,
5744 ..TjsonOptions::default()
5745 },
5746 )
5747 .unwrap();
5748 assert_eq!(rendered_object, " \"alpha\":1 \"beta key\":2");
5749
5750 let table_value = tjson_value(
5751 "{\"rows\":[{\"alpha\":1,\"beta\":2},{\"alpha\":3,\"beta\":4},{\"alpha\":5,\"beta\":6}]}",
5752 );
5753 let rendered_table = render_string_with_options(
5754 &table_value,
5755 TjsonOptions {
5756 bare_keys: BareStyle::None,
5757 table_min_columns: 2,
5758 ..TjsonOptions::default()
5759 },
5760 )
5761 .unwrap();
5762 assert_eq!(
5763 rendered_table,
5764 " \"rows\":\n |\"alpha\" |\"beta\" |\n |1 |2 |\n |3 |4 |\n |5 |6 |"
5765 );
5766 let reparsed = parse_str(&rendered_table)
5767 .unwrap()
5768 .to_json()
5769 .unwrap();
5770 assert_eq!(reparsed, table_value.to_json().unwrap());
5771 }
5772
5773 #[test]
5774 fn force_markers_applies_to_root_and_key_nested_single_levels_kv1() {
5775 let value =
5776 tjson_value("{\"a\":5,\"6\":\"fred\",\"xy\":[],\"de\":{},\"e\":[1],\"o\":{\"k\":2}}");
5777 let rendered = render_string_with_options(
5778 &value,
5779 TjsonOptions {
5780 force_markers: true,
5781 kv_pack_multiple: 1,
5782 ..TjsonOptions::default()
5783 },
5784 )
5785 .unwrap();
5786 assert_eq!(
5787 rendered,
5788 "{ a:5 6: fred xy:[] de:{}\n e: 1\n o:\n { k:2"
5789 );
5790 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
5791 assert_eq!(reparsed, value.to_json().unwrap());
5792 }
5793
5794 #[test]
5795 fn force_markers_applies_to_root_and_key_nested_single_levels() {
5796 let value =
5797 tjson_value("{\"a\":5,\"6\":\"fred\",\"xy\":[],\"de\":{},\"e\":[1],\"o\":{\"k\":2}}");
5798 let rendered = render_string_with_options(
5799 &value,
5800 TjsonOptions {
5801 force_markers: true,
5802 ..TjsonOptions::default()
5803 },
5804 )
5805 .unwrap();
5806 assert_eq!(
5807 rendered,
5808 "{ a:5 6: fred xy:[] de:{}\n e: 1\n o:\n { k:2"
5809 );
5810 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
5811 assert_eq!(reparsed, value.to_json().unwrap());
5812 }
5813
5814 #[test]
5815 fn force_markers_applies_to_root_arrays() {
5816 let value = tjson_value("[1,2,3]");
5817 let rendered = render_string_with_options(
5818 &value,
5819 TjsonOptions {
5820 force_markers: true,
5821 ..TjsonOptions::default()
5822 },
5823 )
5824 .unwrap();
5825 assert_eq!(rendered, "[ 1, 2, 3");
5826 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
5827 assert_eq!(reparsed, value.to_json().unwrap());
5828 }
5829
5830 #[test]
5831 fn force_markers_suppresses_table_rendering_for_array_containers() {
5832 let value = tjson_value("[{\"a\":1,\"b\":2},{\"a\":3,\"b\":4},{\"a\":5,\"b\":6}]");
5833 let rendered = render_string_with_options(
5834 &value,
5835 TjsonOptions {
5836 force_markers: true,
5837 table_min_columns: 2,
5838 ..TjsonOptions::default()
5839 },
5840 )
5841 .unwrap();
5842 assert_eq!(rendered, "[ |a |b |\n |1 |2 |\n |3 |4 |\n |5 |6 |");
5843 }
5844
5845 #[test]
5846 fn string_array_style_spaces_forces_space_packing() {
5847 let value = tjson_value("{\"items\":[\"alpha\",\"beta\",\"gamma\"]}");
5848 let rendered = render_string_with_options(
5849 &value,
5850 TjsonOptions {
5851 string_array_style: StringArrayStyle::Spaces,
5852 ..TjsonOptions::default()
5853 },
5854 )
5855 .unwrap();
5856 assert_eq!(rendered, " items: alpha beta gamma");
5857 }
5858
5859 #[test]
5860 fn string_array_style_none_disables_string_array_packing() {
5861 let value = tjson_value("{\"items\":[\"alpha\",\"beta\",\"gamma\"]}");
5862 let rendered = render_string_with_options(
5863 &value,
5864 TjsonOptions {
5865 string_array_style: StringArrayStyle::None,
5866 ..TjsonOptions::default()
5867 },
5868 )
5869 .unwrap();
5870 assert_eq!(rendered, " items:\n alpha\n beta\n gamma");
5871 }
5872
5873 #[test]
5874 fn prefer_comma_can_fall_back_to_spaces_when_wrap_is_cleaner() {
5875 let value = tjson_value("{\"items\":[\"aa\",\"bb\",\"cc\"]}");
5876 let comma = render_string_with_options(
5877 &value,
5878 TjsonOptions {
5879 string_array_style: StringArrayStyle::Comma,
5880 wrap_width: Some(18),
5881 ..TjsonOptions::default()
5882 },
5883 )
5884 .unwrap();
5885 let prefer_comma = render_string_with_options(
5886 &value,
5887 TjsonOptions {
5888 string_array_style: StringArrayStyle::PreferComma,
5889 wrap_width: Some(18),
5890 ..TjsonOptions::default()
5891 },
5892 )
5893 .unwrap();
5894 assert_eq!(comma, " items: aa, bb,\n cc");
5895 assert_eq!(prefer_comma, " items: aa bb\n cc");
5896 }
5897
5898 #[test]
5899 fn quotes_comma_strings_in_packed_arrays_so_they_round_trip() {
5900 let value = tjson_value("{\"items\":[\"apples, oranges\",\"pears, plums\",\"grapes\"]}");
5901 let rendered = render_string(&value).unwrap();
5902 assert_eq!(
5903 rendered,
5904 " items: \"apples, oranges\", \"pears, plums\", grapes"
5905 );
5906 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
5907 assert_eq!(reparsed, value.to_json().unwrap());
5908 }
5909
5910 #[test]
5911 fn spaces_style_quotes_comma_strings_and_round_trips() {
5912 let value = tjson_value("{\"items\":[\"apples, oranges\",\"pears, plums\"]}");
5913 let rendered = render_string_with_options(
5914 &value,
5915 TjsonOptions {
5916 string_array_style: StringArrayStyle::Spaces,
5917 ..TjsonOptions::default()
5918 },
5919 )
5920 .unwrap();
5921 assert_eq!(rendered, " items: \"apples, oranges\" \"pears, plums\"");
5922 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
5923 assert_eq!(reparsed, value.to_json().unwrap());
5924 }
5925
5926 #[test]
5927 fn canonical_rendering_disables_tables_and_inline_packing() {
5928 let value = tjson_value(
5929 "[{\"a\":1,\"b\":\"x\",\"c\":true},{\"a\":2,\"b\":\"y\",\"c\":false},{\"a\":3,\"b\":\"z\",\"c\":null}]",
5930 );
5931 let rendered = render_string_with_options(&value, TjsonOptions::canonical())
5932 .unwrap();
5933 assert!(!rendered.contains('|'));
5934 assert!(!rendered.contains(", "));
5935 }
5936
5937 #[test]
5943 fn bare_fold_none_does_not_fold() {
5944 let value = TjsonValue::from(json(r#"{"k":"aaaaa bbbbb"}"#));
5946 let rendered = render_string_with_options(
5947 &value,
5948 TjsonOptions::default()
5949 .wrap_width(Some(15))
5950 .string_bare_fold_style(FoldStyle::None),
5951 ).unwrap();
5952 assert!(!rendered.contains("/ "), "None fold style must not fold: {rendered}");
5953 }
5954
5955 #[test]
5956 fn bare_fold_fixed_folds_at_wrap_width() {
5957 let value = TjsonValue::from(json(r#"{"k":"aaaaabbbbbcccccdddd"}"#));
5962 let rendered = render_string_with_options(
5963 &value,
5964 TjsonOptions::default()
5965 .wrap_width(Some(20))
5966 .string_bare_fold_style(FoldStyle::Fixed),
5967 ).unwrap();
5968 assert!(rendered.contains("/ "), "Fixed must fold: {rendered}");
5969 assert!(!rendered.contains("/ ") || rendered.lines().count() == 2, "exactly one fold: {rendered}");
5970 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
5971 assert_eq!(reparsed, json(r#"{"k":"aaaaabbbbbcccccdddd"}"#));
5972 }
5973
5974 #[test]
5975 fn bare_fold_auto_folds_at_single_space() {
5976 let value = TjsonValue::from(json(r#"{"k":"aaaaa bbbbbccccc"}"#));
5980 let rendered = render_string_with_options(
5981 &value,
5982 TjsonOptions::default()
5983 .wrap_width(Some(20))
5984 .string_bare_fold_style(FoldStyle::Auto),
5985 ).unwrap();
5986 assert_eq!(rendered, " k: aaaaa\n / bbbbbccccc");
5987 }
5988
5989 #[test]
5990 fn bare_fold_auto_folds_at_word_boundary_slash() {
5991 let value = TjsonValue::from(json(r#"{"k":"aaaaa/bbbbbccccc"}"#));
5995 let rendered = render_string_with_options(
5996 &value,
5997 TjsonOptions::default()
5998 .wrap_width(Some(20))
5999 .string_bare_fold_style(FoldStyle::Auto),
6000 ).unwrap();
6001 assert!(rendered.contains("/ "), "expected fold: {rendered}");
6002 assert!(rendered.contains("aaaaa/\n"), "slash must trail the line: {rendered}");
6003 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
6004 assert_eq!(reparsed, json(r#"{"k":"aaaaa/bbbbbccccc"}"#));
6005 }
6006
6007 #[test]
6008 fn bare_fold_auto_prefers_space_over_word_boundary() {
6009 let value = TjsonValue::from(json(r#"{"k":"aa/bbbbbbbbb cccc"}"#));
6013 let rendered = render_string_with_options(
6014 &value,
6015 TjsonOptions::default()
6016 .wrap_width(Some(20))
6017 .string_bare_fold_style(FoldStyle::Auto),
6018 ).unwrap();
6019 assert!(rendered.contains("/ "), "expected fold: {rendered}");
6020 assert!(rendered.contains("aa/bbbbbbbbb\n"), "must fold at space not slash: {rendered}");
6022 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
6023 assert_eq!(reparsed, json(r#"{"k":"aa/bbbbbbbbb cccc"}"#));
6024 }
6025
6026 #[test]
6027 fn quoted_fold_auto_folds_at_word_boundary_slash() {
6028 let value = TjsonValue::from(json(r#"{"k":"aaaaa/bbbbbcccccc"}"#));
6032 let rendered = render_string_with_options(
6033 &value,
6034 TjsonOptions::default()
6035 .wrap_width(Some(20))
6036 .bare_strings(BareStyle::None)
6037 .string_quoted_fold_style(FoldStyle::Auto),
6038 ).unwrap();
6039 assert!(rendered.contains("/ "), "expected fold: {rendered}");
6040 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
6041 assert_eq!(reparsed, json(r#"{"k":"aaaaa/bbbbbcccccc"}"#));
6042 }
6043
6044 #[test]
6045 fn quoted_fold_none_does_not_fold() {
6046 let value = TjsonValue::from(json(r#"{"kk":"aaaaabbbbbcccccdddd"}"#));
6049 let rendered = render_string_with_options(
6050 &value,
6051 TjsonOptions::default()
6052 .wrap_width(Some(20))
6053 .bare_strings(BareStyle::None)
6054 .bare_keys(BareStyle::None)
6055 .string_quoted_fold_style(FoldStyle::None),
6056 ).unwrap();
6057 assert!(rendered.contains('"'), "must be quoted");
6058 assert!(!rendered.contains("/ "), "None fold style must not fold: {rendered}");
6059 }
6060
6061 #[test]
6062 fn quoted_fold_fixed_folds_and_roundtrips() {
6063 let value = TjsonValue::from(json(r#"{"k":"aaaaabbbbbcccccdd"}"#));
6066 let rendered = render_string_with_options(
6067 &value,
6068 TjsonOptions::default()
6069 .wrap_width(Some(20))
6070 .bare_strings(BareStyle::None)
6071 .string_quoted_fold_style(FoldStyle::Fixed),
6072 ).unwrap();
6073 assert!(rendered.contains("/ "), "Fixed must fold: {rendered}");
6074 assert!(!rendered.contains('`'), "must be a JSON string fold, not multiline");
6075 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
6076 assert_eq!(reparsed, json(r#"{"k":"aaaaabbbbbcccccdd"}"#));
6077 }
6078
6079 #[test]
6080 fn quoted_fold_auto_folds_at_single_space() {
6081 let value = TjsonValue::from(json(r#"{"k":"aaaaa bbbbbccccc"}"#));
6085 let rendered = render_string_with_options(
6086 &value,
6087 TjsonOptions::default()
6088 .wrap_width(Some(20))
6089 .bare_strings(BareStyle::None)
6090 .string_quoted_fold_style(FoldStyle::Auto),
6091 ).unwrap();
6092 assert!(rendered.contains("/ "), "Auto must fold: {rendered}");
6093 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
6094 assert_eq!(reparsed, json(r#"{"k":"aaaaa bbbbbccccc"}"#));
6095 }
6096
6097 #[test]
6098 fn multiline_fold_none_does_not_fold_body_lines() {
6099 let value = TjsonValue::String("aaaaabbbbbcccccdddddeeeeefff\nsecond".to_owned());
6101 let rendered = render_string_with_options(
6102 &value,
6103 TjsonOptions::default()
6104 .wrap_width(Some(20))
6105 .string_multiline_fold_style(FoldStyle::None),
6106 ).unwrap();
6107 assert!(rendered.contains('`'), "must be multiline");
6108 assert!(rendered.contains("aaaaabbbbbcccccddddd"), "body must not be folded: {rendered}");
6109 }
6110
6111 #[test]
6112 fn fold_style_none_on_all_types_produces_no_fold_continuations() {
6113 let value = TjsonValue::from(json(r#"{"a":"aaaaa bbbbbccccc","b":"x,y,z abcdefghij"}"#));
6115 let rendered = render_string_with_options(
6116 &value,
6117 TjsonOptions::default()
6118 .wrap_width(Some(20))
6119 .string_bare_fold_style(FoldStyle::None)
6120 .string_quoted_fold_style(FoldStyle::None)
6121 .string_multiline_fold_style(FoldStyle::None),
6122 ).unwrap();
6123 assert!(!rendered.contains("/ "), "no fold continuations expected: {rendered}");
6124 }
6125
6126 #[test]
6127 fn number_fold_none_does_not_fold() {
6128 let value = TjsonValue::Number("123456789012345678901234".parse().unwrap());
6130 let rendered = value.to_tjson_with(
6131 TjsonOptions::default()
6132 .wrap_width(Some(20))
6133 .number_fold_style(FoldStyle::None),
6134 ).unwrap();
6135 assert!(!rendered.contains("/ "), "expected no fold: {rendered}");
6136 assert!(rendered.contains("123456789012345678901234"), "must contain full number: {rendered}");
6137 }
6138
6139 #[test]
6140 fn number_fold_fixed_splits_between_digits() {
6141 let value = TjsonValue::Number("123456789012345678901234".parse().unwrap());
6143 let rendered = value.to_tjson_with(
6144 TjsonOptions::default()
6145 .wrap_width(Some(20))
6146 .number_fold_style(FoldStyle::Fixed),
6147 ).unwrap();
6148 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
6149 let reparsed = rendered.parse::<TjsonValue>().unwrap();
6150 assert_eq!(reparsed, TjsonValue::Number("123456789012345678901234".parse().unwrap()),
6151 "roundtrip must recover original number");
6152 }
6153
6154 #[test]
6155 fn number_fold_auto_prefers_decimal_point() {
6156 let value = TjsonValue::Number("1234567890123456789.01".parse().unwrap());
6160 let rendered = value.to_tjson_with(
6161 TjsonOptions::default()
6162 .wrap_width(Some(20))
6163 .number_fold_style(FoldStyle::Auto),
6164 ).unwrap();
6165 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
6166 let first_line = rendered.lines().next().unwrap();
6167 assert!(first_line.ends_with("1234567890123456789"), "should fold before `.`: {rendered}");
6168 let reparsed = rendered.parse::<TjsonValue>().unwrap();
6169 assert_eq!(reparsed, TjsonValue::Number("1234567890123456789.01".parse().unwrap()),
6170 "roundtrip must recover original number");
6171 }
6172
6173 #[test]
6174 fn number_fold_auto_prefers_exponent() {
6175 let value = TjsonValue::Number("1.23456789012345678e+97".parse().unwrap());
6179 let rendered = value.to_tjson_with(
6180 TjsonOptions::default()
6181 .wrap_width(Some(20))
6182 .number_fold_style(FoldStyle::Auto),
6183 ).unwrap();
6184 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
6185 let first_line = rendered.lines().next().unwrap();
6186 assert!(first_line.ends_with("1.23456789012345678"), "should fold before `e`: {rendered}");
6187 let reparsed = rendered.parse::<TjsonValue>().unwrap();
6188 assert_eq!(reparsed, TjsonValue::Number("1.23456789012345678e+97".parse().unwrap()),
6189 "roundtrip must recover original number");
6190 }
6191
6192 #[test]
6193 fn number_fold_auto_folds_before_decimal_point() {
6194 let value = TjsonValue::Number("1234567890123456789.01".parse().unwrap());
6198 let rendered = value.to_tjson_with(
6199 TjsonOptions::default()
6200 .wrap_width(Some(20))
6201 .number_fold_style(FoldStyle::Auto),
6202 ).unwrap();
6203 assert!(rendered.contains("/ "), "expected fold: {rendered}");
6204 let first_line = rendered.lines().next().unwrap();
6205 assert!(first_line.ends_with("1234567890123456789"),
6206 "should fold before '.': {rendered}");
6207 let cont_line = rendered.lines().nth(1).unwrap();
6208 assert!(cont_line.starts_with("/ ."),
6209 "continuation must start with '/ .': {rendered}");
6210 let reparsed = rendered.parse::<TjsonValue>().unwrap();
6211 assert_eq!(reparsed, TjsonValue::Number("1234567890123456789.01".parse().unwrap()),
6212 "roundtrip must recover original number");
6213 }
6214
6215 #[test]
6216 fn number_fold_auto_folds_before_exponent() {
6217 let value = TjsonValue::Number("1.23456789012345678e+97".parse().unwrap());
6221 let rendered = value.to_tjson_with(
6222 TjsonOptions::default()
6223 .wrap_width(Some(20))
6224 .number_fold_style(FoldStyle::Auto),
6225 ).unwrap();
6226 assert!(rendered.contains("/ "), "expected fold: {rendered}");
6227 let first_line = rendered.lines().next().unwrap();
6228 assert!(first_line.ends_with("1.23456789012345678"),
6229 "should fold before 'e': {rendered}");
6230 let cont_line = rendered.lines().nth(1).unwrap();
6231 assert!(cont_line.starts_with("/ e"),
6232 "continuation must start with '/ e': {rendered}");
6233 let reparsed = rendered.parse::<TjsonValue>().unwrap();
6234 assert_eq!(reparsed, TjsonValue::Number("1.23456789012345678e+97".parse().unwrap()),
6235 "roundtrip must recover original number");
6236 }
6237
6238 #[test]
6239 fn number_fold_fixed_splits_at_wrap_boundary() {
6240 let value = TjsonValue::Number("123456789012345678901".parse().unwrap());
6243 let rendered = value.to_tjson_with(
6244 TjsonOptions::default()
6245 .wrap_width(Some(20))
6246 .number_fold_style(FoldStyle::Fixed),
6247 ).unwrap();
6248 assert!(rendered.contains("/ "), "expected fold: {rendered}");
6249 let first_line = rendered.lines().next().unwrap();
6250 assert_eq!(first_line, "12345678901234567890",
6251 "fixed fold must split exactly at wrap=20: {rendered}");
6252 let reparsed = rendered.parse::<TjsonValue>().unwrap();
6253 assert_eq!(reparsed, TjsonValue::Number("123456789012345678901".parse().unwrap()),
6254 "roundtrip must recover original number");
6255 }
6256
6257 #[test]
6258 fn number_fold_auto_falls_back_to_digit_split() {
6259 let value = TjsonValue::Number("123456789012345678901234".parse().unwrap());
6262 let rendered = value.to_tjson_with(
6263 TjsonOptions::default()
6264 .wrap_width(Some(20))
6265 .number_fold_style(FoldStyle::Auto),
6266 ).unwrap();
6267 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
6268 let first_line = rendered.lines().next().unwrap();
6269 assert_eq!(first_line, "12345678901234567890",
6270 "auto fallback must split at digit boundary at wrap=20: {rendered}");
6271 let reparsed = rendered.parse::<TjsonValue>().unwrap();
6272 assert_eq!(reparsed, TjsonValue::Number("123456789012345678901234".parse().unwrap()),
6273 "roundtrip must recover original number");
6274 }
6275
6276 #[test]
6277 fn bare_key_fold_fixed_folds_and_roundtrips() {
6278 let value = TjsonValue::from(json(r#"{"abcdefghijklmnopqrst":1}"#));
6281 let rendered = value.to_tjson_with(
6282 TjsonOptions::default()
6283 .wrap_width(Some(15))
6284 .string_bare_fold_style(FoldStyle::Fixed),
6285 ).unwrap();
6286 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
6287 let reparsed = rendered.parse::<TjsonValue>().unwrap().to_json().unwrap();
6288 assert_eq!(reparsed, json(r#"{"abcdefghijklmnopqrst":1}"#),
6289 "roundtrip must recover original key");
6290 }
6291
6292 #[test]
6293 fn bare_key_fold_none_does_not_fold() {
6294 let value = TjsonValue::from(json(r#"{"abcdefghijklmnopqrst":1}"#));
6296 let rendered = value.to_tjson_with(
6297 TjsonOptions::default()
6298 .wrap_width(Some(15))
6299 .string_bare_fold_style(FoldStyle::None),
6300 ).unwrap();
6301 assert!(!rendered.contains("/ "), "expected no fold: {rendered}");
6302 }
6303
6304 #[test]
6305 fn quoted_key_fold_fixed_folds_and_roundtrips() {
6306 let value = TjsonValue::from(json(r#"{"abcdefghijklmnop":1}"#));
6310 let rendered = value.to_tjson_with(
6311 TjsonOptions::default()
6312 .wrap_width(Some(15))
6313 .bare_keys(BareStyle::None)
6314 .string_quoted_fold_style(FoldStyle::Fixed),
6315 ).unwrap();
6316 assert!(rendered.contains("/ "), "expected fold continuation: {rendered}");
6317 let reparsed = rendered.parse::<TjsonValue>().unwrap().to_json().unwrap();
6318 assert_eq!(reparsed, json(r#"{"abcdefghijklmnop":1}"#),
6319 "roundtrip must recover original key");
6320 }
6321
6322 #[test]
6323 fn round_trips_generated_examples() {
6324 let values = [
6325 json("{\"a\":5,\"6\":\"fred\",\"xy\":[],\"de\":{},\"e\":[1]}"),
6326 json("{\"nested\":[[1],[2,3],{\"x\":\"y\"}],\"empty\":[],\"text\":\"plain english\"}"),
6327 json("{\"note\":\"first\\nsecond\\n indented\"}"),
6328 json(
6329 "[{\"a\":1,\"b\":\"x\",\"c\":true},{\"a\":2,\"b\":\"y\",\"c\":false},{\"a\":3,\"b\":\"z\",\"c\":null}]",
6330 ),
6331 ];
6332 for value in values {
6333 let rendered = render_string(&TjsonValue::from(value.clone())).unwrap();
6334 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
6335 assert_eq!(reparsed, value);
6336 }
6337 }
6338
6339 #[test]
6340 fn keeps_key_order_at_the_ast_and_json_boundary() {
6341 let input = " first:1\n second:2\n third:3";
6342 let value = parse_str(input).unwrap();
6343 match &value {
6344 TjsonValue::Object(entries) => {
6345 let keys = entries
6346 .iter()
6347 .map(|(key, _)| key.as_str())
6348 .collect::<Vec<_>>();
6349 assert_eq!(keys, vec!["first", "second", "third"]);
6350 }
6351 other => panic!("expected an object, found {other:?}"),
6352 }
6353 let json = value.to_json().unwrap();
6354 let keys = json
6355 .as_object()
6356 .unwrap()
6357 .keys()
6358 .map(String::as_str)
6359 .collect::<Vec<_>>();
6360 assert_eq!(keys, vec!["first", "second", "third"]);
6361 }
6362
6363 #[test]
6364 fn duplicate_keys_are_localized_to_the_json_boundary() {
6365 let input = " dup:1\n dup:2\n keep:3";
6366 let value = parse_str(input).unwrap();
6367 match &value {
6368 TjsonValue::Object(entries) => assert_eq!(entries.len(), 3),
6369 other => panic!("expected an object, found {other:?}"),
6370 }
6371 let json_value = value.to_json().unwrap();
6372 assert_eq!(json_value, json("{\"dup\":2,\"keep\":3}"));
6373 }
6374
6375 #[test]
6378 fn expand_indent_adjustments_noops_when_no_glyph_present() {
6379 let input = " a:1\n b:2\n";
6380 assert_eq!(expand_indent_adjustments(input), input);
6381 }
6382
6383 #[test]
6384 fn expand_indent_adjustments_removes_opener_and_re_indents_content() {
6385 let input = " outer: /<\n |a |b |\n | x | y |\n />\n sib:1\n";
6387 let result = expand_indent_adjustments(input);
6388 let expected = " outer:\n |a |b |\n | x | y |\n sib:1\n";
6393 assert_eq!(result, expected);
6394 }
6395
6396 #[test]
6397 fn expand_indent_adjustments_handles_nested_opener() {
6398 let input = " a: /<\n b: /<\n c:1\n />\n d:2\n />\n e:3\n";
6400 let result = expand_indent_adjustments(input);
6401 let expected = " a:\n b:\n c:1\n d:2\n e:3\n";
6409 assert_eq!(result, expected);
6410 }
6411
6412 #[test]
6413 fn parses_indent_offset_table() {
6414 let input = concat!(
6416 " outer:\n",
6417 " h: /<\n",
6418 " |name |score |\n",
6419 " | Alice |100 |\n",
6420 " | Bob |200 |\n",
6421 " | Carol |300 |\n",
6422 " />\n",
6423 " sib: value\n",
6424 );
6425 let value = parse_str(input).unwrap().to_json().unwrap();
6426 let expected = serde_json::json!({
6427 "outer": {
6428 "h": [
6429 {"name": "Alice", "score": 100},
6430 {"name": "Bob", "score": 200},
6431 {"name": "Carol", "score": 300},
6432 ],
6433 "sib": "value"
6434 }
6435 });
6436 assert_eq!(value, expected);
6437 }
6438
6439 #[test]
6440 fn parses_indent_offset_deep_nesting() {
6441 let input = concat!(
6443 " a:\n",
6444 " b: /<\n",
6445 " c: /<\n",
6446 " d:99\n",
6447 " />\n",
6448 " e:42\n",
6449 " />\n",
6450 " f:1\n",
6451 );
6452 let value = parse_str(input).unwrap().to_json().unwrap();
6453 let expected = serde_json::json!({
6456 "a": {"b": {"c": {"d": 99}, "e": 42}},
6457 "f": 1
6458 });
6459 assert_eq!(value, expected);
6460 }
6461
6462 #[test]
6463 fn renderer_uses_indent_offset_for_deep_tables_that_overflow() {
6464 let deep_table_json = r#"{
6467 "a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":[
6468 {"c1":"really long value 1","c2":"somewhat long val 1","c3":"another long val 12"},
6469 {"c1":"row two c1 value","c2":"row two c2 value","c3":"row two c3 value"},
6470 {"c1":"row three c1 val","c2":"row three c2 val","c3":"row three c3 val"}
6471 ]}}}}}}}}
6472 "#;
6473 let value = TjsonValue::from(serde_json::from_str::<JsonValue>(deep_table_json).unwrap());
6474 let rendered = render_string_with_options(
6475 &value,
6476 TjsonOptions {
6477 wrap_width: Some(80),
6478 ..TjsonOptions::default()
6479 },
6480 )
6481 .unwrap();
6482 assert!(
6483 rendered.contains(" /<"),
6484 "expected /< in rendered output:\n{rendered}"
6485 );
6486 assert!(
6487 rendered.contains("/>"),
6488 "expected /> in rendered output:\n{rendered}"
6489 );
6490 let reparsed = parse_str(&rendered).unwrap().to_json().unwrap();
6492 let original = value.to_json().unwrap();
6493 assert_eq!(reparsed, original);
6494 }
6495
6496 #[test]
6497 fn renderer_does_not_use_indent_offset_with_unlimited_wrap() {
6498 let deep_table_json = r#"{
6499 "a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":[
6500 {"c1":"really long value 1","c2":"somewhat long val 1","c3":"another long val 12"},
6501 {"c1":"row two c1 value","c2":"row two c2 value","c3":"row two c3 value"},
6502 {"c1":"row three c1 val","c2":"row three c2 val","c3":"row three c3 val"}
6503 ]}}}}}}}}
6504 "#;
6505 let value = TjsonValue::from(serde_json::from_str::<JsonValue>(deep_table_json).unwrap());
6506 let rendered = render_string_with_options(
6507 &value,
6508 TjsonOptions {
6509 wrap_width: None, ..TjsonOptions::default()
6511 },
6512 )
6513 .unwrap();
6514 assert!(
6515 !rendered.contains(" /<"),
6516 "expected no /< with unlimited wrap:\n{rendered}"
6517 );
6518 }
6519
6520 fn deep3_table_value() -> TjsonValue {
6525 TjsonValue::from(serde_json::from_str::<JsonValue>(r#"{
6526 "a":{"b":{"c":[
6527 {"col1":"value one here","col2":"value two here","col3":"value three here"},
6528 {"col1":"row two col1","col2":"row two col2","col3":"row two col3"},
6529 {"col1":"row three c1","col2":"row three c2","col3":"row three c3"}
6530 ]}}}"#).unwrap())
6531 }
6532
6533 #[test]
6534 fn table_unindent_style_none_never_uses_glyphs() {
6535 let rendered = render_string_with_options(
6537 &deep3_table_value(),
6538 TjsonOptions::default()
6539 .wrap_width(Some(50))
6540 .table_unindent_style(TableUnindentStyle::None),
6541 ).unwrap();
6542 assert!(!rendered.contains("/<"), "None must not use indent glyphs: {rendered}");
6543 }
6544
6545 #[test]
6546 fn table_unindent_style_left_always_uses_glyphs_when_fits_at_zero() {
6547 let rendered = render_string_with_options(
6550 &deep3_table_value(),
6551 TjsonOptions::default()
6552 .wrap_width(None)
6553 .table_unindent_style(TableUnindentStyle::Left),
6554 ).unwrap();
6555 assert!(rendered.contains("/<"), "Left must always use indent glyphs: {rendered}");
6556 let reparsed = rendered.parse::<TjsonValue>().unwrap().to_json().unwrap();
6557 assert_eq!(reparsed, deep3_table_value().to_json().unwrap());
6558 }
6559
6560 #[test]
6561 fn table_unindent_style_auto_uses_glyphs_only_on_overflow() {
6562 let value = deep3_table_value();
6563 let wide = render_string_with_options(
6565 &value,
6566 TjsonOptions::default()
6567 .wrap_width(None)
6568 .table_unindent_style(TableUnindentStyle::Auto),
6569 ).unwrap();
6570 assert!(!wide.contains("/<"), "Auto must not use glyphs when table fits: {wide}");
6571
6572 let narrow = render_string_with_options(
6574 &value,
6575 TjsonOptions::default()
6576 .wrap_width(Some(60))
6577 .table_unindent_style(TableUnindentStyle::Auto),
6578 ).unwrap();
6579 assert!(narrow.contains("/<"), "Auto must use glyphs on overflow: {narrow}");
6580 let reparsed = narrow.parse::<TjsonValue>().unwrap().to_json().unwrap();
6581 assert_eq!(reparsed, value.to_json().unwrap());
6582 }
6583
6584 #[test]
6585 fn table_unindent_style_floating_pushes_minimum_needed() {
6586 let value = deep3_table_value();
6592 let rendered = render_string_with_options(
6593 &value,
6594 TjsonOptions::default()
6595 .wrap_width(Some(65))
6596 .table_unindent_style(TableUnindentStyle::Floating),
6597 ).unwrap();
6598 if rendered.contains("/<") {
6602 let row_line = rendered.lines().find(|l| l.contains('|') && !l.contains("/<") && !l.contains("/>")).unwrap_or("");
6603 let row_indent = row_line.len() - row_line.trim_start().len();
6604 assert!(row_indent > 2, "Floating must not push all the way to indent 0: {rendered}");
6605 }
6606 let reparsed = rendered.parse::<TjsonValue>().unwrap().to_json().unwrap();
6607 assert_eq!(reparsed, value.to_json().unwrap());
6608 }
6609
6610 #[test]
6611 fn table_unindent_style_none_with_indent_glyph_none_also_no_glyphs() {
6612 let rendered = render_string_with_options(
6614 &deep3_table_value(),
6615 TjsonOptions::default()
6616 .wrap_width(Some(50))
6617 .table_unindent_style(TableUnindentStyle::None)
6618 .indent_glyph_style(IndentGlyphStyle::None),
6619 ).unwrap();
6620 assert!(!rendered.contains("/<"), "must not use indent glyphs: {rendered}");
6621 }
6622
6623 #[test]
6624 fn table_unindent_style_left_independent_of_indent_glyph_none() {
6625 let rendered = render_string_with_options(
6627 &deep3_table_value(),
6628 TjsonOptions::default()
6629 .wrap_width(None)
6630 .table_unindent_style(TableUnindentStyle::Left)
6631 .indent_glyph_style(IndentGlyphStyle::None),
6632 ).unwrap();
6633 assert!(rendered.contains("/<"), "table_unindent_style=Left must still fire with indent_glyph_style=None: {rendered}");
6634 }
6635
6636 #[test]
6637 fn renderer_does_not_use_indent_offset_when_indent_is_small() {
6638 let json_str = r#"{"h":[
6640 {"c1":"really long value 1","c2":"somewhat long val 1","c3":"another long val 12"},
6641 {"c1":"row two c1 value","c2":"row two c2 value","c3":"row two c3 value"},
6642 {"c1":"row three c1 val","c2":"row three c2 val","c3":"row three c3 val"}
6643 ]}"#;
6644 let value = TjsonValue::from(serde_json::from_str::<JsonValue>(json_str).unwrap());
6645 let rendered = render_string_with_options(
6646 &value,
6647 TjsonOptions {
6648 wrap_width: Some(80),
6649 ..TjsonOptions::default()
6650 },
6651 )
6652 .unwrap();
6653 assert!(
6654 !rendered.contains(" /<"),
6655 "expected no /< when indent is small:\n{rendered}"
6656 );
6657 }
6658
6659 #[test]
6660 fn tjson_config_camel_case_enums() {
6661 let c: TjsonConfig = serde_json::from_str(r#"{"stringArrayStyle":"preferSpaces","multilineStyle":"boldFloating"}"#).unwrap();
6663 assert_eq!(c.string_array_style, Some(StringArrayStyle::PreferSpaces));
6664 assert_eq!(c.multiline_style, Some(MultilineStyle::BoldFloating));
6665
6666 let c: TjsonConfig = serde_json::from_str(r#"{"stringArrayStyle":"PreferComma","multilineStyle":"FoldingQuotes"}"#).unwrap();
6668 assert_eq!(c.string_array_style, Some(StringArrayStyle::PreferComma));
6669 assert_eq!(c.multiline_style, Some(MultilineStyle::FoldingQuotes));
6670
6671 let c: TjsonConfig = serde_json::from_str(r#"{
6673 "bareStrings": "prefer",
6674 "numberFoldStyle": "auto",
6675 "indentGlyphStyle": "fixed",
6676 "tableUnindentStyle": "floating",
6677 "indentGlyphMarkerStyle": "compact"
6678 }"#).unwrap();
6679 assert_eq!(c.bare_strings, Some(BareStyle::Prefer));
6680 assert_eq!(c.number_fold_style, Some(FoldStyle::Auto));
6681 assert_eq!(c.indent_glyph_style, Some(IndentGlyphStyle::Fixed));
6682 assert_eq!(c.table_unindent_style, Some(TableUnindentStyle::Floating));
6683 assert_eq!(c.indent_glyph_marker_style, Some(IndentGlyphMarkerStyle::Compact));
6684 }
6685}