% 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-08-27} {0.9.0} 
  {JSON Parse}

\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 ~ external ~ file: ~ #1.
}

\msg_new:nnn { jsonparse } { loading-external } {
  \msg_info_text:n { jsonparse } \iow_newline:
  Loading ~ from ~ external ~ file: ~ #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

\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_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_new:N \l__jsonparse_escape_tex_chars_clist
\clist_set:Nn \l__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 \l__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 } ,
  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 \l__jsonparse_escape_tex_chars_clist {
          \bool_set_true:c { l__jsonparse_escape_ ##1 _bool }
        }
      } 
      { none } {
        \clist_map_inline:Nn \l__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_rescan:Nnn { 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 \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_eq:nn { en } { T }
\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 \str_if_eq:nn { en , eV } { T , TF }

\prop_new:N \g_jsonparse_entries_prop
\prop_new: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_last_int

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

\ior_new:N \l__jsonparse_json_ior
\iow_new:N \g__jsonparse_externalize_iow

% ===

\cctab_const:Nn \c__jsonparse_json_escape_cctab {
  \cctab_select:N \c_str_cctab
  \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 }
}

% ===

\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 } : } {
    \fp_if_nan:nTF {#1} { 
      % nan
      \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_protected:Npn \jsonparse_filter:Nn #1#2 {
  \prop_clear:N \l__jsonparse_temp_prop
  \prop_map_inline:Nn #1 {
    \str_case_e:en { 
      \tl_range:nne {##1} { 1 } { \int_eval:n { \tl_count:n {#2} + 1 } } 
    } {
      { #2 \l__jsonparse_child_sep_str } {
        \prop_put:Nen \l__jsonparse_temp_prop 
          { \tl_range:nen {##1} { \int_eval:n { \tl_count:n {#2} + 2 } } { -1 } } {##2} 
      }
      { #2 \l__jsonparse_array_sep_left_str } {
        \prop_put:Nen \l__jsonparse_temp_prop 
          { \tl_range:nen {##1} { \int_eval:n { \tl_count:n {#2} + 1 } } { -1 } } {##2} 
      }
    }
  }
  \prop_set_eq:NN #1 \l__jsonparse_temp_prop
}

% ===

\NewDocumentCommand { \JSONParsePut } { m m +v } {
  \prop_if_exist:NF #1 {
    \prop_new: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 }
    } {
      \prop_new: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 }
    } {
      \prop_new: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 }
        \cs_set:Npn \x [ ##1 ] [ ##2 ] { \prop_item:ce {##1} {##2} }
        \file_get:nnN {#3} { \cctab_select:N \c__jsonparse_json_escape_cctab } \g__jsonparse_json_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:
}

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

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

\cs_new:Npn \__jsonparse_rescan:n #1 {
  \group_begin:
    \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:Npn \u { \char" }
    \tl_set:Ne \l__jsonparse_temp_tl {#1}
    \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 \l__jsonparse_temp_tl { \c_hash_str } { \c_backslash_str \c_hash_str }
    }
    \bool_if:NT \l__jsonparse_escape_dollar_sign_bool {
      \tl_replace_all:Noe \l__jsonparse_temp_tl { \c_dollar_str } { \c_backslash_str \c_dollar_str }
    }
    \bool_if:NT \l__jsonparse_escape_percent_sign_bool {
      \tl_replace_all:Noe \l__jsonparse_temp_tl { \c_percent_str } { \c_backslash_str \c_percent_str }
    }
    \bool_if:NT \l__jsonparse_escape_ampersand_bool {
      \tl_replace_all:Noe \l__jsonparse_temp_tl { \c_ampersand_str } { \c_backslash_str \c_ampersand_str }
    }
    \bool_if:NT \l__jsonparse_escape_circumflex_accent_bool {
      \tl_replace_all:Non \l__jsonparse_temp_tl { \c_circumflex_str } { \textasciicircum }
    }
    \bool_if:NT \l__jsonparse_escape_low_line_bool {
      \tl_replace_all:Noe \l__jsonparse_temp_tl { \c_underscore_str } { \c_backslash_str \c_underscore_str }
    }
    \bool_if:NT \l__jsonparse_escape_tilde_bool {
      \tl_replace_all:Non \l__jsonparse_temp_tl { \c_tilde_str } { \textasciitilde }
    }
    \tl_rescan:no { } { \l__jsonparse_temp_tl }
  \group_end:
}

\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:
}

\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_inline:Nn #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 " }
      }
    }
  \tl_put_right:Nn \l__jsonparse_temp_tl { ] } 
  \tl_set_eq:NN #2 \l__jsonparse_temp_tl
}

\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:Nn \l__jsonparse_temp_tl {#2}
    \jsonparse_filter:Nn \l__jsonparse_temp_tl {#3}
    \bool_set_true:N \l__jsonparse_prop_map_first_bool
    \prop_map_inline:Nn \l__jsonparse_temp_tl {
      \str_if_eq:enT { 
        \tl_range:nen {##1} { \int_eval:n { -1 * \tl_count:n {#4} } } { -1 } 
      } {#4} {
        \bool_if:NTF \l__jsonparse_prop_map_first_bool { 
          \bool_set_false:N \l__jsonparse_prop_map_first_bool
        } {
          #5 
        }
        \bool_if:NTF \l__jsonparse_rescan_bool {
          \__jsonparse_rescan:n {##2}
        } {
          ##2 
        }
      } 
    }
  \group_end:
}

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

\NewDocumentCommand { \JSONParseArrayCount } { m m } {
  \group_begin:
    \jsonparse_filter:Nn #1 {#2}
    \int_zero:N \l__jsonparse_array_count_int
    \int_set:Nn \l__jsonparse_array_count_last_int { -1 }
    \prop_map_inline:Nn #1 {
      \int_compare:nNnF { 
        \__jsonparse_get_array_index:w ##1 \q_stop 
      } = { \l__jsonparse_array_count_last_int } {
        \int_incr:N \l__jsonparse_array_count_int
      }
      \int_set:Nn \l__jsonparse_array_count_last_int {
        \__jsonparse_get_array_index:w ##1 \q_stop
      }
    } 
    \int_use:N \l__jsonparse_array_count_int
  \group_end:
}

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

\NewDocumentCommand { \JSONParseArrayValuesMap } { O{} m m O{} m } {
  \group_begin:
    \keys_set_known:nn { jsonparse / typeset } {#1} \l__jsonparse_unused_keys_clist
    \__jsonparse_warning_unused_keys:
    \jsonparse_filter:Nn #2 {#3}
    \prop_map_inline:Nn #2 {
      \str_if_eq:enT { 
        \tl_range:nen {##1} { \int_eval:n { -1 * \tl_count:n {#4} } } { -1 } 
      } {#4} {
        \int_incr:N \l__jsonparse_array_index_int
        \tl_set:Ne \JSONParseArrayIndex { \__jsonparse_get_array_index:w ##1 \q_stop }
        \tl_set:Nn \JSONParseArrayKey {##1}
        \bool_if:NTF \l__jsonparse_rescan_bool {
          \tl_set:Nn \JSONParseArrayValue { \exp_args:Ne \__jsonparse_rescan:n { \prop_item:Nn #2 {##1} } }
        } {
          \tl_set:Nn \JSONParseArrayValue { \prop_item:Nn #2 {##1} }
        }
        \use:c {#5} 
      }
    } 
  \group_end:
}

% EOF