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