%%
%% inspirehep.sty -- live INSPIRE-HEP data in a LaTeX document.
%%
%% Copyright (C) 2026 Lawrence Lee
%%
%% This work may be distributed and/or modified under the conditions of the
%% LaTeX Project Public License, either version 1.3c of this license or (at
%% your option) any later version.  The latest version of this license is in
%%     https://www.latex-project.org/lppl.txt
%% This work has the LPPL maintenance status `maintained'.
%% The Current Maintainer of this work is Lawrence Lee.
%%
%% https://github.com/lawrenceleejr/inspirehep-latex
%%
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{inspirehep}[2026/08/29 v0.2
  Live INSPIRE-HEP citation counts, references, and plots]

\RequirePackage{xparse}
\RequirePackage{l3keys2e}

\ExplSyntaxOn

%% ===========================================================================
%% Settings
%% ===========================================================================
%% Every setting is both a package option and a per-call option, so a document
%% sets house style once and any single entry can depart from it:
%%
%%   \usepackage[cites=false]{inspirehep}
%%   \inspirepub[cites]{2642414}
%%
%% There is deliberately no author= option: a document may well discuss several
%% people, so every author-level command takes the id it is asking about.

\bool_new:N \g_inspirehep_pgfplots_bool
\bool_new:N \l_inspirehep_cites_bool
\bool_new:N \l_inspirehep_ref_bool
\bool_new:N \l_inspirehep_year_bool
\bool_new:N \l_inspirehep_errata_bool
\bool_new:N \l_inspirehep_collab_bool
\bool_new:N \l_inspirehep_link_bool
\tl_new:N   \l_inspirehep_title_tl
\int_new:N  \l_inspirehep_round_int
\tl_new:N   \g_inspirehep_data_tl
\tl_new:N   \g_inspirehep_bib_tl
\tl_new:N   \g_inspirehep_style_tl
\tl_new:N   \g_inspirehep_fetch_tl
\tl_new:N   \l_inspirehep_sep_tl
\tl_new:N   \g_inspirehep_script_tl
\int_new:N  \g_inspirehep_maxage_int
\int_new:N  \l_inspirehep_mincites_int

\keys_define:nn { inspirehep }
  {
    % what a record entry shows
    cites   .bool_set:N   = \l_inspirehep_cites_bool,
    cites   .initial:n    = { true },
    ref     .bool_set:N   = \l_inspirehep_ref_bool,
    ref     .initial:n    = { false },
    year    .bool_set:N   = \l_inspirehep_year_bool,
    year    .initial:n    = { false },
    % INSPIRE's reference carries any erratum; most CVs do not want it.
    errata  .bool_set:N   = \l_inspirehep_errata_bool,
    errata  .initial:n    = { false },
    % Credit a collaboration paper to the collaboration rather than to the
    % first author and "et al." -- the usual convention in a CV.
    collab  .bool_set:N   = \l_inspirehep_collab_bool,
    collab  .initial:n    = { false },
    link    .bool_set:N   = \l_inspirehep_link_bool,
    link    .initial:n    = { true },
    title   .tl_set:N     = \l_inspirehep_title_tl,
    title   .initial:n    = { },
    round   .int_set:N    = \l_inspirehep_round_int,
    round   .initial:n    = { 1 },

    % document-wide plumbing
    data    .tl_gset:N    = \g_inspirehep_data_tl,
    data    .initial:n    = { inspirehep-data },
    bib     .tl_gset:N    = \g_inspirehep_bib_tl,
    bib     .initial:n    = { inspirehep-refs },
    style   .tl_gset:N    = \g_inspirehep_style_tl,
    style   .initial:n    = { latex-eu },
    fetch   .tl_gset:N    = \g_inspirehep_fetch_tl,
    fetch   .initial:n    = { auto },
    maxage  .int_gset:N   = \g_inspirehep_maxage_int,
    maxage  .initial:n    = { 120 },
    % Local, so [mincites=...] on one entry cannot leak past its group.
    mincites .int_set:N   = \l_inspirehep_mincites_int,
    mincites .initial:n   = { 1 },
    % Where the auto-fetch finds the helper: adjust when the package lives in
    % a subdirectory or a git submodule.
    script  .tl_gset:N    = \g_inspirehep_script_tl,
    script  .initial:n    = { inspirehep-fetch.py },
    % pgfplots can only be loaded in the preamble, and a CV that never plots
    % anything should not pay for tikz, so plotting is opt-in.
    plots   .bool_gset:N  = \g_inspirehep_pgfplots_bool,
    plots   .initial:n    = { false },

    % thousands separator: comma by default, or any of these, or your own text
    % The separator is set locally, so a change inside a group -- or on one
    % call -- ends with that group.  Set at the top level it holds document-wide.
    sep     .choices:nn   =
      { comma, period, space, thin, underscore, none }
      {
        \tl_set:Nx \l_inspirehep_sep_tl
          {
            \str_case:Vn \l_keys_choice_tl
              {
                { comma      } { , }
                { period     } { . }
                { space      } { \exp_not:N \nobreakspace }
                { thin       } { \exp_not:N \, }
                { underscore } { \exp_not:N \_ }
                { none       } { }
              }
          }
      },
    sep     .value_required:n = true,
    sepstring .tl_set:N   = \l_inspirehep_sep_tl,
  }
\tl_set:Nn \l_inspirehep_sep_tl { , }

\ProcessKeysOptions { inspirehep }

% Loading pgfplots has to happen now, while we are still in the preamble.
\bool_if:NT \g_inspirehep_pgfplots_bool
  { \RequirePackage { pgfplots } \pgfplotsset { compat = 1.18 } }

% \inspiresetup{<options>} -- change the defaults anywhere in the document.
\NewDocumentCommand \inspiresetup { m } { \keys_set:nn { inspirehep } {#1} }

%% ===========================================================================
%% Stored data
%% ===========================================================================
%% The generated file is a flat list of declarations, written either by this
%% package through shell escape or by inspirehep-fetch.py.  Both write the same
%% file, so it does not matter which produced it.

\prop_new:N \g_inspirehep_cites_prop
\prop_new:N \g_inspirehep_title_prop
\prop_new:N \g_inspirehep_key_prop
\prop_new:N \g_inspirehep_recid_prop
\prop_new:N \g_inspirehep_ref_prop          % a whole reference, set by hand
\prop_new:N \g_inspirehep_refauthors_prop
\prop_new:N \g_inspirehep_refpub_prop
\prop_new:N \g_inspirehep_referrata_prop
\prop_new:N \g_inspirehep_refcollab_prop
\prop_new:N \g_inspirehep_years_prop
\prop_new:N \g_inspirehep_stat_prop
\prop_new:N \g_inspirehep_ayears_prop
\tl_new:N   \g_inspirehep_fetched_tl
\int_new:N  \g_inspirehep_fetchedday_int

% Everything the document asks about, so the fetcher knows what to request.
\seq_new:N \g_inspirehep_records_seq
\seq_new:N \g_inspirehep_plots_seq
\seq_new:N \g_inspirehep_bibs_seq
\seq_new:N \g_inspirehep_authors_seq
\bool_new:N \g_inspirehep_missing_bool

\cs_new_protected:Npn \inspirehep_want:Nn #1#2
  { \seq_if_in:NnF #1 {#2} { \seq_gput_right:Nn #1 {#2} } }

\NewDocumentCommand \inspiresetcites      { m m }   { \prop_gput:Nnn \g_inspirehep_cites_prop {#1} {#2} }
\NewDocumentCommand \inspiresettitle      { m m }   { \prop_gput:Nnn \g_inspirehep_title_prop {#1} {#2} }
\NewDocumentCommand \inspiresetkey        { m m }   { \prop_gput:Nnn \g_inspirehep_key_prop   {#1} {#2} }
\NewDocumentCommand \inspiresetrecid      { m m }   { \prop_gput:Nnn \g_inspirehep_recid_prop {#1} {#2} }
\NewDocumentCommand \inspiresetref         { m m }   { \prop_gput:Nnn \g_inspirehep_ref_prop         {#1} {#2} }
\NewDocumentCommand \inspiresetrefauthors  { m m }   { \prop_gput:Nnn \g_inspirehep_refauthors_prop  {#1} {#2} }
\NewDocumentCommand \inspiresetrefpub      { m m }   { \prop_gput:Nnn \g_inspirehep_refpub_prop      {#1} {#2} }
\NewDocumentCommand \inspiresetreferrata   { m m }   { \prop_gput:Nnn \g_inspirehep_referrata_prop   {#1} {#2} }
\NewDocumentCommand \inspiresetrefcollab   { m m }   { \prop_gput:Nnn \g_inspirehep_refcollab_prop   {#1} {#2} }
\NewDocumentCommand \inspiresetyears      { m m }   { \prop_gput:Nnn \g_inspirehep_years_prop {#1} {#2} }
\NewDocumentCommand \inspiresetstat       { m m }   { \prop_gput:Nnn \g_inspirehep_stat_prop  {#1} {#2} }
\NewDocumentCommand \inspiresetauthorstat { m m m } { \prop_gput:Nnn \g_inspirehep_stat_prop  {#1/#2} {#3} }
\NewDocumentCommand \inspiresetauthoryears { m m m } { \prop_gput:Nnn \g_inspirehep_ayears_prop {#1/#2} {#3} }
\NewDocumentCommand \inspiresetfetched    { m m }
  { \tl_gset:Nn \g_inspirehep_fetched_tl {#1} \int_gset:Nn \g_inspirehep_fetchedday_int {#2} }

% \inspiredefault...{...}{...} -- values used only until the real ones arrive.
\NewDocumentCommand \inspiredefaultstat { m m }
  { \prop_if_in:NnF \g_inspirehep_stat_prop {#1} { \inspiresetstat {#1} {#2} } }
\NewDocumentCommand \inspiredefaultauthorstat { m m m }
  { \prop_if_in:NnF \g_inspirehep_stat_prop {#1/#2} { \inspiresetauthorstat {#1} {#2} {#3} } }

%% ===========================================================================
%% Numbers
%% ===========================================================================

\providecommand \inspirenumsep { \l_inspirehep_sep_tl }

\cs_new:Npn \inspirehep_group:n #1
  {
    \int_compare:nNnTF { \tl_count:n {#1} } > { 3 }
      {
        \exp_args:Nf \inspirehep_group:n
          { \tl_range:nnn {#1} { 1 } { \int_eval:n { \tl_count:n {#1} - 3 } } }
        \inspirenumsep
        \tl_range:nnn {#1} { \int_eval:n { \tl_count:n {#1} - 2 } } { \tl_count:n {#1} }
      }
      {#1}
  }
\providecommand \inspirenumformat [1]
  { \exp_args:Nf \inspirehep_group:n { \int_to_arabic:n {#1} } }

% Round DOWN to a multiple of the round= option, so an "over N" claim stays
% true as the real figure grows.
\cs_new:Npn \inspirehep_round:n #1
  {
    \int_compare:nNnTF { \l_inspirehep_round_int } > { 1 }
      { \int_eval:n { \int_div_truncate:nn {#1} { \l_inspirehep_round_int } * \l_inspirehep_round_int } }
      { \int_eval:n {#1} }
  }
\ExplSyntaxOff

\ExplSyntaxOn

%% ===========================================================================
%% Typesetting hooks
%% ===========================================================================

\providecommand \inspiretitleformat  [1] { \textbf {#1} }
\providecommand \inspirecitestext    [1] { #1 \nobreakspace \int_compare:nNnTF {#1} = { 1 } { citation } { citations } }
\providecommand \inspirecitesformat  [1] { \nobreakspace { \small [#1] } }
\providecommand \inspireyearformat   [1] { \nobreakspace (#1) }
\providecommand \inspiremissing      [1] { \textbf { [?~#1] } }

\cs_new:Npn \inspirehep_link:nn #1#2 { #2 }
\AddToHook { begindocument }
  { \cs_if_exist:NT \href { \cs_gset:Npn \inspirehep_link:nn #1#2 { \href {#1} {#2} } } }
%% A record can be named either by its recid (2642414) or by its INSPIRE
%% texkey (Lee:2018pag); the two are told apart by shape, so nothing has to be
%% declared.  Lookups just use whichever string the document wrote, because the
%% fetcher stores the data under that same string.  Only the URL needs the
%% number, which the fetcher supplies alongside when a texkey was used.
\prg_new_conditional:Npnn \inspirehep_is_texkey:n #1 { TF }
  {
    \regex_match:nnTF { \A [A-Za-z] [\w.'\-]* : \d{4} [A-Za-z0-9]+ \Z } {#1}
      { \prg_return_true: } { \prg_return_false: }
  }

%% The URL is built into a variable rather than expanded in place: \href reads
%% its argument under special catcodes and will not tolerate a conditional
%% there, so anything non-expandable has to happen first.
\cs_new_protected:Npn \inspirehep_url:nN #1#2
  {
    \prop_get:NnNF \g_inspirehep_recid_prop {#1} #2 { \tl_set:Nn #2 {#1} }
    \tl_put_left:Nn #2 { https://inspirehep.net/literature/ }
    % Flatten to a string, so every character sits at catcode 12.  hyperref
    % decides between a URL and a local file by looking for a protocol, and
    % under \ExplSyntaxOn a `:' is a letter rather than an ordinary character
    % -- so `https:' did not read as a protocol, \href took the target for a
    % filename, and hyperref appended its default .pdf extension.  Every link
    % the package drew pointed at inspirehep.net/literature/<id>.pdf, which is
    % not a page.
    \tl_set:Nx #2 { \tl_to_str:N #2 }
  }

\cs_new_protected:Npn \inspirehep_lookup:NnnTF #1#2#3
  {
    \prop_get:NnNTF #1 {#2} #3
      { \use_i:nn } { \bool_gset_true:N \g_inspirehep_missing_bool \use_ii:nn }
  }

%% ===========================================================================
%% Records
%% ===========================================================================

%% \inspirecites{<id>} -- the citation annotation alone.
\NewDocumentCommand \inspirecites { m }
  {
    \inspirehep_want:Nn \g_inspirehep_records_seq {#1}
    \prop_get:NnNTF \g_inspirehep_cites_prop {#1} \l_tmpa_tl
      {
        \int_compare:nNnT { \l_tmpa_tl } > { \l_inspirehep_mincites_int - 1 }
          { \inspirecitesformat { \inspirecitestext { \tl_use:N \l_tmpa_tl } } }
      }
      { \bool_gset_true:N \g_inspirehep_missing_bool }
  }

%% \inspiretitle{<id>} -- the title as INSPIRE records it.
\NewDocumentCommand \inspiretitle { m }
  {
    \inspirehep_want:Nn \g_inspirehep_records_seq {#1}
    \inspirehep_lookup:NnnTF \g_inspirehep_title_prop {#1} \l_tmpa_tl
      { \tl_use:N \l_tmpa_tl } { \inspiremissing { title~#1 } }
  }

%% \inspirehep_ref:n -- the reference, assembled from the parts INSPIRE's own
%% renderer produced.  Keeping them apart is what lets `errata' and `collab' be
%% ordinary options rather than choices frozen when the data was fetched.
%%
%% A whole reference set by hand with \inspiresetref wins over the parts, so an
%% entry INSPIRE does not carry can still be written out in full.
\providecommand \inspirecollabformat [1] { #1~Collaboration }

\cs_new_protected:Npn \inspirehep_ref:n #1
  {
    \prop_get:NnNTF \g_inspirehep_ref_prop {#1} \l_tmpa_tl
      { \tl_use:N \l_tmpa_tl }
      {
        % who
        \bool_lazy_and:nnTF
          { \l_inspirehep_collab_bool }
          { \prop_if_in_p:Nn \g_inspirehep_refcollab_prop {#1} }
          {
            \prop_get:NnN \g_inspirehep_refcollab_prop {#1} \l_tmpa_tl
            \inspirecollabformat { \tl_use:N \l_tmpa_tl }
          }
          {
            \prop_get:NnNT \g_inspirehep_refauthors_prop {#1} \l_tmpa_tl
              { \tl_use:N \l_tmpa_tl }
          }
        % title
        \prop_get:NnNT \g_inspirehep_title_prop {#1} \l_tmpa_tl
          { ,~ `` \tl_use:N \l_tmpa_tl ,'' }
        % where
        \prop_get:NnNT \g_inspirehep_refpub_prop {#1} \l_tmpa_tl
          { ~ \tl_use:N \l_tmpa_tl }
        % and the erratum, if asked for
        \bool_if:NT \l_inspirehep_errata_bool
          {
            \prop_get:NnNT \g_inspirehep_referrata_prop {#1} \l_tmpa_tl
              { ~ \tl_use:N \l_tmpa_tl }
          }
      }
  }

\cs_new_protected:Npn \inspirehep_has_ref:nTF #1
  {
    \bool_lazy_or:nnTF
      { \prop_if_in_p:Nn \g_inspirehep_ref_prop {#1} }
      { \prop_if_in_p:Nn \g_inspirehep_refpub_prop {#1} }
  }

%% \inspireref[<options>]{<id>} -- the full reference.
\NewDocumentCommand \inspireref { O{} m }
  {
    \group_begin:
      \keys_set:nn { inspirehep } {#1}
      \inspirehep_want:Nn \g_inspirehep_records_seq {#2}
      \inspirehep_has_ref:nTF {#2}
        { \inspirehep_ref:n {#2} }
        { \bool_gset_true:N \g_inspirehep_missing_bool \inspiremissing { ref~#2 } }
      \bool_if:NT \l_inspirehep_cites_bool { \inspirecites {#2} }
    \group_end:
  }

%% \inspirekey{<id>} -- the BibTeX key, and \inspirecite{<id>} -- \cite with it.
\NewDocumentCommand \inspirekey { m }
  {
    \inspirehep_want:Nn \g_inspirehep_bibs_seq {#1}
    \prop_get:NnNTF \g_inspirehep_key_prop {#1} \l_tmpa_tl
      { \tl_use:N \l_tmpa_tl }
      {
        % Given a texkey, that is already the answer -- so \inspirecite works
        % before anything has ever been fetched.  Given a recid we cannot know
        % it yet, so say so and ask the next fetch for it.
        \bool_gset_true:N \g_inspirehep_missing_bool
        \inspirehep_is_texkey:nTF {#1} {#1} { \inspiremissing { key~#1 } }
      }
  }
%% \cite turns its argument into a control sequence, so it cannot be handed an
%% unexpanded \inspirekey -- the key is looked up first and passed by value.
\NewDocumentCommand \inspirecite { m }
  {
    \inspirehep_want:Nn \g_inspirehep_bibs_seq {#1}
    \prop_get:NnNTF \g_inspirehep_key_prop {#1} \l_tmpa_tl
      { \exp_args:NV \cite \l_tmpa_tl }
      {
        \bool_gset_true:N \g_inspirehep_missing_bool
        \inspirehep_is_texkey:nTF {#1}
          { \cite {#1} }                       % a texkey is already the key
          { \inspiremissing { key~#1 } }
      }
  }

%% \inspirepub[<options>]{<id>} -- a publication entry.
%%
%% With no options it is the title, linked, with its citation count.  The title
%% comes from INSPIRE unless title= supplies one; ref= swaps the title for the
%% full reference; year= appends the publication year; cites=false drops the
%% count; link=false drops the hyperlink.
\NewDocumentCommand \inspirepub { O{} m }
  {
    \group_begin:
      \keys_set:nn { inspirehep } {#1}
      \inspirehep_want:Nn \g_inspirehep_records_seq {#2}
      \bool_if:NTF \l_inspirehep_ref_bool
        {
          \inspirehep_has_ref:nTF {#2}
            { \inspirehep_show:nn {#2} { \inspirehep_ref:n {#2} } }
            { \bool_gset_true:N \g_inspirehep_missing_bool \inspiremissing { ref~#2 } }
        }
        {
          \tl_if_empty:NTF \l_inspirehep_title_tl
            {
              \inspirehep_lookup:NnnTF \g_inspirehep_title_prop {#2} \l_tmpa_tl
                { \inspirehep_show:nn {#2} { \inspiretitleformat { \tl_use:N \l_tmpa_tl } } }
                { \inspiremissing { title~#2 } }
            }
            { \inspirehep_show:nn {#2} { \inspiretitleformat { \tl_use:N \l_inspirehep_title_tl } } }
        }
      \bool_if:NT \l_inspirehep_cites_bool { \inspirecites {#2} }
      \bool_if:NT \l_inspirehep_year_bool { \inspireyear {#2} }
    \group_end:
  }

\tl_new:N \l_inspirehep_url_tl
\cs_new_protected:Npn \inspirehep_show:nn #1#2
  {
    \bool_if:NTF \l_inspirehep_link_bool
      {
        \inspirehep_url:nN {#1} \l_inspirehep_url_tl
        \exp_args:NV \inspirehep_link:nn \l_inspirehep_url_tl {#2}
      }
      {#2}
  }

%% \inspireyear{<id>} -- the publication year, taken from the reference.
\NewDocumentCommand \inspireyear { m }
  {
    \inspirehep_want:Nn \g_inspirehep_records_seq {#1}
    \prop_get:NnNTF \g_inspirehep_refpub_prop {#1} \l_tmpa_tl
      {
        \regex_extract_once:nVNTF { \((\d{4})\) } \l_tmpa_tl \l_tmpa_seq
          { \inspireyearformat { \seq_item:Nn \l_tmpa_seq { 2 } } }
          { }
      }
      { \bool_gset_true:N \g_inspirehep_missing_bool }
  }

%% ===========================================================================
%% Authors
%% ===========================================================================
%% Each takes the id it is asking about, so one document can discuss several
%% people.  The id is the number in a profile URL, inspirehep.net/authors/N,
%% or a BAI such as J.Smith.1.

\NewDocumentCommand \inspireauthorstat { O{} m m }
  {
    \group_begin:
      \keys_set:nn { inspirehep } {#1}
      \inspirehep_want:Nn \g_inspirehep_authors_seq {#2}
      \inspirehep_lookup:NnnTF \g_inspirehep_stat_prop {#2/#3} \l_tmpa_tl
        { \inspirenumformat { \inspirehep_round:n { \l_tmpa_tl } } }
        { \inspiremissing {#3} }
    \group_end:
  }

\NewDocumentCommand \inspirepapers    { O{} m } { \inspireauthorstat [#1] {#2} { papers    } }
\NewDocumentCommand \inspirecitations { O{} m } { \inspireauthorstat [#1] {#2} { citations } }
\NewDocumentCommand \inspirehindex    { O{} m } { \inspireauthorstat [#1] {#2} { hindex    } }
\ExplSyntaxOff

\ExplSyntaxOn

%% ===========================================================================
%% Plots
%% ===========================================================================
%% A year series arrives as "2019:1,2020:7,..." and is drawn with pgfplots,
%% which is loaded only if a plot is actually asked for -- a CV that never
%% plots anything should not pay for the dependency.
%%
%% The default style is deliberately plain: one line, no grid, no legend, no
%% box, axis lines only where they carry information.  Everything is
%% overridable, and \inspireplotstyle is where to put house style once.

\cs_new_protected:Npn \inspirehep_need_pgfplots:
  {
    \cs_if_exist:NF \pgfplotsset
      {
        \PackageError { inspirehep }
          { plotting~needs~pgfplots }
          { Pass~`plots'~to~\string\usepackage{inspirehep},~or~load~pgfplots~yourself. }
      }
  }

\providecommand \inspireplotstyle {}
\providecommand \inspireplotwidth { 0.8\linewidth }
\providecommand \inspireplotheight { 4cm }

%% The picture itself is built at normal catcodes: pgfplots keys are full of
%% characters that \ExplSyntaxOn reinterprets, and the coordinate list has to
%% keep its spaces.  expl3 only prepares the coordinates and hands them over.
\ExplSyntaxOff
\newcommand{\inspirehepdrawplot}[2]{%
  \begin{tikzpicture}
    \begin{axis}[
        width=\inspireplotwidth, height=\inspireplotheight,
        axis lines=left, axis line style={-,gray!60},
        tick style={gray!60}, tick align=outside,
        xtick distance=5, scaled y ticks=false,
        y tick label style={/pgf/number format/1000 sep={\inspirenumsep}},
        x tick label style={/pgf/number format/1000 sep={}},
        ylabel={#2}, ylabel style={font=\small},
        tick label style={font=\small},
        % A little headroom above the peak: without it the highest point sits
        % exactly on the top edge, where a thick line reads as clipped.  Only
        % the upper bound moves, so the axis still starts at zero.
        ymin=0, enlarge y limits={upper, value=0.08},
        enlarge x limits=0.03, clip=false,
        \inspireplotstyle
      ]
      \addplot[thick, mark=none, black] coordinates {#1};
    \end{axis}
  \end{tikzpicture}%
}
\ExplSyntaxOn

\cs_new_protected:Npn \inspirehep_draw:nn #1#2
  {
    \inspirehep_need_pgfplots:
    \inspirehepdrawplot {#1} {#2}
  }

%% \inspireplot[<options>]{<id>} -- citations per year for one paper.
\NewDocumentCommand \inspireplot { O{} m }
  {
    \group_begin:
      \keys_set:nn { inspirehep } {#1}
      \inspirehep_want:Nn \g_inspirehep_plots_seq {#2}
      \inspirehep_want:Nn \g_inspirehep_records_seq {#2}
      \inspirehep_lookup:NnnTF \g_inspirehep_years_prop {#2} \l_tmpa_tl
        { \inspirehep_draw:nn { \l_tmpa_tl } { citations } }
        { \inspiremissing { plot~#2 } }
    \group_end:
  }

%% \inspireauthorplot[<options>]{<id>}{papers|citations} -- per year, for a
%% person: how much they published, or how much they were cited.
\NewDocumentCommand \inspireauthorplot { O{} m m }
  {
    \group_begin:
      \keys_set:nn { inspirehep } {#1}
      \inspirehep_want:Nn \g_inspirehep_authors_seq {#2}
      \inspirehep_lookup:NnnTF \g_inspirehep_ayears_prop {#2/#3} \l_tmpa_tl
        { \inspirehep_draw:nn { \l_tmpa_tl } {#3} }
        { \inspiremissing { plot~#2/#3 } }
    \group_end:
  }

%% ===========================================================================
%% Refreshing the data
%% ===========================================================================
%% The package can refresh everything itself through TeX's shell escape, by
%% handing the work to inspirehep-fetch.py -- which is shipped alongside it and
%% already knows how to read the sources and write the file.  That keeps one
%% implementation of the API work instead of two that can disagree.
%%
%% Without shell escape nothing is fetched and the committed data file is used
%% as-is, which is what happens on Overleaf.  See the README.

\bool_new:N \g_inspirehep_stale_bool
\bool_new:N \g_inspirehep_canfetch_bool

\cs_new:Npn \inspirehep_dayno:
  { \int_eval:n { 365 * \c_sys_year_int + 31 * \c_sys_month_int + \c_sys_day_int } }
\cs_new:Npn \inspirehep_datafile: { \g_inspirehep_data_tl .tex }

\str_case:VnF \g_inspirehep_fetch_tl
  {
    { off } { }
    { on }
      {
        \sys_if_shell_unrestricted:TF
          { \bool_gset_true:N \g_inspirehep_canfetch_bool }
          { \PackageWarningNoLine { inspirehep }
              { fetch=on~but~unrestricted~shell~escape~is~off. \MessageBreak
                Compile~with~-shell-escape,~run~inspirehep-fetch.py,~or~set~fetch=off } }
      }
    { auto } { \sys_if_shell_unrestricted:T { \bool_gset_true:N \g_inspirehep_canfetch_bool } }
  }
  { \PackageWarningNoLine { inspirehep } { unknown~fetch=\g_inspirehep_fetch_tl } }

\AddToHook { begindocument }
  {
    \file_if_exist:nTF { \inspirehep_datafile: }
      { \file_input:n { \inspirehep_datafile: } }
      { \bool_gset_true:N \g_inspirehep_stale_bool }
    \int_compare:nNnT { \g_inspirehep_fetchedday_int } > { 0 }
      {
        \int_compare:nNnT
          { \inspirehep_dayno: - \g_inspirehep_fetchedday_int } > { \g_inspirehep_maxage_int }
          {
            \bool_gset_true:N \g_inspirehep_stale_bool
            \bool_if:NF \g_inspirehep_canfetch_bool
              { \PackageWarningNoLine { inspirehep }
                  { The~data~dates~from~\g_inspirehep_fetched_tl\space (about~
                    \int_eval:n { \inspirehep_dayno: - \g_inspirehep_fetchedday_int }~days~ago).
                    \MessageBreak Refresh~with~-shell-escape~or~inspirehep-fetch.py } }
          }
      }
  }

\AddToHook { enddocument / afterlastpage }
  {
    \bool_lazy_and:nnT
      { \g_inspirehep_canfetch_bool }
      { \bool_lazy_or_p:nn { \g_inspirehep_stale_bool } { \g_inspirehep_missing_bool } }
      {
        \PackageInfo { inspirehep } { refreshing~from~inspirehep.net }
        \sys_shell_now:x
          {
            % The separators below are \c_space_tl, not ~, after a variable.
            % Under \ExplSyntaxOn a ~ is an ordinary space token, and TeX
            % discards spaces following a control word -- so a ~ written
            % straight after \g_inspirehep_script_tl is swallowed as that
            % csname's terminator, and the script name runs into the file
            % name.  A ~ after a plain character (python3, --output) is
            % fine, which is why only some of these need it.
            python3~\g_inspirehep_script_tl \c_space_tl
            \c_sys_jobname_str .tex \c_space_tl
            --output~\g_inspirehep_data_tl .tex \c_space_tl
            --bib~\g_inspirehep_bib_tl .bib \c_space_tl
            --style~\g_inspirehep_style_tl
          }
        \PackageWarningNoLine { inspirehep }
          { The~data~was~refreshed.~Rerun~LaTeX~to~pick~it~up }
      }
  }

\ExplSyntaxOff
\endinput
