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