% File: jsonparse.sty
% Copyright 2024 Jasper Habicht (mail(at)jasperhabicht.de).
%
% This work may be distributed and/or modified under the
% conditions of the LaTeX Project Public License version 1.3c,
% available at http://www.latex-project.org/lppl/.
%
% This file is part of the `jsonparse' package (The Work in LPPL)
% and all files in that bundle must be distributed together.
%
% This work has the LPPL maintenance status `maintained'.
%
\ProvidesExplPackage {jsonparse} {2024-12-10} {0.9.10}
  {A handy way to parse, store and access JSON data from files or strings in LaTeX documents}

\msg_new:nnn { jsonparse } { old-kernel } {
  LaTeX ~ kernel ~ too ~ old. \iow_newline:
  The ~ jsonparse ~ package ~ does ~ not ~ support ~ this ~ LaTeX ~ version. \iow_newline: 
  Please ~ update ~ to ~ a ~ newer ~ version. 
}
  
\cs_if_exist:NF \ProcessKeyOptions {
  \msg_critical:nn { jsonparse } { old-kernel }
}

% ===

\bool_new:N \l__jsonparse_debug_mode_bool

\keys_define:nn { jsonparse / global } {
  debug   .bool_set:N = \l__jsonparse_debug_mode_bool ,
  debug   .default:n  = { true } ,
  debug   .initial:n  = { false } 
}
\ProcessKeyOptions [ jsonparse / global ]

\msg_new:nnn { jsonparse } { debug-info } {
  #1
}

\msg_new:nnn { jsonparse } { parsing-error } {
  \msg_error_text:n { jsonparse } \iow_newline:
  Could ~ not ~ parse ~ JSON. \iow_newline:
  Parsing ~ error ~ at ~ key ~ `#1` ~ with ~ value ~ `#2`.
}

\msg_new:nnn { jsonparse } { nested-non-expandable } {
  \msg_error_text:n { jsonparse } \iow_newline:
  Nested ~ use ~ of ~ \token_to_str:N \JSONParseValue \c_space_tl not ~ allowed. \iow_newline:
  Use ~ \token_to_str:N \JSONParseExpandableValue \c_space_tl instead.
}

\msg_new:nnn { jsonparse } { file-not-found } {
  \msg_error_text:n { jsonparse } \iow_newline:
  Could ~ not ~ find ~ file ~ #1.
}

\msg_new:nnn { jsonparse } { file-exists } {
  \msg_error_text:n { jsonparse } \iow_newline:
  File ~ #1 ~ already ~ existing.
}

\msg_new:nnn { jsonparse } { escape-in-key } {
  \msg_error_text:n { jsonparse } \iow_newline:
  Invalid ~ escape ~ sequence ~ #1 ~ in ~ key.
}

\msg_new:nnn { jsonparse } { escape-char-not-found } {
  \msg_error_text:n { jsonparse } \iow_newline:
  Escape ~ character ~ #1 ~ not ~ found.
}

\msg_new:nnn { jsonparse } { unknown-key } {
  \msg_warning_text:n { jsonparse } \iow_newline:
  Ignoring ~ unknown ~ key: ~ #1.
}

\msg_new:nnn { jsonparse } { saving-external } {
  \msg_info_text:n { jsonparse } \iow_newline:
  Saving ~ to ~ external ~ file: ~ #1.
}

\msg_new:nnn { jsonparse } { loading-external } {
  \msg_info_text:n { jsonparse } \iow_newline:
  Loading ~ from ~ external ~ file: ~ #1.
}

\msg_new:nnn { jsonparse } { not-array-item } {
  \msg_info_text:n { jsonparse } \iow_newline:
  Key ~ does ~ not ~ represent ~ an ~ array ~ item: ~ #1
}

% ===

\str_new:N \l_jsonparse_externalize_prefix_str
\str_new:N \l_jsonparse_current_prop_str

\tl_new:N \l__jsonparse_externalize_file_name_tl
\tl_new:N \l__jsonparse_externalize_file_data_tl

\str_new:N \l__jsonparse_child_sep_str
\str_new:N \l__jsonparse_array_sep_left_str
\str_new:N \l__jsonparse_array_sep_right_str
\str_new:N \l__jsonparse_true_str
\str_new:N \l__jsonparse_false_str
\str_new:N \l__jsonparse_null_str
\bool_new:N \l__jsonparse_zero_based_bool
\bool_new:N \l__jsonparse_check_num_bool
\bool_new:N \l__jsonparse_rescan_bool
\bool_new:N \l__jsonparse_externalize_bool

\str_new:N \l__jsonparse_backspace_str
\str_new:N \l__jsonparse_formfeed_str
\str_new:N \l__jsonparse_linefeed_str
\str_new:N \l__jsonparse_carriage_return_str
\str_new:N \l__jsonparse_horizontal_tab_str

\clist_new:N \l__jsonparse_unused_keys_clist

\clist_const:Nn \c__jsonparse_escape_tex_chars_clist {
  number_sign ,
  dollar_sign ,
  percent_sign ,
  ampersand ,
  circumflex_accent ,
  low_line ,
  tilde
}

\str_new:N \l__jsonparse_escape_temp_str
\clist_map_inline:Nn \c__jsonparse_escape_tex_chars_clist {
  \bool_new:c { l__jsonparse_escape_ #1 _bool }
}

\keys_define:nn { jsonparse / parse } {
  externalize                 .bool_set:N = \l__jsonparse_externalize_bool ,
  externalize                 .default:n  = { true } ,
  externalize                 .initial:n  = { false } ,
  externalize ~ prefix        .str_set:N  = \l_jsonparse_externalize_prefix_str ,
  externalize ~ prefix        .initial:n  = { } ,
  externalize ~ file ~ name   .tl_set:N   = \l__jsonparse_externalize_file_name_tl ,
  externalize ~ file ~ name   .initial:n  = {
    \l_jsonparse_externalize_prefix_str \c_sys_jobname_str \c_underscore_str \l_jsonparse_current_prop_str
  } ,
  separator                   .code:n     = { \keys_set:nn { jsonparse / parse / separator } {#1} } ,
  separator / child           .str_set:N  = \l__jsonparse_child_sep_str ,
  separator / child           .initial:n  = { . } ,
  separator / array ~ left    .str_set:N  = \l__jsonparse_array_sep_left_str ,
  separator / array ~ left    .initial:n  = { [ } ,
  separator / array ~ right   .str_set:N  = \l__jsonparse_array_sep_right_str ,
  separator / array ~ right   .initial:n  = { ] } ,
  zero-based                  .bool_set:N = \l__jsonparse_zero_based_bool ,
  zero-based                  .default:n  = { true } ,
  zero-based                  .initial:n  = { true } ,
  check ~ num                 .bool_set:N = \l__jsonparse_check_num_bool ,
  check ~ num                 .default:n  = { true } ,
  check ~ num                 .initial:n  = { true } ,
  replace                     .code:n     = { \keys_set:nn { jsonparse / parse / replace } {#1} } ,
  replace / true              .str_set:N  = \l__jsonparse_true_str ,
  replace / true              .initial:n  = { true } ,
  replace / false             .str_set:N  = \l__jsonparse_false_str ,
  replace / false             .initial:n  = { false } ,
  replace / null              .str_set:N  = \l__jsonparse_null_str ,
  replace / null              .initial:n  = { null }
}

\keys_define:nn { jsonparse / typeset } {
  replace                     .code:n     = { \keys_set:nn { jsonparse / typeset / replace } {#1} } ,
  replace / backspace         .str_set:N  = \l__jsonparse_backspace_str ,
  replace / backspace         .initial:n  = { ~ } ,
  replace / formfeed          .str_set:N  = \l__jsonparse_formfeed_str ,
  replace / formfeed          .initial:n  = { ~ } ,
  replace / linefeed          .str_set:N  = \l__jsonparse_linefeed_str ,
  replace / linefeed          .initial:n  = { ~ } ,
  replace / carriage ~ return .str_set:N  = \l__jsonparse_carriage_return_str ,
  replace / carriage ~ return .initial:n  = { ~ } ,
  replace / horizontal ~ tab  .str_set:N  = \l__jsonparse_horizontal_tab_str ,
  replace / horizontal ~ tab  .initial:n  = { ~ } ,
  escape                      .code:n     = {
    \str_case:nnF {#1} {
      { all } {
        \clist_map_inline:Nn \c__jsonparse_escape_tex_chars_clist {
          \bool_set_true:c { l__jsonparse_escape_ ##1 _bool }
        }
      }
      { none } {
        \clist_map_inline:Nn \c__jsonparse_escape_tex_chars_clist {
          \bool_set_false:c { l__jsonparse_escape_ ##1 _bool }
        }
      }
    } {
      \clist_map_inline:nn {#1} {
        \str_set:Nn \l__jsonparse_escape_temp_str {##1}
        \str_replace_all:Nnn \l__jsonparse_escape_temp_str { ~ } { _ }
        \bool_if_exist:cTF { l__jsonparse_escape_ \l__jsonparse_escape_temp_str _bool } {
          \bool_set_true:c { l__jsonparse_escape_ \l__jsonparse_escape_temp_str _bool }
        } {
          \str_case:nnF {##1} {
            { hash } {
              \bool_set_true:c { l__jsonparse_escape_number_sign_bool }
            }
            { dollar } {
              \bool_set_true:c { l__jsonparse_escape_dollar_sign_bool }
            }
            { percent } {
              \bool_set_true:c { l__jsonparse_escape_percent_sign_bool }
            }
            { circumflex } {
              \bool_set_true:c { l__jsonparse_escape_circumflex_accent_bool }
            }
            { underscore } {
              \bool_set_true:c { l__jsonparse_escape_low_line_bool }
            }
          } {
            \msg_error:nno { jsonparse } { escape-char-not-found }
              {##1}
          }
        }
      }
    }
  } ,
  escape                      .groups:n   = { output } ,
  rescan                      .bool_set:N = \l__jsonparse_rescan_bool ,
  rescan                      .default:n  = { true } ,
  rescan                      .initial:n  = { true } ,
  rescan                      .groups:n   = { output }
}

\cs_new:Npn \__jsonparse_warning_unused_keys: {
  \clist_map_inline:Nn \l__jsonparse_unused_keys_clist {
    \msg_warning:nne { jsonparse } { unknown-key } {
      ##1
    }
  }
}

\NewDocumentCommand { \JSONParseSet } { m } {
  \keys_set_known:nnN { jsonparse / global } {#1} \l__jsonparse_unused_keys_clist
  \keys_set_known:noN { jsonparse / parse } { \l__jsonparse_unused_keys_clist } \l__jsonparse_unused_keys_clist
  \keys_set_known:noN { jsonparse / typeset } { \l__jsonparse_unused_keys_clist } \l__jsonparse_unused_keys_clist
  \__jsonparse_warning_unused_keys:
}

% ===

\cs_if_exist:NF \str_casefold:n {
  \cs_new:Npn \str_casefold:n { \str_foldcase:n }
}

\cs_generate_variant:Nn \file_input:n { e }
\cs_generate_variant:Nn \tl_gset:Nn { Ne , ce }
\cs_generate_variant:Nn \tl_gset_rescan:Nnn { Nno , Nne }
\cs_generate_variant:Nn \tl_range:nnn { nne , nen }
\cs_generate_variant:Nn \tl_range:Nnn { Nne , Nen }
\cs_generate_variant:Nn \tl_remove_once:Nn { NV }
\cs_generate_variant:Nn \tl_replace_all:Nnn { Non , Noe }
\cs_generate_variant:Nn \tl_replace_once:Nnn { Non }
\cs_generate_variant:Nn \tl_rescan:nn { no , ne }
\cs_generate_variant:Nn \tl_set:Nn { Ne }
\cs_generate_variant:Nn \tl_trim_spaces:n { e }
\cs_generate_variant:Nn \str_case_e:nn { en }
\cs_generate_variant:Nn \str_casefold:n { o }
\cs_generate_variant:Nn \str_head_ignore_spaces:n { o }
\cs_generate_variant:Nn \str_set:Nn { Ne }
\cs_generate_variant:Nn \clist_const:Nn { Ne }
\cs_generate_variant:Nn \seq_put_right:Nn { Ne }
\cs_generate_variant:Nn \prop_gput:Nnn { Nee }
\cs_generate_variant:Nn \prop_item:Nn { Ne , ce }
\cs_generate_variant:Nn \prop_put:Nnn { Nen , Nee }
\cs_generate_variant:Nn \keys_set_known:nnN { noN }
\cs_generate_variant:Nn \iow_now:Nn { Ne }
\cs_generate_variant:Nn \iow_open:Nn { Ne }
\cs_generate_variant:Nn \msg_error:nnn { nno }
\cs_generate_variant:Nn \msg_error:nnnn { nnoo }
\cs_generate_variant:Nn \msg_info:nnn { nne }
\cs_generate_variant:Nn \msg_log:nnn { nne }
\cs_generate_variant:Nn \msg_warning:nnn { nne }

\prg_generate_conditional_variant:Nnn \file_if_exist:n { e } { T , TF }
\prg_generate_conditional_variant:Nnn \tl_if_head_eq_charcode:nN { oN } { T , TF }
\prg_generate_conditional_variant:Nnn \tl_if_in:nn { nV } { F }
\prg_generate_conditional_variant:Nnn \seq_if_in:Nn { Ne } { F }
\prg_generate_conditional_variant:Nnn \str_if_eq:nn { en , eV } { T , F , TF }
\prg_generate_conditional_variant:Nnn \str_if_eq:nn { Vn } { p }

% ===

\bool_new:N \l__jsonparse_compat_bool

\cs_if_exist:NF \IfExplAtLeastTF {
  \cs_new:Npn \IfExplAtLeastTF #1#2#3 { }
  \bool_set_true:N \l__jsonparse_compat_bool
}

\IfExplAtLeastTF { 2024-12-09 } { } {
  \bool_set_true:N \l__jsonparse_compat_bool
}

% ===

\bool_if:NTF \l__jsonparse_compat_bool {
  \prop_new:N \g_jsonparse_entries_prop
  \prop_new:N \l__jsonparse_temp_prop
} {
  \prop_new_linked:N \g_jsonparse_entries_prop
  \prop_new_linked:N \l__jsonparse_temp_prop
}

\tl_new:N \g__jsonparse_json_tl
\tl_new:N \l__jsonparse_input_tl
\tl_new:N \l__jsonparse_temp_tl
\tl_new:N \l__jsonparse_prefix_tl
\tl_new:N \l__jsonparse_key_tl
\tl_new:N \l__jsonparse_val_tl
\tl_new:N \l__jsonparse_object_array_key_tl
\tl_new:N \l__jsonparse_object_array_val_tl
\tl_new:N \l__jsonparse_remainder_tl

\int_new:N \l__jsonparse_array_index_int
\int_new:N \l__jsonparse_array_count_int
\int_new:N \l__jsonparse_array_count_aux_int
\seq_new:N \l__jsonparse_array_count_last_seq

\str_new:N \l__jsonparse_filter_key_str

\int_new:N \l__jsonparse_array_keys_index_int
\tl_new:N \l__jsonparse_array_keys_index_roman_tl

\str_new:N \l__jsonparse_array_values_key_str
\tl_new:N \l__jsonparse_array_values_insert_tl 

\clist_new:N \l__jsonparse_array_map_keys_clist
\str_new:N \l__jsonparse_array_map_function_str

\bool_new:N \l__jsonparse_prop_map_first_bool
\bool_new:N \l__jsonparse_externalize_load_bool

\iow_new:N \g__jsonparse_externalize_iow

% ===

\cctab_const:Nn \c__jsonparse_json_escape_cctab {
  \cctab_select:N \c_str_cctab
  \char_set_catcode_space:n { 9 }
  \char_set_catcode_escape:n { 92 }
  \bool_lazy_or:nnF
    { \sys_if_engine_xetex_p: } { \sys_if_engine_luatex_p: }
    { \int_step_function:nnN { 128 } { 255 } \char_set_catcode_active:n }
}

% ===

\tl_new:N \l__jsonparse_num_input_tl
\tl_new:N \l__jsonparse_num_remainder_tl
\tl_new:N \l__jsonparse_num_input_last_tl
\bool_new:N \l__jsonparse_num_bool
\bool_new:N \l__jsonparse_num_zero_seen_bool
\bool_new:N \l__jsonparse_num_plus_minus_seen_bool
\bool_new:N \l__jsonparse_num_fraction_seen_bool
\bool_new:N \l__jsonparse_num_exponent_seen_bool

\clist_const:Nn \c__jsonparse_num_digits_clist {
  0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
}

\clist_const:Ne \c__jsonparse_num_plus_minus_fraction_clist {
  \tl_to_str:n { + } ,
  \tl_to_str:n { - } ,
  \tl_to_str:n { . }
}

\clist_const:Ne \c__jsonparse_num_exponent_clist {
  \tl_to_str:n { e } ,
  \tl_to_str:n { E }
}

\clist_concat:NNN \c__jsonparse_num_others_clist 
  \c__jsonparse_num_plus_minus_fraction_clist
  \c__jsonparse_num_exponent_clist

\prg_new_conditional:Npnn \jsonparse_if_num:n #1 { p , T , F , TF } {
  \bool_set_true:N \l__jsonparse_num_bool
  \bool_set_false:N \l__jsonparse_num_zero_seen_bool
  \bool_set_false:N \l__jsonparse_num_plus_minus_seen_bool
  \bool_set_false:N \l__jsonparse_num_fraction_seen_bool
  \bool_set_false:N \l__jsonparse_num_exponent_seen_bool
  \tl_set:Nn \l__jsonparse_num_remainder_tl { #1 }
  \tl_if_empty:NTF \l__jsonparse_num_remainder_tl {
    \bool_set_false:N \l__jsonparse_num_bool
  } {
    \__jsonparse_parse_num:n {#1}
  }
  \clist_if_in:NVT \c__jsonparse_num_others_clist \l__jsonparse_num_input_last_tl {
    \bool_set_false:N \l__jsonparse_num_bool
  }
  \bool_if:NTF \l__jsonparse_num_bool {
    \prg_return_true:
  } {
    \prg_return_false:
  }
}

\cs_new:Npn \__jsonparse_parse_num:n #1 {
  \tl_set:Ne \l__jsonparse_num_input_tl { \tl_trim_spaces:e {#1} }
  \tl_if_empty:NF \l__jsonparse_num_input_tl {
    \cs_if_exist_use:cTF { __jsonparse_parse_num_ \str_head_ignore_spaces:o { \l__jsonparse_num_input_tl } :w } {
      \l__jsonparse_num_input_tl \q_stop
    } {
      \exp_last_unbraced:No
      \__jsonparse_parse_num_other:w \l__jsonparse_num_input_tl \q_stop
    }
  }
}

\cs_new:cpn { __jsonparse_parse_num_ + :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_num_plus_minus:w #1 \q_stop
}

\cs_new:cpn { __jsonparse_parse_num_ - :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_num_plus_minus:w #1 \q_stop
}

\cs_new:cpn { __jsonparse_parse_num_ . :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_num_fraction:w #1 \q_stop
}

\cs_new:cpn { __jsonparse_parse_num_ e :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_num_exponent:w #1 \q_stop
}

\cs_new:cpn { __jsonparse_parse_num_ E :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_num_exponent:w #1 \q_stop
}

\clist_map_inline:Nn \c__jsonparse_num_digits_clist {
  \cs_new:cpn { __jsonparse_parse_num_ #1 :w } ##1 \q_stop {
    \exp_last_unbraced:No
      \__jsonparse_parse_num_digit:w ##1 \q_stop
  }
}

\cs_new:Npn \__jsonparse_parse_num_plus_minus:w #1 \q_stop {
  \bool_if:NTF \l__jsonparse_num_plus_minus_seen_bool {
    \tl_set:Nn \l__jsonparse_num_remainder_tl { }
    \bool_set_false:N \l__jsonparse_num_bool
  } {
    \tl_if_empty:NTF \l__jsonparse_num_input_last_tl {
      \str_if_eq:enT { \tl_head:n {#1} } { + } {
        \tl_set:Nn \l__jsonparse_num_remainder_tl { }
        \bool_set_false:N \l__jsonparse_num_bool
      }
    } {
      \clist_if_in:NVT \c__jsonparse_num_plus_minus_fraction_clist \l__jsonparse_num_input_last_tl {
        \tl_set:Nn \l__jsonparse_num_remainder_tl { }
        \bool_set_false:N \l__jsonparse_num_bool
      }
    }
  }
  \bool_set_true:N \l__jsonparse_num_plus_minus_seen_bool
  \tl_set:Ne \l__jsonparse_num_input_last_tl { \tl_head:n {#1} }
  \tl_set:Ne \l__jsonparse_num_remainder_tl { \tl_tail:n {#1} }
  \__jsonparse_parse_num:n { \l__jsonparse_num_remainder_tl }
}

\cs_new:Npn \__jsonparse_parse_num_fraction:w #1 \q_stop {
  \bool_if:NTF \l__jsonparse_num_fraction_seen_bool {
    \tl_set:Nn \l__jsonparse_num_remainder_tl { }
    \bool_set_false:N \l__jsonparse_num_bool
  } {
    \tl_if_empty:NTF \l__jsonparse_num_input_last_tl {
      \tl_set:Nn \l__jsonparse_num_remainder_tl { }
      \bool_set_false:N \l__jsonparse_num_bool
    } {
      \clist_if_in:NVT \c__jsonparse_num_others_clist \l__jsonparse_num_input_last_tl {
        \tl_set:Nn \l__jsonparse_num_remainder_tl { }
        \bool_set_false:N \l__jsonparse_num_bool
      }
    }
  }
  \bool_set_false:N \l__jsonparse_num_zero_seen_bool
  \bool_set_true:N \l__jsonparse_num_plus_minus_seen_bool
  \bool_set_true:N \l__jsonparse_num_fraction_seen_bool
  \tl_set:Ne \l__jsonparse_num_input_last_tl { \tl_head:n {#1} }
  \tl_set:Ne \l__jsonparse_num_remainder_tl { \tl_tail:n {#1} }
  \__jsonparse_parse_num:n { \l__jsonparse_num_remainder_tl }
}

\cs_new:Npn \__jsonparse_parse_num_exponent:w #1 \q_stop {
  \bool_if:NTF \l__jsonparse_num_exponent_seen_bool {
    \tl_set:Nn \l__jsonparse_num_remainder_tl { }
    \bool_set_false:N \l__jsonparse_num_bool
  } {
    \tl_if_empty:NTF \l__jsonparse_num_input_last_tl {
      \tl_set:Nn \l__jsonparse_num_remainder_tl { }
      \bool_set_false:N \l__jsonparse_num_bool
    } {
      \clist_if_in:NVT \c__jsonparse_num_others_clist \l__jsonparse_num_input_last_tl {
        \tl_set:Nn \l__jsonparse_num_remainder_tl { }
        \bool_set_false:N \l__jsonparse_num_bool
      }
    }
  }
  \bool_set_false:N \l__jsonparse_num_zero_seen_bool
  \bool_set_false:N \l__jsonparse_num_plus_minus_seen_bool
  \bool_set_true:N \l__jsonparse_num_fraction_seen_bool
  \bool_set_true:N \l__jsonparse_num_exponent_seen_bool
  \tl_set:Ne \l__jsonparse_num_input_last_tl { \tl_head:n {#1} }
  \tl_set:Ne \l__jsonparse_num_remainder_tl { \tl_tail:n {#1} }
  \__jsonparse_parse_num:n { \l__jsonparse_num_remainder_tl }
}

\cs_new:Npn \__jsonparse_parse_num_digit:w #1 \q_stop {
  \bool_lazy_any:nTF {
    { \tl_if_empty_p:N \l__jsonparse_num_input_last_tl }
    { \bool_lazy_and_p:nn
      { \str_if_eq_p:Vn \l__jsonparse_num_input_last_tl { - } }
      { \bool_not_p:n { \l__jsonparse_num_exponent_seen_bool } }
    }
  } {
    \str_if_eq:enT { \tl_head:n {#1} } { 0 } {
      \bool_set_true:N \l__jsonparse_num_zero_seen_bool
    }
  } {
    \bool_if:NT \l__jsonparse_num_zero_seen_bool {
      \tl_set:Nn \l__jsonparse_num_remainder_tl { }
      \bool_set_false:N \l__jsonparse_num_bool
    }
  }
  \tl_set:Ne \l__jsonparse_num_input_last_tl { \tl_head:n {#1} }
  \tl_set:Ne \l__jsonparse_num_remainder_tl { \tl_tail:n {#1} }
  \__jsonparse_parse_num:n { \l__jsonparse_num_remainder_tl }
}

\cs_new:Npn \__jsonparse_parse_num_other:w #1 \q_stop {
  \tl_set:Nn \l__jsonparse_num_remainder_tl { }
  \bool_set_false:N \l__jsonparse_num_bool
}

% ===

\cs_new_protected:Npn \jsonparse_parse_to_prop:Nn #1#2 {
  \bool_if:NT \l__jsonparse_debug_mode_bool {
    \msg_log:nne { jsonparse } { debug-info } {
      \iow_newline:
      Parsing ~ JSON ~ ...
    }
  }
  \prop_gclear:N \g_jsonparse_entries_prop
  \jsonparse_parse:n {#2}
  \prop_gset_eq:NN #1 \g_jsonparse_entries_prop
  \bool_if:NT \l__jsonparse_debug_mode_bool {
    \msg_log:nne { jsonparse } { debug-info } {
      JSON ~ parsing ~ done. \iow_newline:
    }
  }
}

\cs_new_protected:Npn \jsonparse_parse:n #1 {
  \tl_set:Ne \l__jsonparse_input_tl { \tl_trim_spaces:e {#1} }
  \cs_if_exist_use:cTF { __jsonparse_parse_ \str_head_ignore_spaces:o { \l__jsonparse_input_tl } :w } {
    \l__jsonparse_input_tl \q_stop
  } {
    % other
    \exp_last_unbraced:No
      \__jsonparse_parse_other:w \l__jsonparse_input_tl \q_stop
  }
}

% ===

\cs_new:cpn { __jsonparse_parse_ \c_left_brace_str :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_object_begin:w #1 \q_stop
}

\cs_new:cpn { __jsonparse_parse_ \c_right_brace_str :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_object_end:w #1 \q_stop
}

\cs_new:cpn { __jsonparse_parse_ [ :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_array_begin:w #1 \q_stop
}

\cs_new:cpn { __jsonparse_parse_ ] :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_array_end:w #1 \q_stop
}

\cs_new:cpn { __jsonparse_parse_ " :w } #1 \q_stop {
  \exp_last_unbraced:No
    \__jsonparse_parse_string_key:w #1 \q_stop
}

\cs_new:Npn \__jsonparse_array_key_set: {
  \str_if_eq:eVT {
    \tl_range:Nen \l__jsonparse_prefix_tl {
      \int_eval:n {
        -1 * \tl_count:N \l__jsonparse_array_sep_left_str
      }
    } { -1 }
  } \l__jsonparse_array_sep_left_str {
    \int_incr:N \l__jsonparse_array_index_int
    \tl_set:Ne \l__jsonparse_key_tl {
      \l__jsonparse_prefix_tl \int_use:N \l__jsonparse_array_index_int \l__jsonparse_array_sep_right_str
    }
  }
}

\exp_last_unbraced:NNo \cs_new:Npn \__jsonparse_parse_object_begin:w \c_left_brace_str #1 \q_stop {
  \__jsonparse_array_key_set:
  \group_begin:
    \tl_set:Nn \l__jsonparse_remainder_tl {#1}
    % object begin
    \bool_if:NT \l__jsonparse_debug_mode_bool {
      \msg_log:nnn { jsonparse } { debug-info } {
        (obj ~ begin)
      }
    }
    \tl_if_empty:NTF \l__jsonparse_key_tl {
      \tl_set_eq:NN \l__jsonparse_object_array_key_tl \l__jsonparse_child_sep_str
    } {
      \tl_set_eq:NN \l__jsonparse_object_array_key_tl \l__jsonparse_key_tl
      \tl_set:Ne \l__jsonparse_prefix_tl { \l__jsonparse_key_tl \l__jsonparse_child_sep_str }
    }
    \tl_set:Nn \l__jsonparse_object_array_val_tl { \c_left_brace_str #1 }
    \__jsonparse_parse_remainder:
}

\exp_last_unbraced:NNo \cs_new:Npn \__jsonparse_parse_object_end:w \c_right_brace_str #1 \q_stop {
    \tl_set:Ne \l__jsonparse_object_array_val_tl {
      \tl_range:Nne \l__jsonparse_object_array_val_tl { 1 } {
        \int_eval:n {
          -1 * \tl_count:n {#1} - 1
        }
      }
    }
    \prop_gput:Nee \g_jsonparse_entries_prop
      { \l__jsonparse_object_array_key_tl } { \l__jsonparse_object_array_val_tl }
    \bool_if:NT \l__jsonparse_debug_mode_bool {
      \msg_log:nne { jsonparse } { debug-info } {
        (key) ~ \str_use:N \l__jsonparse_object_array_key_tl : \iow_newline:
        \iow_char:N \  \iow_char:N \  (obj) ~ \str_use:N \l__jsonparse_object_array_val_tl
      }
    }
  \group_end:
  % object end
  \bool_if:NT \l__jsonparse_debug_mode_bool {
    \msg_log:nnn { jsonparse } { debug-info } {
      (obj ~ end)
    }
  }
  \tl_set:Nn \l__jsonparse_remainder_tl {#1}
  \__jsonparse_parse_remainder:
}

\cs_new:Npn \__jsonparse_parse_array_begin:w [ #1 \q_stop {
  \__jsonparse_array_key_set:
  \group_begin:
    \tl_set:Nn \l__jsonparse_remainder_tl {#1}
    % array begin
    \bool_if:NT \l__jsonparse_debug_mode_bool {
      \msg_log:nnn { jsonparse } { debug-info } {
        (arr ~ begin)
      }
    }
    \int_zero:N \l__jsonparse_array_index_int
    \bool_if:NT \l__jsonparse_zero_based_bool {
      \int_decr:N \l__jsonparse_array_index_int
    }
    \tl_set_eq:NN \l__jsonparse_object_array_key_tl \l__jsonparse_key_tl
    \tl_set:Nn \l__jsonparse_object_array_val_tl { [ #1 }
    \tl_set:Ne \l__jsonparse_prefix_tl { \l__jsonparse_key_tl \l__jsonparse_array_sep_left_str }
    \__jsonparse_parse_remainder:
}

\cs_new:Npn \__jsonparse_parse_array_end:w ] #1 \q_stop {
    \tl_set:Ne \l__jsonparse_object_array_val_tl {
      \tl_range:Nne \l__jsonparse_object_array_val_tl { 1 } {
        \int_eval:n {
          -1 * \tl_count:n {#1} - 1
        }
      }
    }
    \prop_gput:Nee \g_jsonparse_entries_prop
      { \l__jsonparse_object_array_key_tl } { \l__jsonparse_object_array_val_tl }
    \bool_if:NT \l__jsonparse_debug_mode_bool {
      \msg_log:nne { jsonparse } { debug-info } {
        (key) ~ \str_use:N \l__jsonparse_object_array_key_tl : \iow_newline:
        \iow_char:N \  \iow_char:N \  (arr) ~ \str_use:N \l__jsonparse_object_array_val_tl
      }
    }
  \group_end:
  % array end
  \bool_if:NT \l__jsonparse_debug_mode_bool {
    \msg_log:nnn { jsonparse } { debug-info } {
      (arr ~ end)
    }
  }
  \tl_set:Nn \l__jsonparse_remainder_tl {#1}
  \__jsonparse_parse_remainder:
}

\cs_new:Npn \__jsonparse_parse_string_key:w " #1 " #2 \q_stop {
  \__jsonparse_array_key_set:
  \tl_set:Ne \l__jsonparse_remainder_tl { \tl_trim_spaces:n {#2} }
  % key or string?
  \tl_if_head_eq_charcode:oNTF { \l__jsonparse_remainder_tl } : {
    \tl_remove_once:NV \l__jsonparse_remainder_tl \c_colon_str
    \tl_set:Ne \l__jsonparse_key_tl { \l__jsonparse_prefix_tl #1 }
  } {
    \tl_set:Nn \l__jsonparse_val_tl {#1}
    \prop_gput:Nee \g_jsonparse_entries_prop
      { \l__jsonparse_key_tl } { \l__jsonparse_val_tl }
    % string
    \bool_if:NT \l__jsonparse_debug_mode_bool {
      \msg_log:nne { jsonparse } { debug-info } {
        (key) ~ \str_use:N \l__jsonparse_key_tl : \iow_newline:
        \iow_char:N \  \iow_char:N \  (str) ~ \str_use:N \l__jsonparse_val_tl
      }
    }
  }
  \__jsonparse_parse_remainder:
}

\cs_new:Npn \__jsonparse_parse_other:w #1 \q_stop {
  \__jsonparse_array_key_set:
  \tl_set:Nn \l__jsonparse_remainder_tl {#1}
  \tl_set:Nn \l__jsonparse_temp_tl { #1 , }
  \tl_replace_once:Nnn \l__jsonparse_temp_tl { ] } { , }
  \tl_replace_once:Non \l__jsonparse_temp_tl { \c_right_brace_str } { , }
  \exp_last_unbraced:No
    \__jsonparse_parse_other_aux:w \l__jsonparse_temp_tl \q_stop
}

\cs_new:Npn \__jsonparse_parse_other_aux:w #1 , #2 \q_stop {
  \tl_set:Ne \l__jsonparse_temp_tl { \tl_trim_spaces:n {#1} }
  \cs_if_exist_use:cF { __jsonparse_parse_ \str_casefold:o { \l__jsonparse_temp_tl } : } {
    \bool_if:NTF \l__jsonparse_check_num_bool {
      \jsonparse_if_num:nTF {#1} {
        \tl_set:Nn \l__jsonparse_val_tl {#1}
        \prop_gput:Nee \g_jsonparse_entries_prop
          { \l__jsonparse_key_tl } { \l__jsonparse_val_tl }
        % number
        \bool_if:NT \l__jsonparse_debug_mode_bool {
          \msg_log:nne { jsonparse } { debug-info } {
            (key) ~ \str_use:N \l__jsonparse_key_tl : \iow_newline:
            \iow_char:N \  \iow_char:N \  (num) ~ \str_use:N \l__jsonparse_val_tl
          }
        }
      } {
        % not a valid JSON fp
        \msg_error:nnoo { jsonparse } { parsing-error }
          { \l__jsonparse_key_tl } {#1}
      }
    } {
      \tl_set:Nn \l__jsonparse_val_tl {#1}
      \prop_gput:Nee \g_jsonparse_entries_prop
        { \l__jsonparse_key_tl } { \l__jsonparse_val_tl }
      % number
      \bool_if:NT \l__jsonparse_debug_mode_bool {
        \msg_log:nne { jsonparse } { debug-info } {
          (key) ~ \str_use:N \l__jsonparse_key_tl : \iow_newline:
          \iow_char:N \  \iow_char:N \  (num) ~ \str_use:N \l__jsonparse_val_tl
        }
      }
    }
  }
  \tl_set:Ne \l__jsonparse_remainder_tl { \tl_trim_spaces:e { \l__jsonparse_remainder_tl } }
  \tl_set:Ne \l__jsonparse_remainder_tl {
    \tl_range:Nen \l__jsonparse_remainder_tl {
      \int_eval:n {
        \tl_count:n {#1} + 1
      }
    } { -1 }
  }
  \__jsonparse_parse_remainder:
}

\cs_new:Npn \__jsonparse_parse_true: {
  \tl_set_eq:NN \l__jsonparse_val_tl \l__jsonparse_true_str
  \prop_gput:Nee \g_jsonparse_entries_prop
    { \l__jsonparse_key_tl } { \l__jsonparse_val_tl }
  % true
  \bool_if:NT \l__jsonparse_debug_mode_bool {
    \msg_log:nne { jsonparse } { debug-info } {
      (key) ~ \str_use:N \l__jsonparse_key_tl : \iow_newline:
      \iow_char:N \  \iow_char:N \  (tru) ~ \str_use:N \l__jsonparse_val_tl
    }
  }
}

\cs_new:Npn \__jsonparse_parse_false: {
  \tl_set_eq:NN \l__jsonparse_val_tl \l__jsonparse_false_str
  \prop_gput:Nee \g_jsonparse_entries_prop
    { \l__jsonparse_key_tl } { \l__jsonparse_val_tl }
  % false
  \bool_if:NT \l__jsonparse_debug_mode_bool {
    \msg_log:nne { jsonparse } { debug-info } {
      (key) ~ \str_use:N \l__jsonparse_key_tl : \iow_newline:
      \iow_char:N \  \iow_char:N \  (fal) ~ \str_use:N \l__jsonparse_val_tl
    }
  }
}

\cs_new:Npn \__jsonparse_parse_null: {
  \tl_set_eq:NN \l__jsonparse_val_tl \l__jsonparse_null_str
  \prop_gput:Nee \g_jsonparse_entries_prop
    { \l__jsonparse_key_tl } { \l__jsonparse_val_tl }
  % null
  \bool_if:NT \l__jsonparse_debug_mode_bool {
    \msg_log:nne { jsonparse } { debug-info } {
      (key) ~ \str_use:N \l__jsonparse_key_tl : \iow_newline:
      \iow_char:N \  \iow_char:N \  (nul) ~ \str_use:N \l__jsonparse_val_tl
    }
  }
}

\cs_new:Npn \__jsonparse_parse_remainder: {
  \tl_set:Ne \l__jsonparse_remainder_tl { \tl_trim_spaces:e { \l__jsonparse_remainder_tl } }
  \tl_if_head_eq_charcode:oNT { \l__jsonparse_remainder_tl } , {
    \tl_remove_once:Nn \l__jsonparse_remainder_tl { , }
  }
  \tl_if_empty:NF \l__jsonparse_remainder_tl {
    \exp_args:No \jsonparse_parse:n { \l__jsonparse_remainder_tl }
  }
}

\cs_new:Npn \__jsonparse_filter:nn #1#2 {
  \str_case_e:en {
    \tl_range:nne {#1} { 1 } { \int_eval:n { 
      \tl_count:o { \l__jsonparse_filter_key_str } + 1 
    } }
  } {
    { \l__jsonparse_filter_key_str \l__jsonparse_child_sep_str } {
      \prop_put:Nen \l__jsonparse_temp_prop
        { \tl_range:nen {#1} { \int_eval:n { 
          \tl_count:o { \l__jsonparse_filter_key_str } + 2 
        } } { -1 } } {#2}
    }
    { \l__jsonparse_filter_key_str \l__jsonparse_array_sep_left_str } {
      \prop_put:Nen \l__jsonparse_temp_prop
        { \tl_range:nen {#1} { \int_eval:n { 
          \tl_count:o { \l__jsonparse_filter_key_str } + 1 
        } } { -1 } } {#2}
    }
  }
}

\cs_new_protected:Npn \jsonparse_filter:Nn #1#2 {
  \prop_clear:N \l__jsonparse_temp_prop
  \str_set:Nn \l__jsonparse_filter_key_str {#2}
  \prop_map_function:NN #1 \__jsonparse_filter:nn
  \prop_set_eq:NN #1 \l__jsonparse_temp_prop
}

% ===

\NewDocumentCommand { \JSONParsePut } { m m +v } {
  \prop_if_exist:NF #1 {
    \bool_if:NTF \l__jsonparse_compat_bool {
      \prop_new:N #1
    } {
      \prop_new_linked:N #1
    }
  }
  \prop_gput:Nnn #1 {#2} {#3}
}

\cs_new:Npn \__jsonparse_externalize:Nn #1#2 {
  \file_if_exist:eTF {#2} {
    \msg_error:nne { jsonparse } { file-exists }
      {#2}
  } {
    \iow_open:Ne \g__jsonparse_externalize_iow {#2}
    \prop_map_inline:Nn #1 {
      \iow_now:Nn \g__jsonparse_externalize_iow {
        \JSONParsePut {#1} {##1} {##2}
      }
    }
    \iow_close:N \g__jsonparse_externalize_iow
    \msg_info:nne { jsonparse } { saving-external }
      {#2}
  }
}

% ===

\cs_new:Npn \__json_nested_construct_cs:Nnn #1#2#3 {
  \cs_set:Npn #1 #2 ##1 #3 #2 ##2 #3 {
    \prop_item:ce {##1} {##2}
  }
}
\cs_generate_variant:Nn \__json_nested_construct_cs:Nnn { Noo }

\NewDocumentCommand { \JSONParse } { O{} m +v } {
  \group_begin:
    \str_set:Ne \l_jsonparse_current_prop_str { \cs_to_str:N #2 }
    \keys_set_known:nnN { jsonparse / global } {#1} \l__jsonparse_unused_keys_clist
    \keys_set_known:noN { jsonparse / parse } { \l__jsonparse_unused_keys_clist } \l__jsonparse_unused_keys_clist
    \__jsonparse_warning_unused_keys:
    \bool_set_false:N \l__jsonparse_externalize_load_bool
    \bool_if:NT \l__jsonparse_externalize_bool {
      \file_if_exist:eT { \l__jsonparse_externalize_file_name_tl .jsonparse } {
        \bool_set_true:N \l__jsonparse_externalize_load_bool
      }
    }
    \bool_if:NTF \l__jsonparse_externalize_load_bool {
      \msg_info:nne { jsonparse } { loading-external }
        { \l__jsonparse_externalize_file_name_tl .jsonparse }
      \file_input:e { \l__jsonparse_externalize_file_name_tl .jsonparse }
    } {
      \bool_if:NTF \l__jsonparse_compat_bool {
        \prop_new:N #2
      } {
        \prop_new_linked:N #2
      }
      \tl_gclear:N \g__jsonparse_json_tl
      \group_begin:
        \cs_set:Npn \" { \exp_not:N \" }
        \cs_set:Npn \/ { \exp_not:N \/ }
        \cs_set:Npn \\ { \exp_not:N \\ }
        \cs_set:Npn \b { \exp_not:N \b }
        \cs_set:Npn \f { \exp_not:N \f }
        \cs_set:Npn \n { \exp_not:N \n }
        \cs_set:Npn \r { \exp_not:N \r }
        \cs_set:Npn \t { \exp_not:N \t }
        \cs_set:Npn \u { \exp_not:N \u }
        \__json_nested_construct_cs:Noo \x \c_left_brace_str \c_right_brace_str
        \tl_set:Nn \obeyedline { ~ }
        \tl_gset_rescan:Nne \g__jsonparse_json_tl { \cctab_select:N \c__jsonparse_json_escape_cctab } {#3}
        \exp_args:NNe \jsonparse_parse_to_prop:Nn #2 { \g__jsonparse_json_tl }
      \group_end:
      \bool_if:NT \l__jsonparse_externalize_bool {
        \__jsonparse_externalize:Nn #2 { \l__jsonparse_externalize_file_name_tl .jsonparse }
      }
    }
  \group_end:
}

\NewDocumentCommand { \JSONParseFromFile } { O{} m m } {
  \file_if_exist:nF {#3} {
    \msg_error:nnn { jsonparse } { file-not-found }
      {#3}
  }
  \group_begin:
    \str_set:Ne \l_jsonparse_current_prop_str { \cs_to_str:N #2 }
    \keys_set_known:nnN { jsonparse / global } {#1} \l__jsonparse_unused_keys_clist
    \keys_set_known:noN { jsonparse / parse } { \l__jsonparse_unused_keys_clist } \l__jsonparse_unused_keys_clist
    \__jsonparse_warning_unused_keys:
    \bool_set_false:N \l__jsonparse_externalize_load_bool
    \bool_if:NT \l__jsonparse_externalize_bool {
      \file_if_exist:eT { \l__jsonparse_externalize_file_name_tl .jsonparse } {
        \bool_set_true:N \l__jsonparse_externalize_load_bool
      }
    }
    \bool_if:NTF \l__jsonparse_externalize_load_bool {
      \msg_info:nne { jsonparse } { loading-external }
        { \l__jsonparse_externalize_file_name_tl .jsonparse }
      \file_input:e { \l__jsonparse_externalize_file_name_tl .jsonparse }
    } {
      \bool_if:NTF \l__jsonparse_compat_bool {
        \prop_new:N #2
      } { 
        \prop_new_linked:N #2
      }
      \tl_gclear:N \g__jsonparse_json_tl
      \group_begin:
        \cs_set:Npn \" { \exp_not:N \" }
        \cs_set:Npn \/ { \exp_not:N \/ }
        \cs_set:Npn \\ { \exp_not:N \\ }
        \cs_set:Npn \b { \exp_not:N \b }
        \cs_set:Npn \f { \exp_not:N \f }
        \cs_set:Npn \n { \exp_not:N \n }
        \cs_set:Npn \r { \exp_not:N \r }
        \cs_set:Npn \t { \exp_not:N \t }
        \cs_set:Npn \u { \exp_not:N \u }
        \file_get:nnN {#3} { \cctab_select:N \c__jsonparse_json_escape_cctab } \l__jsonparse_externalize_file_data_tl
        \tl_gset_eq:NN \g__jsonparse_json_tl \l__jsonparse_externalize_file_data_tl
        \exp_args:NNe \jsonparse_parse_to_prop:Nn #2 { \g__jsonparse_json_tl }
      \group_end:
      \bool_if:NT \l__jsonparse_externalize_bool {
        \__jsonparse_externalize:Nn #2 { \l__jsonparse_externalize_file_name_tl .jsonparse }
      }
    }
  \group_end:
}

\cs_set_eq:NN \__jsonparse_tex_quote: \"
\cs_set_eq:NN \__jsonparse_tex_backslash: \\

\cs_new:Npn \__jsonparse_unicode_char:NNNN #1#2#3#4 {
  \symbol{ " \str_uppercase:n { #1 #2 #3 #4 } } { }
}

\cs_new:Npn \__jsonparse_rescan_setup:Nn #1#2 {
  \cs_set:Npn \" { " }
  \cs_set:Npn \/ { / }
  \cs_set:Npn \\ { \c_backslash_str }
  \cs_set:Npn \b { \l__jsonparse_backspace_str }
  \cs_set:Npn \f { \l__jsonparse_formfeed_str }
  \cs_set:Npn \n { \l__jsonparse_linefeed_str }
  \cs_set:Npn \r { \l__jsonparse_carriage_return_str }
  \cs_set:Npn \t { \l__jsonparse_horizontal_tab_str }
  \cs_set_eq:NN \u \__jsonparse_unicode_char:NNNN
  \tl_set:Ne #1 {#2}
  \cs_set_eq:NN \" \__jsonparse_tex_quote:
  \cs_set_eq:NN \\ \__jsonparse_tex_backslash:
  \bool_if:NT \l__jsonparse_escape_number_sign_bool {
    \tl_replace_all:Noe #1 { \c_hash_str } { \c_backslash_str \c_hash_str }
  }
  \bool_if:NT \l__jsonparse_escape_dollar_sign_bool {
    \tl_replace_all:Noe #1 { \c_dollar_str } { \c_backslash_str \c_dollar_str }
  }
  \bool_if:NT \l__jsonparse_escape_percent_sign_bool {
    \tl_replace_all:Noe #1 { \c_percent_str } { \c_backslash_str \c_percent_str }
  }
  \bool_if:NT \l__jsonparse_escape_ampersand_bool {
    \tl_replace_all:Noe #1 { \c_ampersand_str } { \c_backslash_str \c_ampersand_str }
  }
  \bool_if:NT \l__jsonparse_escape_circumflex_accent_bool {
    \tl_replace_all:Non #1 { \c_circumflex_str } { \textasciicircum }
  }
  \bool_if:NT \l__jsonparse_escape_low_line_bool {
    \tl_replace_all:Noe #1 { \c_underscore_str } { \c_backslash_str \c_underscore_str }
  }
  \bool_if:NT \l__jsonparse_escape_tilde_bool {
    \tl_replace_all:Non #1 { \c_tilde_str } { \textasciitilde }
  }
}

\cs_new:Npn \__jsonparse_rescan:n #1 {
  \group_begin:
    \__jsonparse_rescan_setup:Nn \l__jsonparse_temp_tl {#1}
    \tl_rescan:no { } { \l__jsonparse_temp_tl }
  \group_end:
}

\cs_new:Npn \__jsonparse_gset_rescan:Nn #1#2 {
  \group_begin:
    \__jsonparse_rescan_setup:Nn \l__jsonparse_temp_tl {#2}
    \tl_gset_rescan:Nno #1 { } { \l__jsonparse_temp_tl }
  \group_end:
}
\cs_generate_variant:Nn \__jsonparse_gset_rescan:Nn { Ne }

\NewExpandableDocumentCommand { \JSONParseExpandableValue } { m m } {
  \prop_item:Ne #1 {#2}
}

\NewDocumentCommand { \JSONParseSetValue } { m m m } {
  \tl_if_exist:NF #1 {
    \tl_new:N #1
  }
  \tl_set:Ne #1 { \prop_item:Ne #2 {#3} }
}

\NewDocumentCommand { \JSONParseSetRescanValue } { m m m } {
  \tl_if_exist:NF #1 {
    \tl_new:N #1
  }
  \__jsonparse_gset_rescan:Ne #1 { \prop_item:Ne #2 {#3} }
}

\NewDocumentCommand { \JSONParseValue } { O{} m m } {
  \group_begin:
    \keys_set_known:nn { jsonparse / typeset } {#1} \l__jsonparse_unused_keys_clist
    \__jsonparse_warning_unused_keys:
    \bool_if:NTF \l__jsonparse_rescan_bool {
      \exp_args:Ne \__jsonparse_rescan:n { \prop_item:Ne #2 {#3} }
    } {
      \prop_item:Ne #2 {#3}
    }
  \group_end:
}

\cs_new:Npn \__jsonparse_keys:n #1 {
  \tl_if_in:nVF {#1} \l__jsonparse_child_sep_str {
    \bool_if:NTF \l__jsonparse_prop_map_first_bool {
      \bool_set_false:N \l__jsonparse_prop_map_first_bool
    } {
      \tl_put_right:Nn \l__jsonparse_temp_tl { , }
    }
    \tl_put_right:Nn \l__jsonparse_temp_tl { " #1 " }
  }
}

\NewDocumentCommand { \JSONParseKeys } { m m } {
  \tl_if_exist:NF #2 {
    \tl_new:N #2
  }
  \bool_set_true:N \l__jsonparse_prop_map_first_bool
  \tl_set:Nn \l__jsonparse_temp_tl { [ }
  \prop_map_function:NN #1 \__jsonparse_keys:n
  \tl_put_right:Nn \l__jsonparse_temp_tl { ] }
  \tl_set_eq:NN #2 \l__jsonparse_temp_tl
}

\cs_new:Npn \__jsonparse_get_array_index:w [ #1 ] #2 \q_stop {
  #1
}

\cs_new:Npn \__jsonparse_array_count:nn #1#2 {
  \str_if_eq:eVF { \tl_head:n {#1} } \l__jsonparse_array_sep_left_str {
    \msg_error:nnn { jsonparse } { not-array-item }
      {#1}
  }
  \seq_if_in:NeF \l__jsonparse_array_count_last_seq {
    \__jsonparse_get_array_index:w #1 \q_stop
  } {
    \int_incr:N \l__jsonparse_array_count_aux_int
    \seq_put_right:Ne \l__jsonparse_array_count_last_seq {
      \__jsonparse_get_array_index:w #1 \q_stop
    }
  }
}

\cs_new_protected:Npn \jsonparse_array_count:NN #1#2 {
  \int_zero:N \l__jsonparse_array_count_aux_int
  \seq_clear:N \l__jsonparse_array_count_last_seq
  \prop_map_function:NN #1 \__jsonparse_array_count:nn
  \int_set_eq:NN #2 \l__jsonparse_array_count_aux_int
}

\NewDocumentCommand { \JSONParseArrayCount } { m m } {
  \group_begin:
    \tl_set_eq:NN \l__jsonparse_temp_tl #1
    \jsonparse_filter:Nn \l__jsonparse_temp_tl {#2}
    \jsonparse_array_count:NN \l__jsonparse_temp_tl \l__jsonparse_array_count_int
    \int_use:N \l__jsonparse_array_count_int
  \group_end:
}

\cs_new:Npn \__jsonparse_array_values:n #1 {
  \bool_if:NTF \l__jsonparse_prop_map_first_bool {
    \bool_set_false:N \l__jsonparse_prop_map_first_bool
  } {
    \l__jsonparse_array_values_insert_tl
  }
  \bool_if:NTF \l__jsonparse_rescan_bool {
    \exp_args:Ne \__jsonparse_rescan:n {
      \prop_item:Ne \l__jsonparse_temp_tl {
        \l__jsonparse_array_sep_left_str
        \bool_if:NTF \l__jsonparse_zero_based_bool {
          \int_eval:n { #1 - 1 }
        } {
          #1
        }
        \l__jsonparse_array_sep_right_str
        \str_if_empty:NF \l__jsonparse_array_values_key_str {
          \l__jsonparse_child_sep_str
          \l__jsonparse_array_values_key_str
        }
      }
    }
  } {
    \prop_item:Ne \l__jsonparse_temp_tl {
      \l__jsonparse_array_sep_left_str
      \bool_if:NTF \l__jsonparse_zero_based_bool {
        \int_eval:n { #1 - 1 }
      } {
        #1
      }
      \l__jsonparse_array_sep_right_str
      \str_if_empty:NF \l__jsonparse_array_values_key_str {
        \l__jsonparse_child_sep_str
        \l__jsonparse_array_values_key_str
      }
    }
  }
}

\NewDocumentCommand { \JSONParseArrayValues } { O{} m m O{} m } {
  \group_begin:
    \keys_set_known:nn { jsonparse / typeset } {#1} \l__jsonparse_unused_keys_clist
    \__jsonparse_warning_unused_keys:
    \tl_set_eq:NN \l__jsonparse_temp_tl #2
    \jsonparse_filter:Nn \l__jsonparse_temp_tl {#3}
    \jsonparse_array_count:NN \l__jsonparse_temp_tl \l__jsonparse_array_count_int
    \str_set:Nn \l__jsonparse_array_values_key_str {#4}
    \tl_set:Nn \l__jsonparse_array_values_insert_tl {#5}
    \bool_set_true:N \l__jsonparse_prop_map_first_bool
    \int_step_function:nN { \l__jsonparse_array_count_int }
      \__jsonparse_array_values:n
  \group_end:
}

\tl_new:N \JSONParseArrayIndex
\tl_new:N \JSONParseArrayKey
\tl_new:N \JSONParseArrayValue

\cs_new:Npn \__jsonparse_array_values_map_keys:n #1 {
  \int_incr:N \l__jsonparse_array_keys_index_int
  \tl_set:Ne \l__jsonparse_array_keys_index_roman_tl {
    \int_to_Roman:n { \l__jsonparse_array_keys_index_int }
  }
  \tl_gset:ce { JSONParseArrayKey \l__jsonparse_array_keys_index_roman_tl } {
    \l__jsonparse_array_sep_left_str
    \JSONParseArrayIndex
    \l__jsonparse_array_sep_right_str
    \l__jsonparse_child_sep_str
    #1
  }
  \bool_if:NTF \l__jsonparse_rescan_bool {
    \tl_gset:cn { JSONParseArrayValue \l__jsonparse_array_keys_index_roman_tl } {
      \exp_args:Ne \__jsonparse_rescan:n {
        \prop_item:Ne \l__jsonparse_temp_tl {
          \l__jsonparse_array_sep_left_str
          \JSONParseArrayIndex
          \l__jsonparse_array_sep_right_str
          \l__jsonparse_child_sep_str
          #1
        }
      }
    }
  } {
    \tl_gset:ce { JSONParseArrayValue \l__jsonparse_array_keys_index_roman_tl } {
      \prop_item:Ne \l__jsonparse_temp_tl {
        \l__jsonparse_array_sep_left_str
        \JSONParseArrayIndex
        \l__jsonparse_array_sep_right_str
        \l__jsonparse_child_sep_str
        #1
      }
    }
  }
}

\cs_new:Npn \__jsonparse_array_values_map:n #1 {
  \bool_if:NTF \l__jsonparse_zero_based_bool {
    \tl_gset:Nn \JSONParseArrayIndex { \int_eval:n { #1 - 1 } }
  } {
    \tl_gset:Nn \JSONParseArrayIndex {#1}
  }
  \clist_if_empty:NTF \l__jsonparse_array_map_keys_clist {
    \tl_gset:Ne \JSONParseArrayKey {
      \l__jsonparse_array_sep_left_str
      \JSONParseArrayIndex
      \l__jsonparse_array_sep_right_str
    }
    \bool_if:NTF \l__jsonparse_rescan_bool {
      \tl_gset:Nn \JSONParseArrayValue {
        \exp_args:Ne \__jsonparse_rescan:n {
          \prop_item:Ne \l__jsonparse_temp_tl {
            \l__jsonparse_array_sep_left_str
            \JSONParseArrayIndex
            \l__jsonparse_array_sep_right_str
          }
        }
      }
    } {
      \tl_gset:Ne \JSONParseArrayValue {
        \prop_item:Ne \l__jsonparse_temp_tl {
          \l__jsonparse_array_sep_left_str
          \JSONParseArrayIndex
          \l__jsonparse_array_sep_right_str
        }
      }
    }
  } {
    \int_zero:N \l__jsonparse_array_keys_index_int
    \clist_map_function:NN \l__jsonparse_array_map_keys_clist 
      \__jsonparse_array_values_map_keys:n
    \tl_gset_eq:NN \JSONParseArrayKey \JSONParseArrayKeyI
    \tl_gset_eq:NN \JSONParseArrayValue \JSONParseArrayValueI
  }
  \use:c { \l__jsonparse_array_map_function_str }
}

\cs_set:Npn \__jsonparse_array_map_generate_cs:n #1 {
  \int_incr:N \l__jsonparse_array_keys_index_int
  \tl_set:Ne \l__jsonparse_array_keys_index_roman_tl {
    \int_to_Roman:n { \l__jsonparse_array_keys_index_int }
  }
  \tl_if_exist:cF { JSONParseArrayKey \l__jsonparse_array_keys_index_roman_tl } {
    \tl_new:c { JSONParseArrayKey \l__jsonparse_array_keys_index_roman_tl }
  }
  \tl_if_exist:cF { JSONParseArrayValue \l__jsonparse_array_keys_index_roman_tl } {
    \tl_new:c { JSONParseArrayValue \l__jsonparse_array_keys_index_roman_tl }
  }
}

\NewDocumentCommand { \JSONParseArrayValuesMap } { O{} m m O{} m O{} O{} } {
  \group_begin:
    \keys_set_known:nn { jsonparse / typeset } {#1} \l__jsonparse_unused_keys_clist
    \__jsonparse_warning_unused_keys:
    \tl_set_eq:NN \l__jsonparse_temp_tl #2
    \jsonparse_filter:Nn \l__jsonparse_temp_tl {#3}
    \jsonparse_array_count:NN \l__jsonparse_temp_tl \l__jsonparse_array_count_int
    \clist_set:Nn \l__jsonparse_array_map_keys_clist {#4}
    \str_set:Nn \l__jsonparse_array_map_function_str {#5}
    \clist_if_empty:NF \l__jsonparse_array_map_keys_clist {
      \int_zero:N \l__jsonparse_array_keys_index_int
      \clist_map_function:NN \l__jsonparse_array_map_keys_clist
        \__jsonparse_array_map_generate_cs:n
    }
    #6
    \int_step_function:nN { \l__jsonparse_array_count_int } 
      \__jsonparse_array_values_map:n 
    #7
  \group_end:
}

% ===
