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