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