Skip to main content

tjson/
options.rs

1use std::str::FromStr;
2use serde::{Deserialize, Serialize};
3
4pub const MIN_WRAP_WIDTH: usize = 20;
5pub const DEFAULT_WRAP_WIDTH: usize = 80;
6pub(crate) const MIN_FOLD_CONTINUATION: usize = 10;
7
8/// Controls when `/<` / `/>` indent-offset glyphs are emitted to push content to visual indent 0.
9///
10/// - `Auto` (default): apply glyphs to avoid overflow and reduce screen volume, using a weighted
11///   algorithm that considers the overall shape of the object.
12/// - `Fixed`: always apply glyphs once the indent depth exceeds a threshold, without waiting for overflow.
13/// - `None`: never apply glyphs; content may overflow `wrap_width`.
14#[non_exhaustive]
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
16pub enum IndentGlyphStyle {
17    /// Apply glyphs in order to avoid overflow and save screen volume, using an
18    /// intelligent weighting algorithm that looks at the entire object shape.
19    #[default]
20    Auto,
21    /// Always apply glyphs past a fixed indent threshold, regardless of overflow.
22    Fixed,
23    /// Never apply indent-offset glyphs.
24    None,
25}
26
27impl FromStr for IndentGlyphStyle {
28    type Err = String;
29    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
30        match input {
31            "auto" => Ok(Self::Auto),
32            "fixed" => Ok(Self::Fixed),
33            "none" => Ok(Self::None),
34            _ => Err(format!(
35                "invalid indent glyph style '{input}' (expected one of: auto, fixed, none)"
36            )),
37        }
38    }
39}
40
41/// Controls how the `/<` opening glyph of an indent-offset block is placed.
42#[non_exhaustive]
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub enum IndentGlyphMarkerStyle {
45    /// `/<` trails the key on the same line: `key: /<` (default).
46    #[default]
47    Compact,
48    /// `/<` appears on its own line at the key's indent level:
49    /// ```text
50    /// key:
51    ///  /<
52    /// ```
53    Separate,
54    // Like `Separate`, but with additional context info after `/<` (reserved for future use).
55    // Currently emits the same output as `Separate`.
56    // TODO: WISHLIST: decide what info to include with Marked (depth, key path, …)
57    //Marked,
58}
59
60/// Internal resolved glyph algorithm. Mapped from [`IndentGlyphStyle`] by `indent_glyph_mode()`.
61/// Not part of the public API — use [`IndentGlyphStyle`] and [`RenderOptions`] instead.
62#[derive(Clone, Copy, Debug, PartialEq)]
63#[allow(dead_code)]
64pub(crate) enum IndentGlyphMode {
65    /// Fire based on pure geometry: `pair_indent × line_count >= threshold × w²`
66    IndentWeighted(f64),
67    /// Fire based on content density: `pair_indent × byte_count >= threshold × w²`
68    /// 
69    /// Not yet used on purpose, but planned for later.
70    ByteWeighted(f64),
71    /// Fire whenever `pair_indent >= w / 2`
72    Fixed,
73    /// Never fire
74    None,
75}
76
77pub(crate) fn indent_glyph_mode(options: &RenderOptions) -> IndentGlyphMode {
78    match options.indent_glyph_style {
79        IndentGlyphStyle::Auto  => IndentGlyphMode::IndentWeighted(0.2),
80        IndentGlyphStyle::Fixed => IndentGlyphMode::Fixed,
81        IndentGlyphStyle::None  => IndentGlyphMode::None,
82    }
83}
84
85/// Controls how tables are horizontally repositioned using `/< />` indent-offset glyphs.
86///
87/// The overflow decision is always made against the table as rendered at its natural indent,
88/// before any table-fold continuations are applied.
89#[non_exhaustive]
90#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
91pub enum TableUnindentStyle {
92    /// Push the table to visual indent 0 using `/< />` glyphs, unless already there.
93    /// Applies regardless of `wrap_width`.
94    Left,
95    /// Push to visual indent 0 only when the table overflows `wrap_width` at its natural
96    /// indent. If the table would still overflow even at indent 0, glyphs are not used.
97    /// With unlimited width this is effectively `None`. Default.
98    #[default]
99    Auto,
100    /// Push left by the minimum amount needed to fit within `wrap_width` — not necessarily
101    /// all the way to 0. If the table fits at its natural indent, nothing moves. With
102    /// unlimited width this is effectively `None`.
103    Floating,
104    /// Never apply indent-offset glyphs to tables, even if the table overflows `wrap_width`
105    /// or would otherwise not be rendered.
106    None,
107}
108
109impl FromStr for TableUnindentStyle {
110    type Err = String;
111    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
112        match input {
113            "left"     => Ok(Self::Left),
114            "auto"     => Ok(Self::Auto),
115            "floating" => Ok(Self::Floating),
116            "none"     => Ok(Self::None),
117            _ => Err(format!(
118                "invalid table unindent style '{input}' (expected one of: left, auto, floating, none)"
119            )),
120        }
121    }
122}
123
124
125#[non_exhaustive]
126#[allow(dead_code)]
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(default)]
129struct ParseOptions {
130    start_indent: usize,
131}
132
133/// Options controlling how TJSON is rendered. Use [`RenderOptions::default`] for sensible
134/// defaults, or [`RenderOptions::canonical`] for a compact, diff-friendly format.
135/// All fields are set via builder methods.
136#[non_exhaustive]
137#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
138#[serde(default)]
139pub struct RenderOptions {
140    pub(crate) wrap_width: Option<usize>,
141    pub(crate) start_indent: usize,
142    pub(crate) force_markers: bool,
143    pub(crate) bare_strings: BareStyle,
144    pub(crate) bare_keys: BareStyle,
145    pub(crate) inline_objects: bool,
146    pub(crate) inline_arrays: bool,
147    pub(crate) string_array_style: StringArrayStyle,
148    pub(crate) number_fold_style: FoldStyle,
149    pub(crate) string_bare_fold_style: FoldStyle,
150    pub(crate) string_quoted_fold_style: FoldStyle,
151    pub(crate) string_multiline_fold_style: FoldStyle,
152    pub(crate) tables: bool,
153    pub(crate) table_fold: bool,
154    pub(crate) table_unindent_style: TableUnindentStyle,
155    pub(crate) indent_glyph_style: IndentGlyphStyle,
156    pub(crate) indent_glyph_marker_style: IndentGlyphMarkerStyle,
157    pub(crate) table_min_rows: usize,
158    pub(crate) table_min_columns: usize,
159    pub(crate) table_min_similarity: f32,
160    pub(crate) table_column_max_width: Option<usize>,
161    /// Undocumented. Use at your own risk — may be discontinued at any time.
162    pub(crate) kv_pack_multiple: usize,
163    pub(crate) multiline_strings: bool,
164    pub(crate) multiline_style: MultilineStyle,
165    pub(crate) multiline_min_lines: usize,
166    pub(crate) multiline_max_lines: usize,
167}
168
169/// Controls how long strings are folded across lines using `/ ` continuation markers.
170///
171/// - `Auto` (default): prefer folding immediately after EOL characters, and at whitespace to word boundaries to fit `wrap_width`.
172/// - `Fixed`: fold right at, or if it violates specification (e.g. not between two data characters), immediately before, `wrap_width`.
173/// - `None`: do not fold, even if it means overflowing past `wrap_width`.
174#[non_exhaustive]
175#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
176pub enum FoldStyle {
177    /// Prefer folding immediately after EOL characters, and immediately before
178    /// whitespace boundaries to fit `wrap_width`.
179    #[default]
180    Auto,
181    /// Fold right at, or if it violates specification (e.g. not between two data
182    /// characters), immediately before, `wrap_width`.
183    Fixed,
184    /// Do not fold, even if it means overflowing past `wrap_width`.
185    None,
186}
187
188impl FromStr for FoldStyle {
189    type Err = String;
190
191    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
192        match input {
193            "auto" => Ok(Self::Auto),
194            "fixed" => Ok(Self::Fixed),
195            "none" => Ok(Self::None),
196            _ => Err(format!(
197                "invalid fold style '{input}' (expected one of: auto, fixed, none)"
198            )),
199        }
200    }
201}
202
203/// Controls which multiline string format is preferred when rendering strings with newlines.
204///
205/// Only affects strings that contain at least one EOL (LF or CRLF). Single-line strings
206/// always follow the normal `bare_strings` / `string_quoted_fold_style` options.
207///
208/// - `Bold` (` `` `, default): body pinned to col 2, each content line begins with `| `. Always safe.
209/// - `Floating` (`` ` ``): single backtick, body at natural indent `n+2`. Falls back to `Bold`
210///   (col 2) on overflow, when the string exceeds `multiline_max_lines`, or when content is
211///   pipe-heavy / backtick-starting.
212/// - `BoldFloating` (` `` `): same format as `Bold`; body at natural indent `n+2` when it fits,
213///   otherwise falls back to col 2.
214/// - `Transparent` (` ``` `): triple backtick, body at col 0. Falls back to `Bold` when content is
215///   pipe-heavy or has backtick-starting lines (visually unsafe in that format).
216/// - `Light` (`` ` `` or ` `` `): prefers `` ` ``; falls back to ` `` ` like `Floating`, but the
217///   fallback reason differs — see variant doc for details.
218/// - `FoldingQuotes` (JSON string with `/ ` folds): never uses any multiline string format.
219///   Renders EOL-containing strings as folded JSON strings. When the encoded string is within
220///   25 % of `wrap_width` from fitting, it is emitted unfolded (overrunning the limit is
221///   preferred over a fold that saves almost nothing).
222#[non_exhaustive]
223#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
224pub enum MultilineStyle {
225    /// Single-backtick (`` ` ``); body at natural indent `n+2`. Falls back to `Bold` (col 2)
226    /// on overflow, excessive length, or pipe-heavy / backtick-starting content.
227    Floating,
228    /// ` `` `: body at col 2, each content line begins with `| `. Always safe.
229    #[default]
230    Bold,
231    /// Same ` `` ` format as `Bold`; body at natural indent `n+2` when it fits within
232    /// `wrap_width`, otherwise falls back to col 2.
233    BoldFloating,
234    /// ` ``` ` with body at col 0; falls back to `Bold` when content is pipe-heavy or
235    /// starts with backtick characters. `string_multiline_fold_style` has no effect here —
236    /// `/ ` continuations are not allowed inside triple-backtick blocks.
237    Transparent,
238    /// `` ` `` preferred; falls back to ` `` ` only when content looks like TJSON markers
239    /// (pipe-heavy or backtick-starting lines). Width overflow and line count do NOT trigger
240    /// fallback — a long `` ` `` is preferred over the heavier ` `` ` format.
241    Light,
242    /// Always a JSON string for EOL-containing strings; folds with `/ ` to fit `wrap_width`
243    /// unless the overrun is within 25 % of `wrap_width`.
244    FoldingQuotes,
245}
246
247impl FromStr for MultilineStyle {
248    type Err = String;
249
250    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
251        match input {
252            "bold" => Ok(Self::Bold),
253            "floating" => Ok(Self::Floating),
254            "bold-floating" => Ok(Self::BoldFloating),
255            "transparent" => Ok(Self::Transparent),
256            "light" => Ok(Self::Light),
257            "folding-quotes" => Ok(Self::FoldingQuotes),
258            _ => Err(format!(
259                "invalid multiline style '{input}' (expected one of: bold, floating, bold-floating, transparent, light, folding-quotes)"
260            )),
261        }
262    }
263}
264
265/// Controls whether bare (unquoted) strings and keys are preferred.
266///
267/// - `Prefer` (default): use bare strings/keys when the value is safe to represent without quotes.
268/// - `None`: always quote strings and keys.
269#[non_exhaustive]
270#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
271pub enum BareStyle {
272    #[default]
273    Prefer,
274    None,
275}
276
277impl FromStr for BareStyle {
278    type Err = String;
279
280    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
281        match input {
282            "prefer" => Ok(Self::Prefer),
283            "none" => Ok(Self::None),
284            _ => Err(format!(
285                "invalid bare style '{input}' (expected one of: prefer, none)"
286            )),
287        }
288    }
289}
290
291/// Controls how arrays of short strings are packed onto a single line.
292///
293/// - `Spaces`: always separate with spaces (e.g. `[ a  b  c`).
294/// - `PreferSpaces`: use spaces when it fits, fall back to block layout.
295/// - `Comma`: always separate with commas (e.g. `[ a, b, c`).
296/// - `PreferComma` (default): use commas when it fits, fall back to block layout.
297/// - `None`: never pack string arrays onto one line.
298#[non_exhaustive]
299#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
300pub enum StringArrayStyle {
301    Spaces,
302    PreferSpaces,
303    Comma,
304    #[default]
305    PreferComma,
306    None,
307}
308
309impl FromStr for StringArrayStyle {
310    type Err = String;
311
312    fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
313        match input {
314            "spaces" => Ok(Self::Spaces),
315            "prefer-spaces" => Ok(Self::PreferSpaces),
316            "comma" => Ok(Self::Comma),
317            "prefer-comma" => Ok(Self::PreferComma),
318            "none" => Ok(Self::None),
319            _ => Err(format!(
320                "invalid string array style '{input}' (expected one of: spaces, prefer-spaces, comma, prefer-comma, none)"
321            )),
322        }
323    }
324}
325
326impl RenderOptions {
327    /// Returns options that produce canonical TJSON: one key-value pair per line,
328    /// no inline packing, no tables, no multiline strings, no folding.
329    pub fn canonical() -> Self {
330        Self {
331            inline_objects: false,
332            inline_arrays: false,
333            string_array_style: StringArrayStyle::None,
334            tables: false,
335            multiline_strings: false,
336            number_fold_style: FoldStyle::None,
337            string_bare_fold_style: FoldStyle::None,
338            string_quoted_fold_style: FoldStyle::None,
339            string_multiline_fold_style: FoldStyle::None,
340            indent_glyph_style: IndentGlyphStyle::None,
341            ..Self::default()
342        }
343    }
344
345    /// When true, force explicit `[` / `{` indent markers even for a only a single n+2
346    /// indent jump at a time, that would normally have an implicit indent marker.
347    /// Normally, we only use markers when we jump at least two indent steps at once (n+2, n+2 again).
348    /// Default is false.
349    pub fn force_markers(mut self, force_markers: bool) -> Self {
350        self.force_markers = force_markers;
351        self
352    }
353
354    /// Controls whether string values use bare string format or JSON quoted strings. `Prefer` uses
355    /// bare strings whenever the spec permits; `None` always uses JSON quoted strings. Default is `Prefer`.
356    pub fn bare_strings(mut self, bare_strings: BareStyle) -> Self {
357        self.bare_strings = bare_strings;
358        self
359    }
360
361    /// Controls whether object keys use bare key format or JSON quoted strings. `Prefer` uses
362    /// bare keys whenever the spec permits; `None` always uses JSON quoted strings. Default is `Prefer`.
363    pub fn bare_keys(mut self, bare_keys: BareStyle) -> Self {
364        self.bare_keys = bare_keys;
365        self
366    }
367
368    /// When true, pack small objects onto a single line when they fit within `wrap_width`. Default is true.
369    pub fn inline_objects(mut self, inline_objects: bool) -> Self {
370        self.inline_objects = inline_objects;
371        self
372    }
373
374    /// When true, pack small arrays onto a single line when they fit within `wrap_width`. Default is true.
375    pub fn inline_arrays(mut self, inline_arrays: bool) -> Self {
376        self.inline_arrays = inline_arrays;
377        self
378    }
379
380    /// Controls how arrays where every element is a string are packed onto a single line.
381    /// Has no effect on arrays that contain any non-string values. Default is `PreferComma`.
382    pub fn string_array_style(mut self, string_array_style: StringArrayStyle) -> Self {
383        self.string_array_style = string_array_style;
384        self
385    }
386
387    /// When true, render homogeneous arrays of objects as pipe tables when they meet the
388    /// minimum row, column, and similarity thresholds. Default is true.
389    pub fn tables(mut self, tables: bool) -> Self {
390        self.tables = tables;
391        self
392    }
393
394    /// Set the wrap width. `None` means no wrap limit (infinite width). Values below 20 are
395    /// clamped to 20 — use [`wrap_width_checked`](Self::wrap_width_checked) if you want an
396    /// error instead.
397    pub fn wrap_width(mut self, wrap_width: Option<usize>) -> Self {
398        self.wrap_width = wrap_width.map(|w| w.clamp(MIN_WRAP_WIDTH, usize::MAX));
399        self
400    }
401
402    /// Set the wrap width with validation. `None` means no wrap limit (infinite width).
403    /// Returns an error if the value is `Some(n)` where `n < 20`.
404    /// Use [`wrap_width`](Self::wrap_width) if you want clamping instead.
405    pub fn wrap_width_checked(self, wrap_width: Option<usize>) -> std::result::Result<Self, String> {
406        if let Some(w) = wrap_width
407            && w < MIN_WRAP_WIDTH {
408                return Err(format!("wrap_width must be at least {MIN_WRAP_WIDTH}, got {w}"));
409            }
410        Ok(self.wrap_width(wrap_width))
411    }
412
413    /// Minimum number of data rows an array must have to be rendered as a table. Default is 3.
414    pub fn table_min_rows(mut self, table_min_rows: usize) -> Self {
415        self.table_min_rows = table_min_rows;
416        self
417    }
418
419    /// Minimum number of columns a table must have to be rendered as a pipe table. Default is 3.
420    pub fn table_min_columns(mut self, table_min_columns: usize) -> Self {
421        self.table_min_columns = table_min_columns;
422        self
423    }
424
425    /// Minimum cell-fill fraction required for table rendering. Computed as
426    /// `filled_cells / (rows × columns)` where `filled_cells` is the count of
427    /// (row, column) pairs where the row's object actually has that key. A value
428    /// of 1.0 requires every row to have every column; 0.0 allows fully sparse
429    /// tables. Range 0.0–1.0; default is 0.8.
430    pub fn table_min_similarity(mut self, v: f32) -> Self {
431        self.table_min_similarity = v;
432        self
433    }
434
435    /// If any column's content width (including the leading space on bare string values) exceeds
436    /// this value, the table is abandoned entirely and falls back to block layout.
437    /// `None` means no limit. Default is `Some(40)`.
438    pub fn table_column_max_width(mut self, table_column_max_width: Option<usize>) -> Self {
439        self.table_column_max_width = table_column_max_width;
440        self
441    }
442
443    /// Undocumented. Use at your own risk — may be discontinued at any time.
444    /// Valid values are 1–4; returns an error otherwise.
445    pub fn kv_pack_multiple(mut self, v: usize) -> std::result::Result<Self, String> {
446        if !(1..=4).contains(&v) {
447            return Err(format!("kv_pack_multiple must be 1–4, got {v}"));
448        }
449        self.kv_pack_multiple = v;
450        Ok(self)
451    }
452
453    /// Undocumented. Use at your own risk — may be discontinued at any time.
454    /// Sets `kv_pack_multiple` with clamping to 1–4 instead of erroring.
455    pub fn kv_pack_multiple_clamped(mut self, v: usize) -> Self {
456        self.kv_pack_multiple = v.clamp(1, 4);
457        self
458    }
459
460    /// Set all four fold styles at once. Individual fold options override this if set after.
461    pub fn fold(self, style: FoldStyle) -> Self {
462        self.number_fold_style(style)
463            .string_bare_fold_style(style)
464            .string_quoted_fold_style(style)
465            .string_multiline_fold_style(style)
466    }
467
468    /// Fold style for numbers. `Auto` folds before `.`/`e`/`E` first, then between digits.
469    /// `Fixed` folds between any two digits at the wrap limit. Default is `Auto`.
470    pub fn number_fold_style(mut self, style: FoldStyle) -> Self {
471        self.number_fold_style = style;
472        self
473    }
474
475    /// Whether and how to fold long bare strings and bare keys across lines using `/ ` continuation
476    /// markers. Applies to both string values and object keys rendered in bare format. Default is `Auto`.
477    pub fn string_bare_fold_style(mut self, style: FoldStyle) -> Self {
478        self.string_bare_fold_style = style;
479        self
480    }
481
482    /// Whether and how to fold long quoted strings and quoted keys across lines using `/ ` continuation
483    /// markers. Applies to both string values and object keys rendered in JSON quoted format. Default is `Auto`.
484    pub fn string_quoted_fold_style(mut self, style: FoldStyle) -> Self {
485        self.string_quoted_fold_style = style;
486        self
487    }
488
489    /// Fold style within `` ` `` and ` `` ` multiline string bodies. Default is `None`.
490    ///
491    /// Note: ` ``` ` (`Transparent`) multilines cannot fold regardless of this setting —
492    /// the spec does not allow `/ ` continuations inside triple-backtick blocks.
493    pub fn string_multiline_fold_style(mut self, style: FoldStyle) -> Self {
494        self.string_multiline_fold_style = style;
495        self
496    }
497
498    /// @experimental When true, emit `/ ` fold continuations for wide table lines. Off by default;
499    /// the spec notes that table folds are almost always a bad idea.
500    pub fn table_fold(mut self, table_fold: bool) -> Self {
501        self.table_fold = table_fold;
502        self
503    }
504
505    /// Controls whether wide tables are repositioned toward the left margin using ` /<' and ` />` indent
506    /// glyphs. Default is `Auto`. This is independent of [`indent_glyph_style`](Self::indent_glyph_style).
507    pub fn table_unindent_style(mut self, style: TableUnindentStyle) -> Self {
508        self.table_unindent_style = style;
509        self
510    }
511
512    /// Controls whether deeply-nested objects and arrays are wrapped in `/< />` glyphs
513    /// and repositioned toward the left margin to reduce visual depth. Default is `Auto`.
514    ///
515    /// This applies to objects and arrays only — it is independent of table repositioning,
516    /// which is controlled by [`table_unindent_style`](Self::table_unindent_style).
517    pub fn indent_glyph_style(mut self, style: IndentGlyphStyle) -> Self {
518        self.indent_glyph_style = style;
519        self
520    }
521
522    /// Controls whether the `/<` opening glyph trails its key on the same line (`Compact`)
523    /// or appears on its own line (`Separate`). Default is `Compact`.
524    pub fn indent_glyph_marker_style(mut self, style: IndentGlyphMarkerStyle) -> Self {
525        self.indent_glyph_marker_style = style;
526        self
527    }
528
529    /// When true, render strings containing newlines using multiline syntax (`` ` ``, ` `` `, or ` ``` `).
530    /// When false, all strings are rendered as JSON strings. Default is true.
531    pub fn multiline_strings(mut self, multiline_strings: bool) -> Self {
532        self.multiline_strings = multiline_strings;
533        self
534    }
535
536    /// Selects the multiline string format: minimal (`` ` ``), bold (` `` `), or transparent (` ``` `),
537    /// each with different body positioning and fallback rules. See [`MultilineStyle`] for the full
538    /// breakdown. Default is `Bold`.
539    pub fn multiline_style(mut self, multiline_style: MultilineStyle) -> Self {
540        self.multiline_style = multiline_style;
541        self
542    }
543
544    /// Minimum number of newlines a string must contain to be rendered as multiline.
545    /// 0 is treated as 1. Default is 1.
546    pub fn multiline_min_lines(mut self, multiline_min_lines: usize) -> Self {
547        self.multiline_min_lines = multiline_min_lines;
548        self
549    }
550
551    /// Maximum number of content lines before `Floating` falls back to `Bold`. 0 means no limit. Default is 10.
552    pub fn multiline_max_lines(mut self, multiline_max_lines: usize) -> Self {
553        self.multiline_max_lines = multiline_max_lines;
554        self
555    }
556}
557
558impl Default for RenderOptions {
559    fn default() -> Self {
560        Self {
561            start_indent: 0,
562            force_markers: false,
563            bare_strings: BareStyle::Prefer,
564            bare_keys: BareStyle::Prefer,
565            inline_objects: true,
566            inline_arrays: true,
567            string_array_style: StringArrayStyle::PreferComma,
568            tables: true,
569            wrap_width: Some(DEFAULT_WRAP_WIDTH),
570            table_min_rows: 3,
571            table_min_columns: 3,
572            table_min_similarity: 0.8,
573            table_column_max_width: Some(40),
574            kv_pack_multiple: 2,
575            number_fold_style: FoldStyle::Auto,
576            string_bare_fold_style: FoldStyle::Auto,
577            string_quoted_fold_style: FoldStyle::Auto,
578            string_multiline_fold_style: FoldStyle::None,
579            table_fold: false,
580            table_unindent_style: TableUnindentStyle::Auto,
581            indent_glyph_style: IndentGlyphStyle::Auto,
582            indent_glyph_marker_style: IndentGlyphMarkerStyle::Compact,
583            multiline_strings: true,
584            multiline_style: MultilineStyle::Bold,
585            multiline_min_lines: 1,
586            multiline_max_lines: 10,
587        }
588    }
589}
590
591// Deserializers that accept camelCase (for JS/WASM) for all enum fields in TjsonConfig.
592// PascalCase (serde default) is also accepted as a fallback.
593mod camel_de {
594    use serde::{Deserialize, Deserializer};
595
596    fn de_str<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
597        Option::<String>::deserialize(d)
598    }
599
600    macro_rules! camel_option_de {
601        ($fn_name:ident, $Enum:ty, $($camel:literal => $variant:expr),+ $(,)?) => {
602            pub fn $fn_name<'de, D: Deserializer<'de>>(d: D) -> Result<Option<$Enum>, D::Error> {
603                let Some(s) = de_str(d)? else { return Ok(None); };
604                match s.as_str() {
605                    $($camel => return Ok(Some($variant)),)+
606                    _ => {}
607                }
608                // Fall back to PascalCase via serde
609                serde_json::from_value(serde_json::Value::String(s.clone()))
610                    .map(Some)
611                    .map_err(|_| serde::de::Error::unknown_variant(&s, &[$($camel),+]))
612            }
613        };
614    }
615
616    camel_option_de!(bare_style, super::BareStyle,
617        "prefer" => super::BareStyle::Prefer,
618        "none"   => super::BareStyle::None,
619    );
620
621    camel_option_de!(fold_style, super::FoldStyle,
622        "auto"  => super::FoldStyle::Auto,
623        "fixed" => super::FoldStyle::Fixed,
624        "none"  => super::FoldStyle::None,
625    );
626
627    camel_option_de!(multiline_style, super::MultilineStyle,
628        "floating"      => super::MultilineStyle::Floating,
629        "bold"          => super::MultilineStyle::Bold,
630        "boldFloating"  => super::MultilineStyle::BoldFloating,
631        "transparent"   => super::MultilineStyle::Transparent,
632        "light"         => super::MultilineStyle::Light,
633        "foldingQuotes" => super::MultilineStyle::FoldingQuotes,
634    );
635
636    camel_option_de!(table_unindent_style, super::TableUnindentStyle,
637        "left"     => super::TableUnindentStyle::Left,
638        "auto"     => super::TableUnindentStyle::Auto,
639        "floating" => super::TableUnindentStyle::Floating,
640        "none"     => super::TableUnindentStyle::None,
641    );
642
643    camel_option_de!(indent_glyph_style, super::IndentGlyphStyle,
644        "auto"  => super::IndentGlyphStyle::Auto,
645        "fixed" => super::IndentGlyphStyle::Fixed,
646        "none"  => super::IndentGlyphStyle::None,
647    );
648
649    camel_option_de!(indent_glyph_marker_style, super::IndentGlyphMarkerStyle,
650        "compact"  => super::IndentGlyphMarkerStyle::Compact,
651        "separate" => super::IndentGlyphMarkerStyle::Separate,
652    );
653
654    camel_option_de!(string_array_style, super::StringArrayStyle,
655        "spaces"       => super::StringArrayStyle::Spaces,
656        "preferSpaces" => super::StringArrayStyle::PreferSpaces,
657        "comma"        => super::StringArrayStyle::Comma,
658        "preferComma"  => super::StringArrayStyle::PreferComma,
659        "none"         => super::StringArrayStyle::None,
660    );
661}
662
663/// A camelCase-deserializable options bag for WASM/JS and test configs.
664/// Not part of the public Rust API — use [`RenderOptions`] directly in Rust code.
665#[doc(hidden)]
666#[derive(Clone, Debug, Default, Deserialize)]
667#[serde(rename_all = "camelCase", default)]
668pub struct TjsonConfig {
669    pub(crate) canonical: bool,
670    pub(crate) force_markers: Option<bool>,
671    pub(crate) wrap_width: Option<usize>,
672    #[serde(deserialize_with = "camel_de::bare_style")]
673    pub(crate) bare_strings: Option<BareStyle>,
674    #[serde(deserialize_with = "camel_de::bare_style")]
675    pub(crate) bare_keys: Option<BareStyle>,
676    pub(crate) inline_objects: Option<bool>,
677    pub(crate) inline_arrays: Option<bool>,
678    pub(crate) multiline_strings: Option<bool>,
679    #[serde(deserialize_with = "camel_de::multiline_style")]
680    pub(crate) multiline_style: Option<MultilineStyle>,
681    pub(crate) multiline_min_lines: Option<usize>,
682    pub(crate) multiline_max_lines: Option<usize>,
683    pub(crate) tables: Option<bool>,
684    pub(crate) table_fold: Option<bool>,
685    #[serde(deserialize_with = "camel_de::table_unindent_style")]
686    pub(crate) table_unindent_style: Option<TableUnindentStyle>,
687    pub(crate) table_min_rows: Option<usize>,
688    pub(crate) table_min_columns: Option<usize>,
689    pub(crate) table_min_similarity: Option<f32>,
690    pub(crate) table_column_max_width: Option<usize>,
691    #[serde(deserialize_with = "camel_de::string_array_style")]
692    pub(crate) string_array_style: Option<StringArrayStyle>,
693    #[serde(deserialize_with = "camel_de::fold_style")]
694    pub(crate) fold: Option<FoldStyle>,
695    #[serde(deserialize_with = "camel_de::fold_style")]
696    pub(crate) number_fold_style: Option<FoldStyle>,
697    #[serde(deserialize_with = "camel_de::fold_style")]
698    pub(crate) string_bare_fold_style: Option<FoldStyle>,
699    #[serde(deserialize_with = "camel_de::fold_style")]
700    pub(crate) string_quoted_fold_style: Option<FoldStyle>,
701    #[serde(deserialize_with = "camel_de::fold_style")]
702    pub(crate) string_multiline_fold_style: Option<FoldStyle>,
703    #[serde(deserialize_with = "camel_de::indent_glyph_style")]
704    pub(crate) indent_glyph_style: Option<IndentGlyphStyle>,
705    #[serde(deserialize_with = "camel_de::indent_glyph_marker_style")]
706    pub(crate) indent_glyph_marker_style: Option<IndentGlyphMarkerStyle>,
707    pub(crate) kv_pack_multiple: Option<usize>,
708}
709
710impl From<TjsonConfig> for RenderOptions {
711    fn from(c: TjsonConfig) -> Self {
712        let mut opts = if c.canonical { RenderOptions::canonical() } else { RenderOptions::default() };
713        if let Some(v) = c.force_markers      { opts = opts.force_markers(v); }
714        if let Some(w) = c.wrap_width         { opts = opts.wrap_width(if w == 0 { None } else { Some(w) }); }
715        if let Some(v) = c.bare_strings       { opts = opts.bare_strings(v); }
716        if let Some(v) = c.bare_keys          { opts = opts.bare_keys(v); }
717        if let Some(v) = c.inline_objects     { opts = opts.inline_objects(v); }
718        if let Some(v) = c.inline_arrays      { opts = opts.inline_arrays(v); }
719        if let Some(v) = c.multiline_strings  { opts = opts.multiline_strings(v); }
720        if let Some(v) = c.multiline_style    { opts = opts.multiline_style(v); }
721        if let Some(v) = c.multiline_min_lines { opts = opts.multiline_min_lines(v); }
722        if let Some(v) = c.multiline_max_lines { opts = opts.multiline_max_lines(v); }
723        if let Some(v) = c.tables             { opts = opts.tables(v); }
724        if let Some(v) = c.table_fold        { opts = opts.table_fold(v); }
725        if let Some(v) = c.table_unindent_style { opts = opts.table_unindent_style(v); }
726        if let Some(v) = c.table_min_rows     { opts = opts.table_min_rows(v); }
727        if let Some(v) = c.table_min_columns     { opts = opts.table_min_columns(v); }
728        if let Some(v) = c.table_min_similarity { opts = opts.table_min_similarity(v); }
729        if let Some(v) = c.table_column_max_width { opts = opts.table_column_max_width(if v == 0 { None } else { Some(v) }); }
730        if let Some(v) = c.string_array_style { opts = opts.string_array_style(v); }
731        if let Some(v) = c.fold               { opts = opts.fold(v); }
732        if let Some(v) = c.number_fold_style  { opts = opts.number_fold_style(v); }
733        if let Some(v) = c.string_bare_fold_style { opts = opts.string_bare_fold_style(v); }
734        if let Some(v) = c.string_quoted_fold_style { opts = opts.string_quoted_fold_style(v); }
735        if let Some(v) = c.string_multiline_fold_style { opts = opts.string_multiline_fold_style(v); }
736        if let Some(v) = c.indent_glyph_style { opts = opts.indent_glyph_style(v); }
737        if let Some(v) = c.indent_glyph_marker_style { opts = opts.indent_glyph_marker_style(v); }
738        if let Some(v) = c.kv_pack_multiple { opts = opts.kv_pack_multiple_clamped(v); }
739        opts
740    }
741}
742