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