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