% File: jsonparse.sty 
% Copyright 2024 Jasper Habicht (mail@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 `author-maintained'.
% 
\ProvidesExplPackage {jsonparse} {2024-04-09} {0.5.5} 
  {JSON Parse}

\bool_new:N \l_jsonparse_debug_mode_bool
\keys_define:nn { jsonparse } { 
    debug .bool_set:N = \l_jsonparse_debug_mode_bool ,
    debug .default:n  = { true } ,
}
\ProcessKeyOptions [ jsonparse ]

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

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

% ===
	
\str_new:N \l_jsonparse_child_sep_str
\str_set:Nn \l_jsonparse_child_sep_str { . }
\str_new:N \l_jsonparse_array_sep_left_str
\str_set:Nn \l_jsonparse_array_sep_left_str { [ }
\str_new:N \l_jsonparse_array_sep_right_str
\str_set:Nn \l_jsonparse_array_sep_right_str { ] }
\str_new:N \l_jsonparse_true_str
\str_set:Nn \l_jsonparse_true_str { true }
\str_new:N \l_jsonparse_false_str
\str_set:Nn \l_jsonparse_false_str { false }
\str_new:N \l_jsonparse_null_str
\str_set:Nn \l_jsonparse_null_str { null }

\bool_new:N \l_jsonparse_array_index_zero_based_bool
\bool_set_true:N \l_jsonparse_array_index_zero_based_bool

\NewDocumentCommand { \JSONParseSetChildSeparator } { m } {
  \str_set:Nn \l_jsonparse_child_sep_str { #1 }
}

\NewDocumentCommand { \JSONParseSetArraySeparator } { m m } {
  \str_set:Nn \l_jsonparse_array_sep_left_str { #1 }
  \str_set:Nn \l_jsonparse_array_sep_right_str { #2 }
}

\NewDocumentCommand { \JSONParseSetTrueString } { m } {
  \str_set:Nn \l_jsonparse_true_str { #1 }
}

\NewDocumentCommand { \JSONParseSetFalseString } { m } {
  \str_set:Nn \l_jsonparse_false_str { #1 }
}

\NewDocumentCommand { \JSONParseSetNullString } { m } {
  \str_set:Nn \l_jsonparse_null_str { #1 }
}

\NewDocumentCommand { \JSONParseSetArrayIndexZeroBased } { } {
  \bool_set_true:N \l_jsonparse_array_index_zero_based_bool
}

\NewDocumentCommand { \JSONParseSetArrayIndexOneBased } { } {
  \bool_set_false:N \l_jsonparse_array_index_zero_based_bool
}

% ===

\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 { Ne }
\cs_generate_variant:Nn \tl_set:Nn { Ne }
\cs_generate_variant:Nn \str_count:n { e }
\cs_generate_variant:Nn \str_head_ignore_spaces:n { e }
\cs_generate_variant:Nn \str_set:Nn { Ne }
\cs_generate_variant:Nn \prop_gput:Nnn { Nee }
\cs_generate_variant:Nn \seq_use:Nn { Ne }
\cs_generate_variant:Nn \msg_log:nnn { nno }

\prg_generate_conditional_variant:Nnn \tl_if_head_is_group:n { e } { TF }
\prg_generate_conditional_variant:Nnn \str_case:nn { en } { F }
\prg_generate_conditional_variant:Nnn \str_case_e:nn { en } { F }
\prg_generate_conditional_variant:Nnn \str_if_eq:nn { ee } { T }
\prg_generate_conditional_variant:Nnn \str_if_eq:nn { en } { T , TF }

\prop_new:N \g_jsonparse_entries_prop
\prop_new:N \l__jsonparse_temp_prop

\tl_new:N \l__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_array_prefix_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__prop_map_first_bool

\ior_new:N \l__jsonparse_json_ior
	
% ===

\cs_new: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
  \group_begin:
    \tl_set_eq:NN \par \c_space_tl
    \jsonparse_parse:n {#2}
  \group_end:
  \prop_set_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:Npn \jsonparse_parse:n #1 {
  \tl_set:Ne \l__jsonparse_input_tl { \tl_trim_spaces:n {#1} }
  \tl_if_head_is_group:eTF { \l__jsonparse_input_tl } {
    \exp_last_unbraced:Ne 
      \__jsonparse_parse_object:w \l__jsonparse_input_tl \q_stop 
  } {
    \str_case_e:enF { \str_head_ignore_spaces:e { \l__jsonparse_input_tl } } {
      { [ } { 
        \exp_last_unbraced:Ne 
          \__jsonparse_parse_array_begin:w \l__jsonparse_input_tl \q_stop 
      }
      { ] } { 
        \exp_last_unbraced:Ne 
          \__jsonparse_parse_array_end:w \l__jsonparse_input_tl \q_stop 
      }
      { " } { 
        \exp_last_unbraced:Ne 
          \__jsonparse_parse_string_key:w \l__jsonparse_input_tl \q_stop 
      }
    } {
      % other
      \exp_last_unbraced:Ne 
        \__jsonparse_parse_other:w \l__jsonparse_input_tl \q_stop 
    }
  }
}

% ===

\cs_new:Npn \__jsonparse_parse_object:w #1#2 \q_stop {
  \__jsonparse_array_key_set:
  \tl_set:Ne \l__jsonparse_remainder_tl {#2}
  \group_begin:
    % object begin
    \bool_if:NT \l_jsonparse_debug_mode_bool {
      \msg_log:nne { jsonparse } { debug-info } {
        (obj ~ begin) 
      } 
    }
    \tl_if_empty:NTF \l__jsonparse_key_tl {
      \tl_set:Ne \l__jsonparse_object_array_key_tl { \l_jsonparse_child_sep_str }
    } {
      \tl_set:Ne \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:Ne \l__jsonparse_val_tl {#1}
    }
    \tl_set:Ne \l__jsonparse_object_array_val_tl { { #1 } }
    \exp_args:Ne \jsonparse_parse:n {#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 
      } 
    } 
    % object end
    \bool_if:NT \l_jsonparse_debug_mode_bool {
      \msg_log:nne { jsonparse } { debug-info } {
        (obj ~ end) 
      } 
    }
  \group_end:
  \__jsonparse_parse_remainder:
}

\cs_new:Npn \__jsonparse_parse_array_begin:w [ #1 \q_stop {
  \__jsonparse_array_key_set:
  \group_begin:
    \tl_set:Ne \l__jsonparse_remainder_tl {#1}
    % array begin
    \bool_if:NT \l_jsonparse_debug_mode_bool {
      \msg_log:nne { jsonparse } { debug-info } {
        (arr ~ begin) 
      }   
    }
    \int_zero:N \l__jsonparse_array_index_int
    \bool_if:NT \l_jsonparse_array_index_zero_based_bool {
      \int_decr:N \l__jsonparse_array_index_int
    }
    \tl_set:Ne \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:e {#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:
  \tl_set:Ne \l__jsonparse_remainder_tl {#1}
  % array end
  \bool_if:NT \l_jsonparse_debug_mode_bool {
    \msg_log:nne { jsonparse } { debug-info } {
      (arr ~ end) 
    } 
  }
  \__jsonparse_parse_remainder:
}

\cs_new:Npn \__jsonparse_array_key_set: {
  \str_if_eq:eeT { \tl_item:Nn \l__jsonparse_prefix_tl { -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 
    }
  }
}

\cs_new:Npn \__jsonparse_parse_string_key:w " #1 " #2 \q_stop {
  \__jsonparse_array_key_set:
  \tl_set:Ne \l__jsonparse_remainder_tl {#2}
  % key or string?
  \str_if_eq:enTF { \tl_head:n { #2 } } { : } {
    \tl_remove_once:Ne \l__jsonparse_remainder_tl { \c_colon_str }
    \tl_set:Ne \l__jsonparse_key_tl { \l__jsonparse_prefix_tl #1 }
  } { 
    \tl_set:Ne \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:Ne \l__jsonparse_remainder_tl {#1}
  \tl_set:Ne \l__jsonparse_temp_tl { #1 , }
  \tl_replace_once:Nnn \l__jsonparse_temp_tl { ] } { , }
  \exp_last_unbraced:Ne
    \__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 {#1}
  \str_case:enF { 
    \str_lowercase:f { \l__jsonparse_temp_tl } 
  } {
    { true } {
      \tl_set:Ne \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
        } 
      }  
    }
    { false } {
      \tl_set:Ne \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
        } 
      }
    }
    { null } {
      \tl_set:Ne \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
        } 
      }
    }
  } {
    \fp_compare:nNnTF {#1} ? { 0 } { 
      % nan
      \msg_error:nnee { jsonparse } { parsing-error }
        { \l__jsonparse_key_tl } {#1}
    } { 
      \tl_set:Ne \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:V \l__jsonparse_remainder_tl }
  \tl_set:Ne \l__jsonparse_remainder_tl { 
    \tl_range:Nen \l__jsonparse_remainder_tl { 
      \int_eval:n {
        \tl_count:e { #1 } + 1 
      }
    } { -1 }
  }
  \__jsonparse_parse_remainder:
}

\cs_new:Npn \__jsonparse_parse_remainder: {
  \tl_set:Ne \l__jsonparse_remainder_tl { \tl_trim_spaces:V \l__jsonparse_remainder_tl }
  \str_if_eq:enT { \tl_head:N \l__jsonparse_remainder_tl } { , } {
    \tl_remove_once:Nn \l__jsonparse_remainder_tl { , }
  }
  \tl_if_empty:NF \l__jsonparse_remainder_tl {
    \exp_args:Ne \jsonparse_parse:n { \l__jsonparse_remainder_tl }
  }
}	

\cs_new:Npn \jsonparse_filter:Nn #1#2 {
  \prop_map_inline:Nn #1 {
    \str_case_e:en { 
      \tl_range:nne {##1} { 1 } { \int_eval:n { \tl_count:e {#2} + 1 } } 
    } {
      { #2 \l_jsonparse_child_sep_str } {
        \prop_put:Nee \l__jsonparse_temp_prop 
          { \tl_range:nen {##1} { \int_eval:n { \tl_count:e {#2} + 2 } } { -1 } } {##2} 
      }
      { #2 \l_jsonparse_array_sep_left_str } {
        \prop_put:Nee \l__jsonparse_temp_prop 
          { \tl_range:nen {##1} { \int_eval:n { \tl_count:e {#2} + 1 } } { -1 } } {##2} 
      }
    }
  }
  \prop_set_eq:NN #1 \l__jsonparse_temp_prop
}

% ===

\NewExpandableDocumentCommand { \JSONParse } { m +m } {
  \prop_new:N #1
  \jsonparse_parse_to_prop:Nn #1 {#2} 
}

\NewExpandableDocumentCommand { \JSONParseFromFile } { m m } {
  \ior_open:Nn \l__jsonparse_json_ior {#2}
  
  \tl_clear:N \l__jsonparse_json_tl
  \ior_map_inline:Nn \l__jsonparse_json_ior {
    \tl_put_right:Nn \l__jsonparse_json_tl {##1}
  }
  
  \ior_close:N \l__jsonparse_json_ior

  \prop_new:N #1
  \jsonparse_parse_to_prop:Nn #1 { \l__jsonparse_json_tl } 
}

\NewExpandableDocumentCommand { \JSONParseGetValue } { m m } {
  \prop_item:Nn #1 {#2} 
}

\NewExpandableDocumentCommand { \JSONParseGetArrayValues } { m m O{} m } {
  \group_begin:
    \jsonparse_filter:Nn #1 {#2}
    \bool_set_true:N \l__prop_map_first_bool
    \prop_map_inline:Nn #1 {
      \str_if_eq:enT { 
        \tl_range:nen {##1} { \int_eval:n { -1 * \tl_count:n {#3} } } { -1 } 
      } {#3} {
        \bool_if:NTF \l__prop_map_first_bool { 
          \bool_set_false:N \l__prop_map_first_bool
        } {        
          #4 
        }
        ##2
      } 
    }
  \group_end:
}

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

\NewExpandableDocumentCommand { \JSONParseGetArrayCount } { 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_aux: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_aux:w ##1 \q_stop
      }
    } 
    \int_use:N \l__jsonparse_array_count_int
  \group_end:
}

\NewExpandableDocumentCommand { \JSONParseUseArrayValues } { m m O{} m } {
  \group_begin:
    \jsonparse_filter:Nn #1 {#2}
    \prop_map_inline:Nn #1 {
      \str_if_eq:enT { 
        \tl_range:nen {##1} { \int_eval:n { -1 * \tl_count:n {#3} } } { -1 } 
      } {#3} {
        \int_incr:N \l__jsonparse_array_index_int
        \tl_set:Ne \JSONParseArrayIndex { \__jsonparse_get_array_index_aux:w ##1 \q_stop }
        \tl_set:Ne \JSONParseArrayKey { ##1 }
        \tl_set:Ne \JSONParseArrayValue { \prop_item:Nn #1 {##1} }
        \use:c {#4} 
      }
    } 
  \group_end:
}

% EOF
