Skip to main content

tjson/
lib.rs

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
13/// The minimum accepted wrap width. Values below this are clamped by [`TjsonOptions::wrap_width`]
14/// and rejected by [`TjsonOptions::wrap_width_checked`].
15pub const MIN_WRAP_WIDTH: usize = 20;
16const MIN_FOLD_CONTINUATION: usize = 10;
17
18/// Controls when `/<` / `/>` indent-offset glyphs are emitted to push content to visual indent 0.
19///
20/// - `Auto` (default): apply glyphs when content would overflow `wrap_width` at its natural indent,
21///   or when the indent depth exceeds half of `wrap_width`.
22/// - `Fixed`: always apply glyphs once the indent depth exceeds a threshold, without waiting for overflow.
23/// - `None`: never apply glyphs; content may overflow `wrap_width`.
24#[non_exhaustive]
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
26pub enum IndentGlyphStyle {
27    /// Apply glyphs reactively on overflow, or proactively at half of `wrap_width`.
28    #[default]
29    Auto,
30    /// Always apply glyphs past a fixed indent threshold, regardless of overflow.
31    Fixed,
32    /// Never apply indent-offset glyphs.
33    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/// Controls how the `/<` opening glyph of an indent-offset block is placed.
51#[non_exhaustive]
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
53pub enum IndentGlyphMarkerStyle {
54    /// `/<` trails the key on the same line: `key: /<` (default).
55    #[default]
56    Compact,
57    /// `/<` appears on its own line at the key's indent level:
58    /// ```text
59    /// key:
60    ///  /<
61    /// ```
62    Separate,
63    // Like `Separate`, but with additional context info after `/<` (reserved for future use).
64    // Currently emits the same output as `Separate`.
65    // TODO: WISHLIST: decide what info to include with Marked (depth, key path, …)
66    //Marked,
67}
68
69/// Internal resolved glyph algorithm. Mapped from [`IndentGlyphStyle`] by `indent_glyph_mode()`.
70/// Not part of the public API — use [`IndentGlyphStyle`] and [`TjsonOptions`] instead.
71#[derive(Clone, Copy, Debug, PartialEq)]
72#[allow(dead_code)]
73enum IndentGlyphMode {
74    /// Fire based on pure geometry: `pair_indent × line_count >= threshold × w²`
75    IndentWeighted(f64),
76    /// Fire based on content density: `pair_indent × byte_count >= threshold × w²`
77    /// 
78    /// Not yet used on purpose, but planned for later.
79    ByteWeighted(f64),
80    /// Fire whenever `pair_indent >= w / 2`
81    Fixed,
82    /// Never fire
83    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/// Controls how tables are horizontally repositioned using `/< />` indent-offset glyphs.
95///
96/// The overflow decision is always made against the table as rendered at its natural indent,
97/// before any table-fold continuations are applied.
98#[non_exhaustive]
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
100pub enum TableUnindentStyle {
101    /// Push the table to visual indent 0 using `/< />` glyphs, unless already there.
102    /// Applies regardless of `wrap_width`.
103    Left,
104    /// Push to visual indent 0 only when the table overflows `wrap_width` at its natural
105    /// indent. If the table would still overflow even at indent 0, glyphs are not used.
106    /// With unlimited width this is effectively `None`. Default.
107    #[default]
108    Auto,
109    /// Push left by the minimum amount needed to fit within `wrap_width` — not necessarily
110    /// all the way to 0. If the table fits at its natural indent, nothing moves. With
111    /// unlimited width this is effectively `None`.
112    Floating,
113    /// Never apply indent-offset glyphs to tables, even if the table overflows `wrap_width`
114    /// or would otherwise not be rendered.
115    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/// Options controlling how TJSON is rendered. Use [`TjsonOptions::default`] for sensible
142/// defaults, or [`TjsonOptions::canonical`] for a compact, diff-friendly format.
143/// All fields are set via builder methods.
144#[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/// Controls how long strings are folded across lines using `/ ` continuation markers.
176///
177/// - `Auto` (default): prefer folding immediately after EOL characters, and at whitespace to word boundaries to fit `wrap_width`.
178/// - `Fixed`: fold right at, or if it violates specification (e.g. not between two data characters), immediately before, `wrap_width`.
179/// - `None`: do not fold, even if it means overflowing past `wrap_width`.
180#[non_exhaustive]
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
182pub enum FoldStyle {
183    /// Prefer folding immediately after EOL characters, and at whitespace to word
184    /// boundaries to fit `wrap_width`.
185    #[default]
186    Auto,
187    /// Fold right at, or if it violates specification (e.g. not between two data
188    /// characters), immediately before, `wrap_width`.
189    Fixed,
190    /// Do not fold, even if it means overflowing past `wrap_width`.
191    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/// Controls which multiline string format is preferred when rendering strings with newlines.
210///
211/// Only affects strings that contain at least one EOL (LF or CRLF). Single-line strings
212/// always follow the normal `bare_strings` / `string_quoted_fold_style` options.
213///
214/// - `Bold` (` `` `, default): double backtick, body always at col 2. Always safe.
215/// - `Floating` (`` ` ``): single backtick, body at `n+2`. Falls back to `Bold` when any content
216///   line would overflow `wrap_width` at that indent, or when the string exceeds
217///   `multiline_max_lines`, or when content is pipe-heavy / backtick-starting.
218/// - `BoldFloating` (` `` `): double backtick, body at `n+2` when it fits, col 2 when it overflows.
219/// - `Transparent` (` ``` `): triple backtick, body at col 0. Falls back to `Bold` when content is
220///   pipe-heavy or has backtick-starting lines (visually unsafe in that format).
221/// - `Light` (`` ` `` or ` `` `): prefers `` ` ``; falls back to ` `` ` like `Floating`, but the
222///   fallback reason differs — see variant doc for details.
223/// - `FoldingQuotes` (JSON string with `/ ` folds): never uses any multiline string format.
224///   Renders EOL-containing strings as folded JSON strings. When the encoded string is within
225///   25 % of `wrap_width` from fitting, it is emitted unfolded (overrunning the limit is
226///   preferred over a fold that saves almost nothing).
227#[non_exhaustive]
228#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
229pub enum MultilineStyle {
230    /// `` ` `` with body at `n+2`; falls back to `Bold` on overflow or excessive length.
231    Floating,
232    /// ` `` ` with body always at col 2.
233    #[default]
234    Bold,
235    /// ` `` ` with body at `n+2` when it fits, col 2 when any line overflows `wrap_width`.
236    BoldFloating,
237    /// ` ``` ` with body at col 0; falls back to `Bold` when content is pipe-heavy or
238    /// starts with backtick characters. `string_multiline_fold_style` has no effect here —
239    /// `/ ` continuations are not allowed inside triple-backtick blocks.
240    Transparent,
241    /// `` ` `` preferred; falls back to ` `` ` only when content looks like TJSON markers
242    /// (pipe-heavy or backtick-starting lines). Width overflow and line count do NOT trigger
243    /// fallback — a long `` ` `` is preferred over the heavier ` `` ` format.
244    Light,
245    /// Always a JSON string for EOL-containing strings; folds with `/ ` to fit `wrap_width`
246    /// unless the overrun is within 25 % of `wrap_width`.
247    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/// Controls whether bare (unquoted) strings and keys are preferred.
269///
270/// - `Prefer` (default): use bare strings/keys when the value is safe to represent without quotes.
271/// - `None`: always quote strings and keys.
272#[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/// Controls how arrays of short strings are packed onto a single line.
295///
296/// - `Spaces`: always separate with spaces (e.g. `[ a  b  c`).
297/// - `PreferSpaces`: use spaces when it fits, fall back to block layout.
298/// - `Comma`: always separate with commas (e.g. `[ a, b, c`).
299/// - `PreferComma` (default): use commas when it fits, fall back to block layout.
300/// - `None`: never pack string arrays onto one line.
301#[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    /// Returns options that produce canonical TJSON: one key-value pair per line,
331    /// no inline packing, no tables, no multiline strings, no folding.
332    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    /// When true, force explicit `[` / `{` markers even for non-empty arrays and objects
349    /// that would normally use implicit (marker-free) layout. Default is false.
350    pub fn force_markers(mut self, force_markers: bool) -> Self {
351        self.force_markers = force_markers;
352        self
353    }
354
355    /// Controls whether string values are rendered bare (unquoted) when possible. Default is `Prefer`.
356    pub fn bare_strings(mut self, bare_strings: BareStyle) -> Self {
357        self.bare_strings = bare_strings;
358        self
359    }
360
361    /// Controls whether object keys are rendered bare (unquoted) when possible. Default is `Prefer`.
362    pub fn bare_keys(mut self, bare_keys: BareStyle) -> Self {
363        self.bare_keys = bare_keys;
364        self
365    }
366
367    /// When true, pack small objects onto a single line when they fit within `wrap_width`. Default is true.
368    pub fn inline_objects(mut self, inline_objects: bool) -> Self {
369        self.inline_objects = inline_objects;
370        self
371    }
372
373    /// When true, pack small arrays onto a single line when they fit within `wrap_width`. Default is true.
374    pub fn inline_arrays(mut self, inline_arrays: bool) -> Self {
375        self.inline_arrays = inline_arrays;
376        self
377    }
378
379    /// Controls how arrays of short strings are packed. Default is `PreferComma`.
380    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    /// When true, render homogeneous arrays of objects as pipe tables when they meet the
386    /// minimum row, column, and similarity thresholds. Default is true.
387    pub fn tables(mut self, tables: bool) -> Self {
388        self.tables = tables;
389        self
390    }
391
392    /// Set the wrap width. `None` means no wrap limit (infinite width). Values below 20 are
393    /// clamped to 20 — use [`wrap_width_checked`](Self::wrap_width_checked) if you want an
394    /// error instead.
395    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    /// Set the wrap width with validation. `None` means no wrap limit (infinite width).
401    /// Returns an error if the value is `Some(n)` where `n < 10`.
402    /// Use [`wrap_width`](Self::wrap_width) if you want clamping instead.
403    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    /// Minimum number of data rows an array must have to be rendered as a table. Default is 3.
412    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    /// Minimum number of columns a table must have to be rendered as a pipe table. Default is 3.
418    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    /// Minimum cell-fill fraction required for table rendering. Computed as
424    /// `filled_cells / (rows × columns)` where `filled_cells` is the count of
425    /// (row, column) pairs where the row's object actually has that key. A value
426    /// of 1.0 requires every row to have every column; 0.0 allows fully sparse
427    /// tables. Range 0.0–1.0; default is 0.8.
428    pub fn table_min_similarity(mut self, v: f32) -> Self {
429        self.table_min_similarity = v;
430        self
431    }
432
433    /// Maximum rendered width (in characters) of any single table column. Default is 40.
434    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    /// Fold style for numbers. `Auto` folds before `.`/`e`/`E` first, then between digits.
440    /// `Fixed` folds between any two digits at the wrap limit. Default is `None`.
441    pub fn number_fold_style(mut self, style: FoldStyle) -> Self {
442        self.number_fold_style = style;
443        self
444    }
445
446    /// Fold style for bare strings. Default is `Auto`.
447    pub fn string_bare_fold_style(mut self, style: FoldStyle) -> Self {
448        self.string_bare_fold_style = style;
449        self
450    }
451
452    /// Fold style for quoted strings. Default is `Auto`.
453    pub fn string_quoted_fold_style(mut self, style: FoldStyle) -> Self {
454        self.string_quoted_fold_style = style;
455        self
456    }
457
458    /// Fold style within `` ` `` and ` `` ` multiline string bodies. Default is `None`.
459    ///
460    /// Note: ` ``` ` (`Transparent`) multilines cannot fold regardless of this setting —
461    /// the spec does not allow `/ ` continuations inside triple-backtick blocks.
462    pub fn string_multiline_fold_style(mut self, style: FoldStyle) -> Self {
463        self.string_multiline_fold_style = style;
464        self
465    }
466
467    /// When true, emit `\ ` fold continuations for wide table cells. Off by default —
468    /// the spec notes that table folds are almost always a bad idea.
469    pub fn table_fold(mut self, table_fold: bool) -> Self {
470        self.table_fold = table_fold;
471        self
472    }
473
474    /// Controls table horizontal repositioning via `/< />` indent-offset glyphs. Default is `Auto`.
475    ///
476    /// Note: [`indent_glyph_style`](Self::indent_glyph_style) must not be `None` for glyphs
477    /// to appear — `table_unindent_style` decides *when* to unindent; `indent_glyph_style`
478    /// decides whether glyphs are permitted at all.
479    pub fn table_unindent_style(mut self, style: TableUnindentStyle) -> Self {
480        self.table_unindent_style = style;
481        self
482    }
483
484    /// Controls when `/<` / `/>` indent-offset glyphs are applied. Default is `Auto`.
485    pub fn indent_glyph_style(mut self, style: IndentGlyphStyle) -> Self {
486        self.indent_glyph_style = style;
487        self
488    }
489
490    /// Controls how the `/<` opening glyph is placed relative to its key. Default is `Compact`.
491    pub fn indent_glyph_marker_style(mut self, style: IndentGlyphMarkerStyle) -> Self {
492        self.indent_glyph_marker_style = style;
493        self
494    }
495
496    /// When true, render strings containing newlines using multiline syntax (`` ` ``, ` `` `, or ` ``` `).
497    /// When false, all strings are rendered as JSON strings. Default is true.
498    pub fn multiline_strings(mut self, multiline_strings: bool) -> Self {
499        self.multiline_strings = multiline_strings;
500        self
501    }
502
503    /// Preferred multiline string rendering style. Default is `Bold`.
504    pub fn multiline_style(mut self, multiline_style: MultilineStyle) -> Self {
505        self.multiline_style = multiline_style;
506        self
507    }
508
509    /// Minimum number of newlines a string must contain to be rendered as multiline.
510    /// 0 is treated as 1. Default is 1.
511    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    /// Maximum number of content lines before `Floating` falls back to `Bold`. 0 means no limit. Default is 10.
517    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/// A camelCase-deserializable options bag for WASM/JS and test configs.
556/// Not part of the public Rust API — use [`TjsonOptions`] directly in Rust code.
557#[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/// A parsed TJSON value. Mirrors the JSON type system with the same six variants.
620///
621/// Numbers are stored as strings to preserve exact representation. Objects are stored as
622/// an ordered `Vec` of key-value pairs, which allows duplicate keys at the data structure
623/// level (though JSON and TJSON parsers typically deduplicate them).
624#[derive(Clone, Debug, PartialEq, Eq)]
625pub enum TjsonValue {
626    /// JSON `null`.
627    Null,
628    /// JSON boolean.
629    Bool(bool),
630    /// JSON number.
631    Number(serde_json::Number),
632    /// JSON string.
633    String(String),
634    /// JSON array.
635    Array(Vec<TjsonValue>),
636    /// JSON object, as an ordered list of key-value pairs.
637    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    /// Render this value as a TJSON string using the given options.
666    ///
667    /// Currently this is effectively infallible in practice — when options conflict or
668    /// content cannot be laid out ideally (e.g. `wrap_width` too narrow with folding
669    /// disabled), the renderer overflows rather than failing. The `Result` return type
670    /// is intentional and forward-looking: a future option like `fail_on_overflow`
671    /// could request strict layout enforcement and return an error rather than overflowing.
672    /// Keeping `Result` here avoids a breaking API change when that option is added.
673    /// At that point `Error` would likely gain a dedicated variant for layout constraint
674    /// failures, distinct from the existing `Error::Render` (malformed data).
675    pub fn to_tjson_with(&self, options: TjsonOptions) -> Result<String> {
676        Renderer::render(self, &options)
677    }
678
679    /// Convert this value to a `serde_json::Value`. If the value contains duplicate object keys,
680    /// only the last value for each key is kept (serde_json maps deduplicate on insert).
681    ///
682    /// ```
683    /// use tjson::TjsonValue;
684    ///
685    /// let json: serde_json::Value = serde_json::json!({"name": "Alice"});
686    /// let tjson = TjsonValue::from(json.clone());
687    /// assert_eq!(tjson.to_json().unwrap(), json);
688    /// ```
689    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
745/// ```
746/// let v: tjson::TjsonValue = "  name: Alice".parse().unwrap();
747/// assert!(matches!(v, tjson::TjsonValue::Object(_)));
748/// ```
749impl 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/// A parse error with source location and optional source line context.
758///
759/// The `Display` implementation formats the error as `line N, column M: message` and,
760/// when source context is available, appends the source line and a caret pointer.
761#[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    /// 1-based line number where the error occurred.
781    pub fn line(&self) -> usize { self.line }
782    /// 1-based column number where the error occurred.
783    pub fn column(&self) -> usize { self.column }
784    /// Human-readable error message.
785    pub fn message(&self) -> &str { &self.message }
786    /// The source line text, if available, for display with a caret pointer.
787    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/// The error type for all TJSON operations.
803#[non_exhaustive]
804#[derive(Debug)]
805pub enum Error {
806    /// A parse error with source location.
807    Parse(ParseError),
808    /// A JSON serialization or deserialization error from serde_json.
809    Json(serde_json::Error),
810    /// A render error (e.g. invalid number representation).
811    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
838/// Convenience `Result` type with [`Error`] as the default error type.
839pub 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
854/// Parse a TJSON string and deserialize it into `T` using serde.
855///
856/// ```
857/// #[derive(serde::Deserialize, PartialEq, Debug)]
858/// struct Person { name: String, city: String }
859///
860/// let p: Person = tjson::from_str("  name: Alice  city: London").unwrap();
861/// assert_eq!(p, Person { name: "Alice".into(), city: "London".into() });
862/// ```
863pub 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
876/// Serialize `value` to a TJSON string using default options.
877///
878/// ```
879/// #[derive(serde::Serialize)]
880/// struct Person { name: &'static str }
881///
882/// let s = tjson::to_string(&Person { name: "Alice" }).unwrap();
883/// assert_eq!(s, "  name: Alice");
884/// ```
885pub fn to_string<T: Serialize>(value: &T) -> Result<String> {
886    to_string_with(value, TjsonOptions::default())
887}
888
889/// Serialize `value` to a TJSON string using the given options.
890///
891/// ```
892/// let s = tjson::to_string_with(&vec![1, 2, 3], tjson::TjsonOptions::default()).unwrap();
893/// assert_eq!(s, "  1, 2, 3");
894/// ```
895pub 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            // Collect fold continuation lines: `\ ` marker at pair_indent (elem_indent - 2),
1076            // two characters to the left of the opening `|` per spec.
1077            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; // skip leading |
1118        cells
1119            .into_iter()
1120            .map(|cell| {
1121                let cell_col = col;
1122                col += cell.len() + 1; // +1 for the | separator
1123                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        // Fold after colon: value starts on a "/ " continuation line at pair_indent.
1307        // Spec: key and basic value are folded as a single unit; fold marker is allowed
1308        // immediately after the ":" (preferred), treating the junction at pair_indent+2 indent.
1309        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            // Bare strings have a leading space, so they sit at elem_indent+1.
1381            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                    // Table header is embedded in this marker chain line.
1499                    // The '|' sits at deepest_parent_indent + 2 + any padding spaces.
1500                    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    /// Parse an object key, returning `(key_string, rest_after_colon)`.
1532    /// Handles fold continuations (`/ `) for both bare keys and JSON string keys.
1533    fn parse_key(
1534        &mut self,
1535        content: &str,
1536        fold_indent: usize,
1537    ) -> std::result::Result<(String, String), ParseError> {
1538        // Bare key on this line
1539        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            // Bare key fills the whole line — look for fold continuations
1544            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; // point to last fold line; caller will +1
1564                        return Ok((key_acc, cont[colon_pos + 1..].to_owned()));
1565                    }
1566                    key_acc.push_str(cont);
1567                }
1568            }
1569        }
1570        // JSON string key on this line
1571        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        // JSON string key that doesn't close on this line — look for fold continuations
1576        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; // point to last fold line; caller will +1
1596                        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                // Check for fold continuations when the bare string fills the rest of the content
1640                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                // Check for fold continuations when the number fills the rest of the line
1693                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        // Closer must exactly match opener glyph including any explicit suffix
1764        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        // Only reject tabs in the leading indent — tabs inside quoted string values are allowed.
1994        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            // Bare key fills the whole line — a fold continuation may carry the colon
2014            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        // JSON string that doesn't close on this line — fold continuation may complete it
2022        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    /// A flat inline token string (number, null, bool, short string, empty array/object).
2058    /// Also carries the original value for lone-overflow fold fallback.
2059    Inline(String, TjsonValue),
2060    /// A block element (multiline string, nonempty array, nonempty object) that interrupts
2061    /// packing. Carries the original value; rendered lazily at the right continuation indent.
2062    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                    // First entry or wrap exceeded: fall through to render_object_entry
2127                    // so folding and other per-entry logic can apply.
2128                }
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        // Key fold lines — last line gets ":" appended before the value.
2159        // Bare keys use string_bare_fold_style; quoted keys use string_quoted_fold_style.
2160        // Only the first (standalone) key on a line is ever folded; inline-packed keys
2161        // are not candidates (they are rendered via render_inline_object_token, not here).
2162        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            // Key itself folds across multiple lines. Determine available space on the last fold
2173            // line (after appending ":") and attach the value there or as a fold continuation.
2174            let last_fold_line = fold_lines.last().unwrap();
2175            // last_fold_line is like "  / lastpart" — pair_indent + "/ " + content.
2176            // Available width after appending ":" = wrap_width - last_fold_line.len() - 1
2177            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            // Check if the value suffix fits on the last fold line, or needs its own continuation
2186            if suffix.is_empty() || after_colon_avail >= suffix.len() {
2187                // Value fits (or is empty: non-scalar like arrays/objects start on the next line)
2188                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                // Value doesn't fit on the last key fold line — fold after colon
2193                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    /// Render a scalar value's lines for use as fold-after-colon continuation(s).
2207    /// The first line uses `first_line_extra = 2` (the "/ " prefix overhead) so that
2208    /// content is correctly fitted to `wrap_width - pair_indent - 2 - (leading space if bare)`.
2209    /// The caller prefixes the first element's content (after stripping `pair_indent`) with "/ ".
2210    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..]; // " value" for bare string, "value" for others
2335
2336                // Check if "key: value" assembled first line overflows wrap_width.
2337                // If so, and key fold is enabled, fold after the colon: key on its own line,
2338                // value as a "/ " continuation at pair_indent.
2339                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                // Two consecutive tables: the second needs a `[ ` marker to separate them.
2421                let first = elem_lines.first().unwrap();
2422                let stripped = &first[elem_indent..]; // e.g. "|col  |..."
2423                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                // Always prepend "[ " — render_explicit_array always needs its marker,
2440                // whether the elements render as a table or in any other form.
2441                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        // FoldingQuotes: for EOL-containing strings, always use folded JSON string —
2577        // checked before the multiline block so it short-circuits even if multiline_strings=false.
2578        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            // parts.len() - 1 == number of EOLs in value
2595            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                // Content safety checks shared across all styles
2600                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                // Whether any content line overflows wrap_width at indent+2
2611                let overflows_at_natural = wrap
2612                    .map(|w| parts.iter().any(|p| indent + 2 + p.len() > w))
2613                    .unwrap_or(false);
2614
2615                // Whether line count exceeds the configured maximum
2616                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                        // Fall back to `` when content is unsafe OR would exceed width/line-count
2628                        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                        // Fall back to `` only when content looks like TJSON markers (pipe-heavy /
2638                        // backtick-starting). Width overflow and line count do NOT trigger fallback —
2639                        // Light prefers a long ` over a heavy ``.
2640                        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    /// Render a multiline string using ` (single backtick, unmarked body at indent+2).
2683    /// Body lines are at indent+2. Fold continuations (if enabled) at indent.
2684    /// No folding is allowed when fold_style is None.
2685    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    /// Render a multiline string using `` (double backtick, pipe-guarded body).
2720    /// Body lines are at body_indent with `| ` prefix. Fold continuations at body_indent-2.
2721    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        // Available width for body content: wrap_width minus the `| ` prefix (2 chars) and body_indent
2732        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    /// Render a multiline string using ``` (triple backtick, body at col 0).
2757    /// No folding is allowed in ``` format per spec.
2758    /// Currently not invoked by the default selection heuristic; available for explicit use.
2759    #[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                // Multiline strings are block elements — cannot be packed inline.
2913                TjsonValue::String(text) if text.contains('\n') || text.contains('\r') => {
2914                    PackedToken::Block(value.clone())
2915                }
2916                // Nonempty arrays and objects are block elements.
2917                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                // Inline string: force JSON quoting for comma-like chars to avoid parse ambiguity.
2922                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                // All other scalars (null, bool, number, empty array, empty object).
2932                _ => {
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    /// Try to fold a lone-overflow inline token value into multiple lines.
2944    /// Returns `Some(lines)` (with 2+ lines) when fold succeeded, `None` when it didn't
2945    /// (value fits or fold is disabled / below MIN_FOLD_CONTINUATION).
2946    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        // Spaces mode is incompatible with block elements (which are never strings).
2987        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        // `current` is the line being built. `current_is_fresh` is true when nothing
2995        // has been appended to `current` yet (it holds only the line prefix).
2996        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                    // Flush the current line if it has content, then render the block.
3004                    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                    // Merge the first block line with the current prefix.
3025                    // block_lines[0] is indented at continuation_indent; strip that and
3026                    // prepend whichever prefix we're currently using.
3027                    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                        // Place the token on the fresh line (first_prefix or continuation).
3045                        current.push_str(&token_str);
3046                        current_is_fresh = false;
3047
3048                        // Lone-overflow check: the token alone already exceeds the width.
3049                        if !fits_wrap(options, &current) {
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                                // Attach the real line prefix to the first fold line.
3062                                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                            // else: overflow accepted — `current` retains the long line.
3077                        }
3078                    } else {
3079                        // Try to pack the token onto the current line.
3080                        let candidate = format!("{current}{separator}{token_str}");
3081                        if fits_wrap(options, &candidate) {
3082                            current = candidate;
3083                        } else {
3084                            // Flush current line, move token to a fresh continuation line.
3085                            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                            // Lone-overflow check on the new continuation line.
3093                            if !fits_wrap(options, &current)
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                                // else: overflow accepted.
3113                        }
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        // Build column order from the first row, then verify all rows use the same order
3139        // for their shared keys. Differing key order would silently reorder keys on
3140        // round-trip — that is data loss, not a similarity issue.
3141        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            // Check that shared keys appear in the same relative order as in the first row.
3160            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        // pair_indent for fold marker is two to the left of the `|` on each row
3237        let pair_indent = parent_indent; // elem rows at parent_indent+2, fold at parent_indent
3238        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                // Check if any cell exceeds table_column_max_width and fold if so.
3253                // The fold splits the row line at a point within a cell's string value,
3254                // between the first and last data character (not between `|` and value start).
3255                // For simplicity, fold the whole row line at the wrap boundary.
3256                let fold_avail = options
3257                    .wrap_width
3258                    .unwrap_or(usize::MAX)
3259                    .saturating_sub(pair_indent + 2); // content after `  ` row prefix
3260                if row_line.len() > fold_avail + pair_indent + 2 {
3261                    // Find a fold point: must be within a cell's string data, after the
3262                    // leading space of a bare string or after the first `"` of a JSON string.
3263                    // We look for a space inside a cell value (not the cell padding spaces).
3264                    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) //matches!(value.as_str(), "true" | "false" | "null")
3296                    // '|' itself is also checked in is_pipe_like but here too for clarity
3297                    && !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
3352// Expands /< /> indent-adjustment glyphs before parsing.
3353//
3354// /< appears as the value in "key: /<" and resets the visible indent to n=0,
3355// meaning subsequent lines are rendered as if at the document root (visual
3356// indent 0).  The actual nesting depth is unchanged.
3357//
3358// /> must be alone on the line (with optional leading/trailing spaces) and
3359// restores the previous indent context.
3360//
3361// Preprocessing converts shifted lines back to their real indent so the main
3362// parser never sees /< or />.
3363fn 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    // Stack entries: (offset, expected_close_file_indent).
3370    // offset_stack.last() is the current offset; effective = file_indent + offset.
3371    // The base entry uses usize::MAX as a sentinel (no /< to close at the root level).
3372    let mut offset_stack: Vec<(usize, usize)> = vec![(0, usize::MAX)];
3373    // When a line ends with ':' and no value, it may be the first half of an own-line
3374    // /< open. Hold it here; flush it as a regular line if the next line is not " /<".
3375    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        // /> – restoration glyph: must be exactly spaces(expected_close_file_indent) + " />".
3381        // Any other indentation is not a close glyph and falls through as a regular line.
3382        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; // consume the line without emitting it
3390        }
3391
3392        // Own-line /< – a line whose trimmed content is exactly " /<" following a pending key.
3393        // The /< must be at pair_indent (= pending key's file_indent) spaces + " /<".
3394        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                // Treat as if the held key line had " /<" appended.
3402                let eff_indent = key_file_indent + current_offset;
3403                let content = &held[key_file_indent..]; // "key:"
3404                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            // Not a /< — flush the held key line as a regular line.
3410            output_lines.push(pending_key_line.take().unwrap());
3411        }
3412
3413        // /< – adjustment glyph: the trimmed line ends with " /<" and what
3414        // precedes it ends with ':' (confirming this is a key-value context,
3415        // not a multiline-string body or other content).
3416        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        // Key-only line (ends with ':' after trimming, no value after the colon):
3428        // may be the first half of an own-line /<. Hold it for one iteration.
3429        if trimmed_end.ends_with(':') && !trimmed_end.trim_start().contains(' ') {
3430            // Preserve any active offset re-indentation in the held form.
3431            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        // Regular line: re-indent if there is an active offset.
3444        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    // Flush any trailing pending key line.
3454    if let Some(held) = pending_key_line.take() { output_lines.push(held); }
3455
3456    // split('\n') produces a trailing "" for inputs that end with '\n'.
3457    // Joining that back with '\n' naturally reproduces the trailing newline,
3458    // so no explicit suffix is needed.
3459    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
3486// Returns the target parent_indent to re-render the table at when /< /> glyphs should be
3487// used, or None if no unindenting should occur.
3488//
3489// `natural_lines` are the table lines as rendered at pair_indent (spaces(pair_indent+2) prefix).
3490fn table_unindent_target(pair_indent: usize, natural_lines: &[String], options: &TjsonOptions) -> Option<usize> {
3491    // indent_glyph_style: None means glyphs are never allowed regardless of table_unindent_style.
3492    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    // data_width: widest line with the natural indent stripped
3498    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            // Always push to indent 0, unless already there.
3505            if n == 0 { None } else {
3506                // Check it fits at 0 (data_width <= w, or unlimited width).
3507                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            // Push to indent 0 only when table overflows at natural indent.
3514            // With unlimited width, never unindent.
3515            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            // Push left by the minimum amount needed to fit within wrap_width.
3523            // With unlimited width, never unindent.
3524            let w = options.wrap_width?;
3525            if max_natural <= w {
3526                return None; // already fits, no need to move
3527            }
3528            // Find the minimum parent_indent such that data_width + (parent_indent + 2) <= w.
3529            // data_width is fixed; we need parent_indent + 2 + data_width <= w.
3530            // minimum parent_indent = 0 if data_width + 2 <= w, else can't help.
3531            if data_width + 2 <= w {
3532                // Find smallest parent_indent that makes table fit.
3533                let target = w.saturating_sub(data_width + 2);
3534                // Only unindent if it actually reduces the indent.
3535                if target < n { Some(target) } else { None }
3536            } else {
3537                None // table too wide even at indent 0
3538            }
3539        }
3540    }
3541}
3542
3543/// Approximate number of output lines a value will produce. Used for glyph volume estimation.
3544/// Empty arrays and objects count as 1 (simple values); non-empty containers recurse.
3545fn 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
3555/// Rough count of content bytes in a subtree. Used to weight volume in `ByteWeighted` mode.
3556fn 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
3567/// Maximum nesting depth of non-empty containers below this value.
3568/// Empty arrays/objects count as 0 (simple values).
3569fn 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
3581/// Returns true if a `/<` indent-offset glyph should be emitted for `value` at `pair_indent`.
3582fn 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
3604/// Build the opening glyph line(s) for an indent-offset block.
3605/// Returns either `["key: /<"]` or `["key:", "INDENT /<"]` depending on options.
3606fn 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 /*| IndentGlyphMarkerStyle::Marked*/ => 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    // TJSON allows literal tab characters inside quoted strings; escape them before JSON parsing.
3683    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        //|| first == '|'
3901        || 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
4046/// Returns true if the line starts with zero or more whitespace chars then the given char.
4047fn 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
4052/// Split a multiline-string body part into segments for fold continuations.
4053/// Returns the original text as a single segment if no fold is needed.
4054/// Segments: first is the line body, rest are fold continuations (without the `/ ` prefix).
4055fn 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                // Find the last space before avail that is not a single consecutive space
4069                // (spec: bare strings may not fold immediately after a single space, but
4070                // multiline folds are within the body text so we just prefer spaces).
4071                let candidate = &rest[..avail.min(rest.len())];
4072                // Find last space boundary
4073                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        // Don't split mid-escape-sequence (keep `\x` pairs together)
4082        // Find the actual safe split point: walk back if we're in the middle of `\x`
4083        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
4093/// Find the last safe byte position to split a JSON-encoded string, not mid-escape.
4094/// `split_at` is the desired split position. May return a smaller value if `split_at`
4095/// would land in the middle of a `\uXXXX` or `\X` escape.
4096fn safe_json_split(s: &str, split_at: usize) -> usize {
4097    // Walk backwards from split_at to find the last `\` and see if split is mid-escape
4098    let bytes = s.as_bytes();
4099    let pos = split_at.min(bytes.len());
4100    // Count consecutive backslashes before pos
4101    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        // We are inside a `\X` escape — back up one more
4109        pos.saturating_sub(1)
4110    } else {
4111        pos
4112    }
4113}
4114
4115/// Attempt to fold a bare string into multiple lines with `/ ` continuations.
4116/// Returns None if folding is not needed or not possible.
4117/// The first element is the first line (`{spaces(indent)} {first_segment}`),
4118/// subsequent elements are fold lines (`{spaces(indent)}/ {segment}`).
4119fn 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    // First-line budget: indent + 1 (space before bare string) + first_line_extra + content
4128    // first_line_extra accounts for any key+colon prefix on the same line.
4129    let first_avail = w.saturating_sub(indent + 1 + first_line_extra);
4130    if value.len() <= first_avail {
4131        return None; // fits on one line, no fold needed
4132    }
4133    // Continuation budget: indent + 2 (`/ ` prefix) + content
4134    let cont_avail = w.saturating_sub(indent + 2);
4135    if cont_avail < MIN_FOLD_CONTINUATION {
4136        return None; // too little room for useful continuation content
4137    }
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        // Find a fold point
4157        let split_at = match style {
4158            FoldStyle::Auto => {
4159                // Spec: "a bare string may never be folded immediately after a single
4160                // consecutive space." Find last space boundary that isn't after a lone space.
4161                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            // No good boundary found on a continuation line — fall back to a hard cut.
4169            current_avail.min(rest.len())
4170        } else if split_at == 0 {
4171            // No fold point on the first line, or Fixed/None style — emit remainder as-is.
4172            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 // only produced one line, no actual fold
4193    } else {
4194        Some(lines)
4195    }
4196}
4197
4198/// Fold a bare key (no leading space) into multiple continuation lines.
4199/// The caller must append `:` to the last returned line.
4200/// Returns None if no fold is needed, impossible, or style is None.
4201fn 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    // key + colon fits — no fold needed
4210    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); // `/ ` prefix
4213    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
4245/// Find a fold point in a number string at or before `avail` bytes.
4246/// Auto mode: prefers splitting before `.` or `e`/`E` (keeping the semantic marker with the
4247/// continuation); falls back to splitting between any two digits at the limit.
4248/// Returns a byte offset (1..avail), or 0 if no valid point found.
4249fn 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        // Prefer the last `.` or `e`/`E` at or before avail — fold before it.
4256        let candidate = &s[..avail];
4257        if let Some(pos) = candidate.rfind(['.', 'e', 'E'])
4258            && pos > 0 {
4259                return pos; // fold before the separator
4260            }
4261    }
4262    // Fall back: split between two digit characters at the avail boundary.
4263    // Walk back to find a digit-digit boundary.
4264    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
4275/// Fold a number value into multiple lines with `/ ` continuations.
4276/// Numbers have no leading space (unlike bare strings). Returns None if no fold needed.
4277fn 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; // fits on one line
4291    }
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        // Subsequent lines use "/ " prefix
4315        let last = lines.last_mut().unwrap();
4316        // First line has no prefix adjustment; continuation lines need "/ " prefix.
4317        // Restructure: first push was the segment, now we need to wrap in continuation format.
4318        // Actually build correctly from the start:
4319        // → rebuild: first line is plain, continuations are "/ segment"
4320        // We already pushed the first segment above — fix continuation format below.
4321        let _ = last; // handled in next iteration via prefix logic
4322    }
4323    // The above loop pushes segments without "/ " prefix on continuations. Rebuild properly.
4324    // Simpler: redo with explicit first/rest tracking.
4325    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/// Character class used by [`find_bare_fold_point`] to assign break priorities.
4360#[derive(Clone, Copy, PartialEq, Eq)]
4361enum CharClass {
4362    Space,
4363    Letter,
4364    Digit,
4365    /// Punctuation that prefers to trail at the end of a line: `.` `,` `/` `-` `_` `~` `@` `:`.
4366    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
4389/// Find a fold point in a bare string candidate slice.
4390/// Returns a byte offset suitable for splitting, or 0 if none found.
4391///
4392/// `lookahead` is the character immediately after the candidate window. When provided,
4393/// the transition at `s.len()` (take the full window) is also considered as a split point.
4394///
4395/// Priorities (highest first, rightmost position within each priority wins):
4396/// 1. Before a `Space` — space moves to the next line.
4397/// 2. `StickyEnd`→`Letter`/`Digit` — punctuation trails the current line, next word starts fresh.
4398/// 3. `Letter`↔`Digit` — finer boundary within an alphanumeric run.
4399/// 4. `Letter`/`Digit`→`StickyEnd`/`Other` — weakest: word trailing into punctuation.
4400fn find_bare_fold_point(s: &str, lookahead: Option<char>) -> usize {
4401    // Track the last-seen position for each priority level (0 = highest).
4402    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                // P1: anything → Space (split before the space)
4410                (_, CharClass::Space) if byte_pos > 0 => best[0] = byte_pos,
4411                // P2: StickyEnd → Letter or Digit (after punctuation run, before a word)
4412                (CharClass::StickyEnd, CharClass::Letter | CharClass::Digit) => best[1] = byte_pos,
4413                // P3: Letter ↔ Digit
4414                (CharClass::Letter, CharClass::Digit) | (CharClass::Digit, CharClass::Letter) => {
4415                    best[2] = byte_pos
4416                }
4417                // P4: Letter/Digit → StickyEnd or Other
4418                (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    // Check the edge: transition between the last char of the window and the lookahead.
4428    // A split here means taking the full window (split_at = s.len()).
4429    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    // Return rightmost position of the highest priority found.
4448    best.into_iter().find(|&p| p > 0).unwrap_or(0)
4449}
4450
4451/// Attempt to fold a JSON-encoded string value into multiple lines with `/ ` continuations.
4452/// The output strings form a JSON string spanning multiple lines with fold markers.
4453/// Returns None if folding is not needed.
4454fn 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    // First-line budget: indent + first_line_extra + content (the encoded string including quotes)
4464    let first_avail = w.saturating_sub(indent + first_line_extra);
4465    if encoded.len() <= first_avail {
4466        return None; // fits on one line
4467    }
4468    let cont_avail = w.saturating_sub(indent + 2);
4469    if cont_avail < MIN_FOLD_CONTINUATION {
4470        return None; // too little room for useful continuation content
4471    }
4472    // The encoded string starts with `"` and ends with `"`.
4473    // We strip the outer quotes and work with the raw encoded content.
4474    let inner = &encoded[1..encoded.len() - 1]; // strip opening and closing `"`
4475    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); // -1 for the opening `"`
4479    loop {
4480        if rest.is_empty() {
4481            // Close the string: add closing `"` to the last line
4482            if let Some(last) = lines.last_mut() {
4483                last.push('"');
4484            }
4485            break;
4486        }
4487        // Adjust avail: first line has opening `"` (-1), last segment needs closing `"` (-1)
4488        let segment_avail = if rest.len() <= current_avail {
4489            // Last segment: needs room for closing `"`
4490            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        // Find fold point
4504        let split_at = match style {
4505            FoldStyle::Auto => {
4506                let candidate = &rest[..segment_avail.min(rest.len())];
4507                // Prefer to split before a space run (spec: "fold BEFORE unescaped space runs")
4508                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            // Can't fold cleanly — emit rest as final segment
4516            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            // Fix: first line should NOT have closing quote yet
4527            let last = lines.last_mut().unwrap();
4528            last.pop(); // remove the premature closing `"`
4529            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
4543/// Count consecutive backslashes immediately before `pos` in `bytes`.
4544fn 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
4554/// Find a fold point in a JSON-encoded string slice.
4555///
4556/// Priority:
4557/// 1. After an escaped EOL sequence (`\n` or `\r` in the encoded inner string) — fold after
4558///    the escape so the EOL stays with the preceding content.
4559/// 2. Before a literal space character.
4560/// 3. Safe split at end.
4561///
4562/// Returns byte offset into `s`, or 0 if no suitable point is found.
4563fn find_json_fold_point(s: &str) -> usize {
4564    let bytes = s.as_bytes();
4565
4566    // Pass 1: prefer splitting after an escaped \n (the encoded two-char sequence `\n`).
4567    // This naturally keeps \r\n together: when value has \r\n, the encoded form is `\r\n`
4568    // and we split after the `\n`, which is after the full pair.
4569    // Scan backward; return the rightmost such position that fits.
4570    let mut i = bytes.len();
4571    while i > 1 {
4572        i -= 1;
4573        if bytes[i] == b'n' && bytes[i - 1] == b'\\' {
4574            // Count the run of backslashes ending at i-1
4575            let bs = count_preceding_backslashes(bytes, i) + 1; // +1 for bytes[i-1]
4576            if bs % 2 == 1 {
4577                // Genuine \n escape — split after it
4578                return (i + 1).min(bytes.len());
4579            }
4580        }
4581    }
4582
4583    // Pass 2: split before a literal space.
4584    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    // Pass 3: fall back to any word boundary (letter-or-number ↔ other).
4596    // The encoded inner string is ASCII-compatible, so we scan for byte-level
4597    // alphanumeric transitions. Non-ASCII escaped as \uXXXX are all alphanumeric
4598    // in the encoded form so boundaries naturally occur at the leading `\`.
4599    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    // Final fallback: hard split at end.
4619    safe_json_split(s, s.len())
4620}
4621
4622/// Render an EOL-containing string as a folded JSON string (`FoldingQuotes` style).
4623///
4624/// Always folds at `\n` boundaries — each newline in the original value becomes a `/ `
4625/// continuation point. Within-piece width folding follows `string_multiline_fold_style`.
4626fn 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    // Encode each piece's inner content (no outer quotes, no \n — we add \n explicitly).
4630    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]; // strip outer quotes
4635        let nl = if is_last { "" } else { "\\n" };
4636        if i == 0 {
4637            lines.push(format!("{}\"{}{}", ind, inner, nl));
4638            if !is_last {
4639                // No closing quote yet — string continues on next line
4640            } 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        // Width-fold within this piece if the line is still too wide
4649        // and string_multiline_fold_style is not None.
4650        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                    // The piece itself overflows; leave it long — within-piece folding
4655                    // of JSON strings mid-escape is not safe to split here.
4656                    // Future: could re-fold the piece using fold_json_string.
4657                }
4658            }
4659    }
4660    lines
4661}
4662
4663/// Split a rendered table row line for a fold continuation.
4664/// The fold must happen within a cell's string value, between the first and last
4665/// data character (spec: "between the first data character... and the last data character").
4666/// Returns `(before_fold, after_fold)` or `None` if no valid fold point is found.
4667fn 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    // Walk backwards from max_len to find a split point inside a string cell.
4673    // A valid fold point is a space character that is inside a cell value
4674    // (not the padding spaces right after `|`, and not the leading space of a bare string).
4675    let scan_end = max_len.min(bytes.len());
4676    // Find the last space that is preceded by a non-space (i.e., inside content)
4677    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(); // skip the space itself
4683            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
4700/// matches a literal '|' pipe or a PIPELIKE CHARACTER
4701/// PIPELIKE CHARACTER in spec:  PIPELIKE CHARACTER DEFINITION A pipelike character is U+007C (VERTICAL LINE) or any character in the following set: U+00A6, U+01C0, U+2016, U+2223, U+2225, U+254E, U+2502, U+2503, U+2551, U+FF5C, U+FFE4
4702fn 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" | "[]" | "{}" | "\"\"") // "" is logically reserved but unreachable: '"' is quote-like and forbidden as a bare string first/last char
4709}
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    // ---- Folding tests ----
4761
4762    // JSON string folding
4763
4764    #[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        // JSON string fold inside an object value
4778        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        // Three fold lines
4789        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        // Fold continuation with leading spaces (trimmed before `/ `)
4800        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    // Bare string folding
4809
4810    #[test]
4811    fn parses_folded_bare_string_root() {
4812        // Root bare string folded across two lines
4813        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        // Bare string value folded
4824        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        // Content after `/ ` starts with a space — that space becomes part of string
4845        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    // Key folding
4854
4855    #[test]
4856    fn parses_folded_bare_key() {
4857        // A long bare key folded across two lines
4858        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        // A long quoted key folded across two lines
4869        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    // Table cell folding
4878
4879    #[test]
4880    fn parses_table_with_folded_cell() {
4881        // A table row where one cell is folded onto the next line using backslash continuation
4882        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        // Table fold where the continuation line lacks a trailing pipe
4901        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        // ``` type: content at col 0, mandatory closing glyph
4919        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        // ``` type with \r\n local EOL indicator
4930        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        // `` type: pipe-guarded content lines, mandatory closing glyph
4941        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        // `` type with \r\n local EOL indicator
4962        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        // `` type with fold continuation line
4973        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        // ` type: content at n+2, mandatory closing glyph
4984        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        // ` type with fold continuation
4995        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        // ` type preserves leading spaces after stripping n+2
5006        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        // Default: Bold style → `` with body at col 2
5072        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        // CrLf: Bold style with \r\n suffix
5083        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        // Floating: indent=0: glyph is " `", body at indent+2 (2 spaces)
5096        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        // Floating: pair_indent=2: glyph "   `", body at 4 spaces
5107        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        // Floating: pair_indent=4: glyph "     `", body at 6 spaces
5117        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        // Floating: three content lines, deeper nesting — pair_indent=6, body at 8 spaces
5130        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        // MultilineStyle::Bold → always `` with body at col 2
5143        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        // MultilineStyle::Transparent → ``` with body at col 0
5158        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        // Transparent falls back to Bold when content is pipe-heavy
5173        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        // ``` bodies must never have / continuations — it's against spec.
5188        // Even with a very narrow wrap width and a long body line, no / appears.
5189        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        // Falls back to Bold when body would need folding? Either way: no / inside the body.
5199        // Strip opener and closer lines and check no fold marker in body.
5200        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        // Explicitly setting fold style to Auto on a Transparent multiline must not fold.
5211        // The note in the doc says it's ignored for Transparent.
5212        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        // 11 lines > multiline_max_lines default of 10 → fall back from ` to ``
5226        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        // A content line longer than wrap_width - indent - 2 triggers fallback
5237        let long_line = "x".repeat(80); // exactly 80 chars: indent=0 + 2 = 82 > wrap_width=80
5238        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        // Only 2 lines, short content — stays as `
5249        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        // Width overflow does NOT trigger fallback for Light — stays as `
5272        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        // Too many lines does NOT trigger fallback for Light — stays as `
5286        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        // Pipe-heavy content IS dangerous → Light falls back to ``
5299        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        // No EOL → FoldingQuotes does not apply, normal bare string rendering
5324        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        // A string with EOL that encodes long enough to need folding.
5336        // JSON encoding of "long string with spaces that needs folding\nsecond" = 52 chars,
5337        // overrun=12 > 25% of 40=10 → fold is triggered (has spaces for fold points).
5338        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        // String whose JSON encoding slightly exceeds wrap_width=40 but by less than 25% (10).
5355        // FoldingQuotes always folds at \n boundaries regardless of line length.
5356        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        // >10% of lines start with whitespace then | → use `` instead of `
5391        // 2 out of 3 lines start with |, which is >10%
5392        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        // A content line starting with backtick triggers the backtick_start heuristic → use ``
5401        // (` ``` ` starts with a backtick, so backtick_start is true)
5402        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        // A content line starting with whitespace then any backtick forces fallback from ` to ``
5410        // (visually confusing for humans even if parseable)
5411        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        // {"a":1,"b":2} has keys [a, b]; {"b":3,"a":4} has keys [b, a].
5439        // Rendering as a table would silently reorder keys on round-trip — hard stop.
5440        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        // Row 2 is missing "c" — that's fine, it's sparse not reordered.
5450        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    // --- Fold style tests ---
5708    // Fixed and None have deterministic output — exact assertions.
5709    // Auto tests use strings with exactly one reasonable fold point (one space between
5710    // two equal-length words) so the fold position is unambiguous.
5711
5712    #[test]
5713    fn bare_fold_none_does_not_fold() {
5714        // "aaaaa bbbbb" at wrap=15 overflows (line would be 17 chars), but None means no fold.
5715        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        // "aaaaabbbbbcccccdddd" (19 chars, no spaces), wrap=20.
5728        // Line "  k: aaaaabbbbbcccccdddd" = 24 chars > 20.
5729        // first_avail = 20-2(indent)-1(space)-2(k:) = 15.
5730        // Fixed splits at 15: first="aaaaabbbbbccccc", cont="dddd".
5731        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        // "aaaaa bbbbbccccc": single space at pos 5, total 16 chars.
5747        // wrap=20: first_avail = 20-2(indent)-1(space)-2(k:) = 15. 16 > 15 → must fold.
5748        // Auto folds before the space: "aaaaa" / " bbbbbccccc".
5749        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        // "aaaaa/bbbbbccccc": StickyEnd→Letter boundary after '/' at pos 6, total 16 chars.
5762        // No spaces → P2 fires: fold after '/', slash trails the line.
5763        // wrap=20: first_avail=15. 16 > 15 → must fold. Fold at pos 6: first="aaaaa/".
5764        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        // "aa/bbbbbbbbb cccc": slash at pos 2, space at pos 11, total 17 chars.
5780        // wrap=20: first_avail=15. 17 > 15 → must fold. Space at pos 11 ≤ 15 → fold at 11.
5781        // Space pass runs first and finds pos 11 — fold before space: "aa/bbbbbbbbb" / " cccc".
5782        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        // Must fold at the space, not at the slash
5791        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        // bare_strings=None forces quoting. "aaaaa/bbbbbcccccc" has one slash boundary.
5799        // encoded = "\"aaaaa/bbbbbcccccc\"" = 19 chars. wrap=20, indent=2, key+colon=2 → first_avail=16.
5800        // 19 > 16 → folds. Word boundary before '/' at inner pos 5. Slash → unambiguous.
5801        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        // bare_strings=None and bare_keys=None force quoting of both key and value.
5817        // wrap=20 overflows ("\"kk\": \"aaaaabbbbbcccccdddd\"" = 27 chars), but fold style None means no fold.
5818        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        // bare_strings=None forces quoting. "aaaaabbbbbcccccdd" encoded = "\"aaaaabbbbbcccccdd\"" = 19 chars.
5834        // wrap=20, indent=2, key "k"+colon = 2 → first_avail = 20-2-2 = 16. 19 > 16 → folds.
5835        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        // bare_strings=None forces quoting. "aaaaa bbbbbccccc" has one space at pos 5.
5852        // encoded "\"aaaaa bbbbbccccc\"" = 18 chars. wrap=20, indent=2, key+colon=2 → first_avail=16.
5853        // 18 > 16 → folds. Auto folds before the space.
5854        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        // Body line overflows wrap but None means no fold inside multiline body.
5870        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        // With all fold styles None, no / continuations should appear anywhere.
5884        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        // number_fold_style None: long number is never folded even when it overflows wrap.
5899        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        // 24 digits, wrap=20, indent=0 → avail=20. Fixed splits at pos 20.
5912        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        // "1234567890123456789.01" (22 chars, '.' at pos 19), wrap=20, avail=20.
5927        // rfind('.') in first 20 chars = pos 19. Fold before '.'.
5928        // First line ends with the integer part.
5929        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        // "1.23456789012345678e+97" (23 chars, 'e' at pos 19), wrap=20, avail=20.
5946        // rfind('.') or 'e'/'E' in first 20 chars: '.' at 1, 'e' at 19 → picks 'e' (rightmost).
5947        // First line: "1.23456789012345678", continuation: "/ e+97".
5948        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        // "1234567890123456789.01" (22 chars, '.' at pos 19), wrap=20, avail=20.
5965        // rfind('.') in first 20 = pos 19. Fold before '.'.
5966        // First line: "1234567890123456789", continuation: "/ .01".
5967        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        // "1.23456789012345678e+97" (23 chars, 'e' at pos 19), wrap=20, avail=20.
5988        // rfind('e') in first 20 chars = pos 19. Fold before 'e'.
5989        // First line: "1.23456789012345678", continuation: "/ e+97".
5990        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        // 21 digits, wrap=20, indent=0: avail=20. Fixed splits exactly at pos 20.
6011        // First line: "12345678901234567890", continuation: "/ 1".
6012        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        // 24 digits, no '.'/`e`: auto falls back to digit-boundary split.
6030        // wrap=20, indent=0 → avail=20. Split at pos 20 (digit-digit boundary).
6031        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        // Key "abcdefghijklmnopqrst" (20 chars) + ":" = 21, indent=0, wrap=15.
6049        // Only one place to fold: at the wrap boundary between two key chars.
6050        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        // Same long key but fold style None — must not fold.
6065        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        // bare_keys=None forces quoting. Key "abcdefghijklmnop" (16 chars),
6077        // quoted = "\"abcdefghijklmnop\"" = 18 chars, indent=0, wrap=15.
6078        // Single fold at the wrap boundary.
6079        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    // ---- /< /> indent-offset tests ----
6146
6147    #[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        // pair_indent=2 ("  outer: /<"), then table at visual 2 → actual 4.
6156        let input = "  outer: /<\n  |a  |b  |\n  | x  | y  |\n   />\n  sib:1\n";
6157        let result = expand_indent_adjustments(input);
6158        // "  outer: /<" → "  outer:" (offset pushed = 2)
6159        // "  |a  |b  |" at file-indent 2 → effective 4 → "    |a  |b  |"
6160        // "   />" → pop, discarded
6161        // "  sib:1" → offset=0, unchanged
6162        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        // Two stacked /< contexts.
6169        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        // After "  a: /<": offset=2
6172        // "  b: /<" at file-indent 2 → eff=4, emit "    b:", push offset=4
6173        // "  c:1" at file-indent 2 → eff=6 → "      c:1"
6174        // "   />" → pop offset to 2
6175        // "  d:2" at file-indent 2 → eff=4 → "    d:2"
6176        // "   />" → pop offset to 0
6177        // "  e:3" unchanged
6178        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        // pair_indent=4 ("    h: /<"), table at visual 2 → actual 6.
6185        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        // Verify that a second /< context stacks correctly and /> restores it.
6212        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        // After both /> pops, offset returns to 0, so "  f:1" is at pair_indent 2 —
6224        // a sibling of "a", not inside "b".
6225        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        // 8 levels deep → pair_indent=16, n*5=80 >= w=80.
6235        // Table is wide enough to overflow at natural indent but fit at offset.
6236        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        // Round-trip: parse the rendered output and verify it matches.
6261        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, // unlimited
6280                ..TjsonOptions::default()
6281            },
6282        )
6283        .unwrap();
6284        assert!(
6285            !rendered.contains(" /<"),
6286            "expected no /< with unlimited wrap:\n{rendered}"
6287        );
6288    }
6289
6290    // --- TableUnindentStyle tests ---
6291    // Uses a 3-level-deep table that overflows at its natural indent but fits at 0.
6292    // pair_indent = 6 (3 nesting levels × 2), table rows are ~60 chars wide.
6293
6294    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        // None: never unindent even if table overflows. No /< /> in output.
6306        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        // Left: always push to indent 0 even when table fits at natural indent.
6318        // Use unlimited width so table fits naturally, but Left still unindents.
6319        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        // With wide wrap: table fits at natural indent → no glyphs.
6334        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        // With narrow wrap (60): table rows are 65 chars, overflows. data_width=57 ≤ 60 → fits at 0.
6343        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        // Floating: push left only enough to fit, not all the way to 0.
6357        // pair_indent=6, table data_width ≈ 58 chars. With wrap=70:
6358        // natural width = 6+2+58=66 ≤ 70 → fits → no glyphs.
6359        // With wrap=60: natural=66 > 60, but data_width=58 > 60-2=58 → exactly fits at target=0.
6360        // Use wrap=65: natural=66 > 65, target = 65-58-2=5 < 6=n → unindents to 5 (not 0).
6361        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        // Should use glyphs but NOT go all the way to indent 0.
6369        // If it goes to 0, rows start at indent 2 ("  |col1...").
6370        // If floating, rows are at indent > 2.
6371        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        // Both None: definitely no glyphs. Belt and suspenders.
6383        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        // indent_glyph_style=None overrides even Left — no glyphs ever.
6396        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        // pair_indent=2 → n*5=10 < w=80, so offset should never apply.
6409        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}